✨ Get Started in 5 Minutes → - Clone, install, bootstrap via GUI, and ship. Full setup from zero to running.
Modern full-stack monorepo with pnpm workspaces and Turborepo. Deploy to Cloudflare Workers with D1, KV, R2, Queues, and Durable Objects.
- Cloudflare-first by design: Workers, D1, KV, R2, Queues, and Durable Objects are first-class citizens.
- Batteries included for SaaS work: auth, RBAC, OttaORM, forms, docs, realtime, queues, and blog/CMS live in one repo.
- Fat-model architecture keeps domain logic close to data instead of scattering it across controllers and services.
- Monorepo ergonomics let you ship integrated changes across apps and packages without version skew.
⚠️ You own the code.Ottabase is a monorepo you clone and modify, not an npm package you install. Once you fork it, you accept full responsibility for:
- Git history & upstream merges - pulling upstream changes may cause merge conflicts across apps, packages, schemas, and config files. There is no
npm update; you rebase or merge manually.- Schema & data migrations - upstream schema changes (new columns, renamed tables, config format changes) must be reconciled with your own models and production data. Back up before migrating.
- Infrastructure & costs - you deploy to your own Cloudflare account. D1 storage, KV operations, R2 bandwidth, Workers invocations, and Queues usage are billed to you.
- Security & compliance - you are responsible for patching dependencies, securing API keys, configuring RLS correctly, and auditing access in your deployment.
- Breaking changes - upstream releases may introduce breaking changes to internal APIs, package interfaces, or build tooling. Migration guides are provided when possible, but your customizations are your own to reconcile.
Recommended: Read ARCHITECTURE.md, CONTRIBUTING.md, and SECURITY.md before making structural changes.
- Docs: Quick Start, README, Architecture, Changelog, Releases
- 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) | ❌ Low | ||
| 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 | |
| Model-driven CRUD primitives | ✅ Built in | ❌ No | ||
| RBAC + multi-tenant SaaS primitives | ✅ Included | ❌ No | ||
| UI/component packages included | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Blog/CMS package included | ✅ Yes | ❌ No | ❌ No | ❌ No |
| Type safety (TS-first) | ✅ Deeply integrated | ✅ Strong | ||
| Local dev experience | ✅ Unified | ✅ 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) | |||
| Cold starts | ✅ Minimal | |||
| Built-in CDN / caching | ✅ Native (Cloudflare) | ❌ No | ❌ No | ❌ No |
| Queues / cron support | ✅ Native (CF bindings) | |||
| Cost predictability | ✅ High | |||
| Vendor lock-in | ❗ High (proprietary model) |
ottabase/
├── apps/
│ ├── otta-web/ # TanStack Router + Vite + Workers (primary)
│ └── otta-landing/ # Next.js + OpenNext (homepage/landing)
├── packages/
│ ├── ottaorm/ # Fat models, auto-migrations, CRUD, RLS
│ ├── db/ # Drizzle D1 driver
│ ├── cf/ # Cloudflare bindings (D1, KV, R2, Queues, Cache Keys)
│ ├── cf-realtime/ # WebSocket pub/sub (Durable Objects)
│ ├── queue/ # Job queue (dispatch, handlers, priority)
│ ├── auth/ # Auth.js v5 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
│ ├── 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 (Post, Category, Tag, 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:*, db:*
│ ├── state/ # Jotai atoms (theme, user, sidebar)
│ ├── ui-shadcn/ # shadcn/ui components
│ ├── ui-mantine/ # Mantine provider + themes
│ ├── ui-components/ # Shared components (DarkModeToggle, Logo)
│ ├── ui-code-highlight/ # Code syntax highlighting
│ ├── ui-split-pane/ # Resizable split pane
│ ├── 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 dev (Vite + Wrangler)
pnpm dev
# Initialize database
curl -X POST http://localhost:3004/api/ottaorm/initThe otta CLI provides a streamlined interface for monorepo tasks:
# Scaffold a new app
otta new web my-app # Vite + TanStack Router + Workers
otta new landing my-site # Next.js landing page
# Development
otta dev otta-web # Start dev server
otta build otta-web # Build for production
otta test otta-web # Run tests
otta lint otta-web # Lint
otta list # List all appsSee otta --help or the CLI README for more commands.
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 { setDriver } from '@ottabase/ottaorm';
import { createD1Driver } from '@ottabase/db/drizzle-d1';
import { Todo } from './ottabase/models/Todo';
// In worker
setDriver(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/db |
Drizzle D1 driver (createD1Driver) |
@ottabase/cf |
D1, KV, R2, Queues, Rate Limiting, Cache Keys, read-through KV cache |
@ottabase/queue |
Job queue (dispatch, handlers, deduplication, chaining, priority) |
@ottabase/auth |
Auth.js v5 with D1 adapter, 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:*, db:* |
| 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-shadcn |
shadcn/ui components, ShadcnProviders |
@ottabase/ui-mantine |
Mantine provider, pre-built themes |
@ottabase/ui-base |
Framework-agnostic base styles |
@ottabase/ui-components |
Shared components: DarkModeToggle, Logo |
@ottabase/ui-code-highlight |
Code syntax highlighting (Prism/Shiki) |
@ottabase/ui-split-pane |
Resizable split-pane layout component |
@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/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 homepage (Next.js, OpenNext, Brand Engine):
# Unix/macOS: cp -r apps/otta-landing apps/my-new-homepage
# Windows: xcopy /E /I apps\otta-landing apps\my-new-homepage
cd apps/my-new-homepage
# Update package.json name
# Edit config/brand.config.ts to customize themeWhen 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 dev # Start all (Vite + Wrangler)
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}"- Architecture
- Changelog
- Contributing Guide
- Security Policy
- Template App README
- OttaORM README
- Cloudflare Deploy
- Cloudflare Config
- Testing