Get started in five minutes → - Clone, configure, bootstrap, and ship.
A Cloudflare-native monorepo for SaaS apps and content sites, built with pnpm workspaces, Turborepo, and Cloudflare Workers.
- Cloudflare-first: Workers, D1, KV, R2, Queues, and Durable Objects are built in.
- SaaS foundations: auth, RBAC, OttaORM, forms, uploads, realtime, queues, and a blog/CMS.
- Fat models keep domain logic close to data instead of scattering it across controllers and services.
- One monorepo for integrated app and package changes.
▲ You own the code.
Ottabase is a monorepo you clone and modify, not an npm package. When you fork it, you own:
- Upstream changes - merge or rebase updates and resolve any conflicts yourself.
- Schema and data migrations - reconcile upstream changes with your production data; back up first.
- Infrastructure, security, and costs - deploy to and operate your own Cloudflare account.
- Breaking changes - adapt your customizations when an upstream API or tool changes.
Recommended: Read ARCHITECTURE.md, CONTRIBUTING.md, and SECURITY.md before making structural changes.
- Docs: Quick Start, README, Architecture, Changelog, Releases
- Build infrastructure: Self-hosted Turborepo remote cache, Cloudflare deployment
- Project health: Contributing, Code of Conduct, Security, Support, Maintainers
- Demo: demo.ottabase.com
- Community: GitHub Discussions
| Capability | Ottabase | DIY Stack (Next.js + Supabase + Stripe + etc) |
|---|---|---|
| Setup complexity | ✓ Minimal | ▲ High |
| Integration effort | ✓ None | ▲ High |
| Time to MVP / App Launch | » Hours to days | ◔ Weeks to months |
| Capability | Ottabase | Supabase | Railway | Convex |
|---|---|---|---|---|
| Primary model | Edge-native full-stack framework (monorepo) | Backend-as-a-service | App hosting platform | Realtime backend platform |
| Best for | Solo founders shipping SaaS - super fast | Backend-heavy apps / Firebase alt | Simple deployments | Realtime-first apps |
| Opinionation level | ✓ Strong (batteries-included) | ◑ Medium | ✗ Low | ◑ Medium |
| Build vs Configure | Build product directly | Configure backend + build app | Build everything yourself | Build within platform constraints |
| Capability | Ottabase | Supabase | Railway | Convex |
|---|---|---|---|---|
| Time to MVP | » Hours to days | ◔ Days–weeks | ◔ Weeks | ◔ Days |
| Full-stack starter in your repo | ✓ Yes | ✗ No | ✗ No | ◑ Partial |
| Model-driven CRUD primitives | ✓ Built in | ◑ DIY | ✗ No | ◑ Partial |
| RBAC + multi-tenant SaaS primitives | ✓ Included | ◑ DIY | ✗ No | ◑ DIY |
| UI/component packages included | ✓ Yes | ✗ No | ✗ No | ✗ No |
| Blog/CMS package included | ✓ Yes | ✗ No | ✗ No | ✗ No |
| Type safety (TS-first) | ✓ Deeply integrated | ◑ Partial | ◑ Depends on stack | ✓ Strong |
| Local dev experience | ✓ Unified | ◑ Mixed | ◑ Basic | ✓ Good |
| Capability | Ottabase | Supabase | Railway | Convex |
|---|---|---|---|---|
| Edge-native (global) | ✓ Cloudflare Workers-first | ✗ No | ✗ No | ✗ No |
| AI Gateway (global) | ✓ Cloudflare Workers-first | ✗ No | ✗ No | ✗ No |
| Global latency | ✓ Low (edge execution) | ◑ Region-based | ◑ Region-based | ◑ Region-based |
| Cold starts | ✓ Minimal | ◑ Possible | ◑ Possible | ◑ Possible |
| Built-in CDN / caching | ✓ Native (Cloudflare) | ✗ No | ✗ No | ✗ No |
| Queues / cron support | ✓ Native (CF bindings) | ◑ Limited / external | ◑ External | ◑ Limited |
| Cost predictability | ✓ High | ◑ Can scale unpredictably | ◑ Infra-based | ◑ Usage-based |
| Vendor lock-in | ◑ High (Framework + infra coupling) | ◑ Medium (infra coupling) | ◑ Low | ▲ High (proprietary model) |
ottabase/
├── apps/
│ ├── otta-web/ # TanStack Router + Vite + Workers (primary)
│ ├── otta-landing/ # Next.js + OpenNext public landing site (content edited in otta-web admin)
│ ├── otta-worker/ # Single Worker: server-rendered + API, same package contract, no SPA
│ └── otta-cache/ # Self-hosted Turborepo remote cache on Workers + R2
├── packages/
│ ├── ottaorm/ # Fat models, auto-migrations, CRUD, RLS
│ ├── ottarouter/ # Cloudflare Workers router (order-free precedence, middleware)
│ ├── db/ # Drizzle D1 driver
│ ├── cf/ # Cloudflare bindings (D1, KV, R2, Queues, Vectorize, Workers AI, Browser, Workflows, DO alarms)
│ ├── cf-realtime/ # WebSocket pub/sub (Durable Objects)
│ ├── queue/ # Job queue (dispatch, handlers, priority)
│ ├── kernel/ # The package contract: one manifest, one registry, one list
│ ├── events/ # Typed in-process event bus with queue fan-out
│ ├── flags/ # Entitlements: plans, features, limits, 402 guards
│ ├── meter/ # Usage credits: atomic balances, periods, ledger
│ ├── apikeys/ # Hashed, scoped, revocable API keys for your customers
│ ├── postmaster/ # Inbound mail, suppression list, one-click unsubscribe, bounce webhooks
│ ├── vault/ # D1 backups to R2 as NDJSON, restore, per-tenant export
│ ├── fleet/ # The beacon: GET /api/fleet/report for whatever watches your apps
│ ├── auth/ # Lightweight custom auth (sessions, OAuth, magic links) with D1
│ ├── rbac/ # Role-based access control with KV caching
│ ├── audit/ # Audit logging with change tracking
│ ├── analytics/ # Cloudflare Analytics Engine (WAE)
│ ├── notifications/ # Multi-channel notifications (email, WebSocket)
│ ├── shortlinks/ # URL shortener with interstitial + WAE tracking
│ ├── ottalanding/ # DB-backed landing sites: typed sections + 3 themes
│ ├── referrals/ # Referral tracking (first-touch, WAE)
│ ├── brand-engine/ # Design tokens, preset expansion, CSS injection
│ ├── brand-engine-react/ # BrandProvider, LayoutResolver, useBrand()
│ ├── ottalayout/ # Layout types, presets, path resolver, React slots
│ ├── ottablog/ # Blog/CMS (articles, blurbs, photo journals, taxonomy, series, Studio)
│ ├── email/ # Email sending (Resend, SES, MailChannels, SMTP)
│ ├── cron/ # Cron handlers (static + DB scheduler)
│ ├── logger/ # Structured logging (multi-transport)
│ ├── config/ # App config, env vars, storage keys
│ ├── scripts/ # CLI: cf:setup, cf:validate, cf:login, clean:*
│ ├── state/ # Jotai atoms (theme, user, sidebar)
│ ├── ui/ # The UI kit: Radix primitives, DS v1 pieces, ConfirmDialog, SplitPane, tokens
│ ├── ui-mantine/ # Mantine provider + themes (optional adapter)
│ ├── ui-code-highlight/ # Code syntax highlighting
│ ├── ottaeditor/ # EditorJS wrapper with 15+ plugins
│ ├── ottaupload/ # File uploads (R2, CF Images)
│ ├── ottarenderer/ # EditorJS block renderer
│ ├── ottaselect/ # Headless select/combobox
│ ├── ui-cropper/ # Vanilla JS image cropper (~3-4 KB)
│ ├── spotlight/ # Command palette
│ ├── docs/ # Markdown doc viewer
│ ├── forms/ # Auto-generated CRUD forms from OttaORM models
│ ├── i18n/ # i18next wrapper (en, es, fr, de)
│ ├── api/ # Type-safe fetch wrapper
│ └── utils/ # Timezone, string, file, URL utilities
└── turbo.json
Note: The structure list is curated for readability; the source of truth for all publishable modules is
packages/*/package.json.
- Node.js:
>=24.0.0 - pnpm:
>=10.0.0 - Windows Users: Ensure Visual C++ Redistributable is installed for builds to work correctly.
# Install
pnpm install
# Build packages (required first time)
pnpm build:pkg
# Start the default app (otta-web: Vite + Wrangler)
pnpm dev
# Initialize database
curl -X POST http://localhost:3004/api/ottaorm/initpnpm otta is the single app-scoped interface. It discovers apps from apps/*, uses the root default when the app is
omitted, and accepts either an app directory name or package name.
# Start apps
pnpm otta start # Default app, development
pnpm otta start otta-landing # A specific app
pnpm otta start otta-web --env stage # Built Worker locally with env.staging
pnpm otta start otta-web --env preview # Built Worker locally with env.preview
pnpm otta start otta-web --env prod # Built Worker locally with env.production
pnpm otta dev otta-web --process worker # One process from the dev topology
# Scaffold a new app
pnpm otta new web my-app # Vite + TanStack Router + Workers
pnpm otta new landing my-site # Next.js landing page
pnpm otta new worker my-tool # Single Worker, server-rendered + API, no SPA
# Install a package into an app (dependency, composition, list entry, client key, tables)
pnpm otta add postmaster my-app
pnpm otta add vault my-tool --dry-run
# App-scoped quality commands
pnpm otta build otta-web
pnpm otta test otta-web
pnpm otta lint otta-web
pnpm otta listNamed environment starts are always local, require the environment to exist in wrangler.jsonc, and pass --local to
Wrangler; deployment remains a separate CI/manual operation. Supervised output is labeled by process, and the entire
process tree is stopped when one member fails. The workspace CLI rebuilds itself only when its source is newer than its
compiled output. See pnpm otta --help or the CLI README for the lifecycle contract and
options.
Central to the codebase. Each model contains schema, validation, relationships, and methods.
// ottabase/models/Todo.ts
import { BaseModel } from '@ottabase/ottaorm';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const todosTable = sqliteTable('todos', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
completed: integer('completed', { mode: 'boolean' }).default(false).notNull(),
userId: text('user_id'),
createdAt: integer('created_at').$defaultFn(() => Date.now()),
});
export class Todo extends BaseModel {
static entity = 'todos';
static table = todosTable;
static primaryKey = 'id';
static casts = {
completed: 'boolean' as const,
createdAt: 'date' as const,
};
// Relationship
async user() {
const { User } = await import('@ottabase/ottaorm');
return this.belongsTo(User, 'userId');
}
// Custom methods
static async incomplete() {
return this.where({ completed: false });
}
async toggle() {
this.set('completed', !this.get('completed'));
return this.save();
}
}// ottabase/db/schema.ts
export { usersTable, postsTable } from '@ottabase/ottaorm'; // Core
export { todosTable } from '../models/Todo'; // Appimport { registerConnection } from '@ottabase/ottaorm';
import { createD1Driver } from '@ottabase/db/drizzle-d1';
import { Todo } from './ottabase/models/Todo';
// In worker
registerConnection('default', createD1Driver(env.OBCF_D1));
// CRUD
const todo = await Todo.create({ title: 'Buy groceries' });
const all = await Todo.all();
const one = await Todo.find('id');
await todo.toggle();
await todo.delete();Tables created automatically from schema:
curl -X POST http://localhost:3004/api/ottaorm/initAdd columns by updating schema and re-running init.
// ottabase/hooks/useTodo.ts
import { createModelHooks } from '@ottabase/ottaorm/client';
import type { TodoType } from '@/ottabase/models/Todo';
export const {
useList: useTodos,
useDetail: useTodo,
useCreate: useCreateTodo,
useUpdate: useUpdateTodo,
useDelete: useDeleteTodo,
} = createModelHooks<TodoType>({ entityName: 'todos' });// Usage in component
const { data: todos } = useTodos();
const createTodo = useCreateTodo();
createTodo.mutate({ title: 'New Todo' });| Package | Purpose |
|---|---|
@ottabase/ottaorm |
Fat models, CRUD, relationships, RLS, auto-migrations |
@ottabase/ottarouter |
Cloudflare Workers router - order-free precedence, middleware, mounts |
@ottabase/db |
Drizzle D1 driver (createD1Driver) |
@ottabase/cf |
D1, KV, R2, Queues, Rate Limiting, Vectorize, Workers AI, Browser Rendering, Workflows, DO alarms, Cache Keys |
@ottabase/cf-pdf |
Cloudflare HTML-to-PDF export with Browser Rendering and safe downloads |
@ottabase/screenshot |
Automated screenshots (png/jpg/webp, quality, optimization pass): Node runner + Browser Rendering route |
@ottabase/queue |
Job queue (dispatch, handlers, deduplication, chaining, priority) |
@ottabase/auth |
Lightweight custom auth: signed sessions, OAuth, Credentials, Magic Link |
@ottabase/rbac |
Role-based access control with per-org KV caching |
@ottabase/audit |
Audit logging with change tracking and RBAC context |
@ottabase/logger |
Structured logging (Console, HTTP, Sentry, Memory, Buffer transports) |
@ottabase/analytics |
Cloudflare Analytics Engine (WAE) - write events, query, funnel, top-K |
@ottabase/config |
App config, env vars, storage key utilities |
@ottabase/cron |
Cron handlers - static code-defined and DB scheduler (Laravel-style) |
@ottabase/scripts |
CLI tools: cf:login, cf:setup, cf:validate, clean:* |
The contract every package is built on, and the primitives a SaaS needs on day one. Each is one entry in
ottabase/packages.ts (or otta add <name> <app>).
| Package | Purpose |
|---|---|
@ottabase/kernel |
The package contract: definePackage, the registry, mountPackages, health, lifecycle |
@ottabase/events |
Typed in-process event bus; queued subscribers fan out through OBCF_QUEUE |
@ottabase/flags |
Entitlements: plans, features, limits, operator overrides, 402 guards, client PlanGate |
@ottabase/legal |
Terms, privacy (GDPR/CCPA), cookies, acceptable use, refunds and sub-processors from one config: clause toggles, overrides, Worker routes and React |
@ottabase/meter |
Usage credits: atomic per-tenant balances, periods, top-ups, a pruned ledger, 402 guard |
@ottabase/apikeys |
API keys for your customers: hashed, scoped, expiring, revocable, shown once |
@ottabase/fleet |
The beacon: GET /api/fleet/report behind FLEET_TOKEN (packages, licenses, health, cron, backup) |
@ottabase/discover |
AI-readable by default: robots.txt naming the AI search crawlers, sitemap, /.well-known/otta.json, JSON-LD, the canonical paragraph |
@ottabase/postmaster |
Inbound mail (Email Routing), suppression list, one-click unsubscribe, Resend/SES bounces |
Sell add-ons for your app, or install someone else's. A Premium Package is an ordinary Ottabase package manifest
(@ottabase/kernel) plus a commercial layer, so installing it is the same one entry in ottabase/packages.ts as any
free package. Rendered pages and Drizzle's static schema export are explicit build-time adapters, kept in sync by the
app's packages-registration test. Licenses are signed tokens verified offline (Web Crypto), so nothing phones home.
An app with no Premium Packages is completely unaffected: nothing mounts and nothing is fetched.
| Package | Purpose |
|---|---|
@ottabase/premium |
Manifests, license verification, entitlements, lifecycle, gated routes, admin UI |
@ottabase/webhooks |
Worked example: HMAC-signed outbound webhooks, free tier + licensed delivery log |
@ottabase/vault |
D1 backups to R2 with restore free (5 kept); nightly schedule, retention, tenant export licensed |
Ottabase Pro is the commercial bundle built this way: Tower (every app you run, on one board), PayPort (sell plans to your app's users), Blackbox (error tracking), Beacon (a status page), Workbench (the database inside the admin), Helpdesk (a support inbox with the customer's plan and errors beside it) and Privacy (GDPR and CCPA export and deletion requests, each with its deadline), sold as source with access to a private repository as the license. Three yearly licenses, priced by how many people commit (Solo, Team, Company); cancel and the code you pulled stays yours. See ottabase.com/pro.
| Package | Purpose |
|---|---|
@ottabase/brand-engine |
Design tokens, preset expansion, CSS injection, email branding |
@ottabase/brand-engine-react |
BrandProvider, LayoutResolver, useBrand() React bindings |
@ottabase/ottalayout |
Layout types, 10 presets, path resolver, React slots, LayoutMeta |
@ottabase/ottablog |
Blog/CMS models (Post, Category, Tag, Series, Version) + Blog Studio |
| Package | Purpose |
|---|---|
@ottabase/ui |
The UI kit: Radix primitives, Design System v1 pieces, ConfirmDialog, MessageBox, DarkModeToggle, SplitPane, JsonEditor, UIProviders, tokens and base styles |
@ottabase/ui-mantine |
Mantine provider, pre-built themes (optional adapter) |
@ottabase/ui-code-highlight |
Code syntax highlighting (highlight.js) |
@ottabase/ottaeditor |
EditorJS wrapper with 15+ plugins (Spoiler, CTA, Review) |
@ottabase/ottaupload |
File upload component (R2, Cloudflare Images) |
@ottabase/ottarenderer |
EditorJS block renderer for React |
@ottabase/ottaselect |
Headless select/combobox component |
@ottabase/ui-cropper |
Vanilla JS image cropper (~3-4 KB, zero deps) |
@ottabase/spotlight |
Spotlight/command palette component |
@ottabase/docs |
Markdown doc viewer with layout themes |
@ottabase/forms |
Auto-generated CRUD forms from OttaORM models |
| Package | Purpose |
|---|---|
@ottabase/cf-realtime |
WebSocket pub/sub via Durable Objects (Pusher alternative) |
@ottabase/shortlinks |
URL shortener: short codes, interstitial, expiry, WAE clicks |
@ottabase/ottalanding |
Landing sites in D1: typed sections, admin editor, 3 themes |
@ottabase/referrals |
Referral tracking - first-touch attribution, WAE clicks |
@ottabase/notifications |
Multi-channel notifications (email, WebSocket, system) |
| Package | Purpose |
|---|---|
@ottabase/state |
Jotai atoms (theme, user, sidebar, org) |
@ottabase/utils |
Timezone, string, file, URL, git utilities |
@ottabase/api |
Type-safe fetch wrapper with deduping and error types |
@ottabase/email |
Email sending (Resend, SES, MailChannels, Nodemailer) |
@ottabase/i18n |
i18next wrapper (en, es, fr, de) |
Multiple apps can share a single database using the optional appId column.
| Mode | scopeByAppId |
appId column |
Behavior |
|---|---|---|---|
| Default | false |
null |
Single app, no filtering |
| Multi-app | true |
"my-app" |
Auto-inject/filter by appId |
import { createAppConfig } from '@ottabase/config';
const config = createAppConfig({
appId: 'my-unique-app-id',
defaults: {
features: { scopeByAppId: true }, // Enable appId scoping
},
});| Variable | Default | Description |
|---|---|---|
APP_ID |
"otta-web" |
Unique app identifier |
SCOPE_BY_APP_ID |
"false" |
Enable appId scoping for DB |
All models include a nullable appId column:
@ottabase/ottaormcore models (User, Session, Account, Post, Tag, etc.)@ottabase/shortlinksfat model@ottabase/referralsfat model
Full-stack SPA (TanStack Router, OttaORM, Auth, RBAC, all CF bindings):
# Unix/macOS: cp -r apps/otta-web apps/my-new-app
# Windows: xcopy /E /I apps\otta-web apps\my-new-app
cd apps/my-new-app
# Update package.json name
# Delete src/pages/demo/ (optional - remove demo pages)Marketing site (Next.js, OpenNext): apps/otta-landing renders the landing content you edit at
/admin/content/landing in otta-web, from the shared D1 database. Pick a theme and edit pages there, with no code
changes. See apps/otta-landing/README.md.
Single Worker (otta new worker my-tool): apps/otta-worker is the same package contract without the SPA. One
Worker serves a server-rendered page and an API, runs the scheduler and the queue over the manifests, creates its tables
from POST /api/admin/init, and uses one operator token until you add @ottabase/auth. For internal tools, webhook
receivers, cron-driven jobs and first versions. See apps/otta-worker/README.md.
When a package owns its tables (like @ottabase/shortlinks), the model and schema live together in the package.
// packages/shortlinks/src/Shortlink.ts
export const shortlinksTable = sqliteTable("shortlinks", { ... });
export class Shortlink extends BaseModel {
static entity = "shortlinks";
static table = shortlinksTable;
}import { Shortlink } from '@ottabase/shortlinks';
registerModels([Shortlink]);// ottabase/db/schema.ts
export { shortlinksTable } from '@ottabase/shortlinks';// ottabase/hooks/useShortlink.ts
import { createModelHooks } from "@ottabase/ottaorm/client";
export const { useList, useCreate, ... } = createModelHooks({ entityName: "shortlinks" });pnpm commands # Every script in one annotated table — start here
pnpm commands clean # Filter to a topicThe table is generated from package.json, so it always matches the scripts that actually exist. The essentials:
pnpm dev # Start the default app (alias for pnpm otta start)
pnpm otta --help # Parameterized app/environment lifecycle
pnpm build # Build everything
pnpm build:pkg # Build packages only
pnpm test # Run tests
pnpm lint # Lint
pnpm type-check # TypeScript check
pnpm storybook # Component docscd apps/otta-web
pnpm wrangler login
pnpm deploy
# Run migrations
curl -X POST https://your-app.workers.dev/api/ottaorm/init \
-H "Authorization: Bearer ${MIGRATION_SECRET}"One rule decides where a package lives: OSS helps you build, Pro helps you operate.
- OSS (MIT, this repo): everything needed to build and ship an app. The framework, ORM, auth, RBAC, UI, content and utilities. Nothing published here ever moves behind a paywall.
- Pro (paid, sold as source): the operations layer for a running business. Billing, fleet health, error tracking, status pages, support, compliance. See ottabase.com/pro.
Litmus test: needed on day one of building, OSS. Needed once paying customers arrive, Pro.
- Architecture
- Changelog
- Contributing Guide
- Security Policy
- Template App README
- OttaORM README
- Cloudflare Deploy
- Cloudflare Config
- Testing