Every solo founder I know wastes the first 2-4 weeks (sometimes, months) of a new SaaS project doing the same setup dance: wire up auth,
implement multi-tenancy, configure RBAC, pick a queue solution, add a blog engine and only then actually build the product.
I got tired of it and built Ottabase: an open-source monorepo you clone and own. Once you fork it, the code is yours.
The repo: github.com/thinkdj/ottabase
The stack
pnpm workspaces + Turborepo, deploying to Cloudflare Workers. All infrastructure is Cloudflare-native - D1 (SQLite at the edge), KV, R2, Queues, Durable Objects. No Docker, no separate database servers etc.
Frontend is Vite + TanStack Router.
Backend is a Cloudflare Worker running on the same repo.
What's included (45+ packages)
Data & Infrastructure
| Package | What it does |
|---|---|
@ottabase/ottaorm |
Fat model ORM for auto-migrations, CRUD API, RLS, relationships, TanStack Query hooks |
@ottabase/db |
Drizzle D1 driver (createD1Driver) |
@ottabase/auth |
Auth.js v5 — OAuth (Google, GitHub), Credentials, Magic Link, D1 adapter |
@ottabase/rbac |
Role-based access control with per-org permission caching in KV |
@ottabase/audit |
Audit logging with field-level change tracking and RBAC context |
@ottabase/queue |
Laravel-style job queue — dispatch, handlers, deduplication, chaining, priority |
@ottabase/cron |
Cron handlers — static code-defined and DB-driven scheduler |
@ottabase/cf |
Cloudflare service wrappers — D1, KV, R2, Queues, Rate Limiting, read-through KV cache |
@ottabase/cf-realtime |
WebSocket pub/sub built on Durable Objects (Pusher alternative) |
@ottabase/analytics |
Cloudflare Analytics Engine (WAE) — write events, query, funnel analysis, top-K |
@ottabase/logger |
Structured logging with Console, HTTP, Sentry, Memory, and Buffer transports |
@ottabase/config |
App config, env var helpers, storage key utilities |
Brand, Layout & Content
| Package | What it does |
|---|---|
@ottabase/brand-engine |
Design tokens, preset expansion, CSS injection, per-tenant theming, email branding |
@ottabase/brand-engine-react |
BrandProvider, LayoutResolver, and useBrand() React bindings |
@ottabase/ottalayout |
Layout types, 10 built-in presets, path resolver, React slot system |
@ottabase/ottablog |
Full blog/CMS — Post, Category, Tag, Series, Version models + Blog Studio editor |
@ottabase/email |
Email sending via Resend, SES, MailChannels, or SMTP |
@ottabase/notifications |
Multi-channel notifications — email and WebSocket push |
@ottabase/shortlinks |
URL shortener with interstitial pages and WAE analytics tracking |
@ottabase/referrals |
Referral tracking with first-touch attribution and WAE analytics |
UI Components
| Package | What it does |
|---|---|
@ottabase/ui-shadcn |
shadcn/ui component library with ShadcnProviders
|
@ottabase/ui-mantine |
Mantine provider with pre-built themes, more like a POC of third party ui lib integration |
@ottabase/ui-tailwind |
Tailwind config and shared design tokens. Core UI. |
@ottabase/ui-datatable |
Advanced data table — TanStack Table v8, server-side sort/filter/pagination, bulk actions |
@ottabase/ui-components |
Shared components — DarkModeToggle, Logo, and more |
@ottabase/ui-code-highlight |
Code syntax highlighting component |
@ottabase/ui-split-pane |
Resizable split pane |
@ottabase/ui-cropper |
Vanilla JS image cropper (~3–4 KB) |
@ottabase/spotlight |
Command palette (Ctrl+K) |
Developer Experience
| Package | What it does |
|---|---|
@ottabase/forms |
Auto-generated CRUD forms and list views from OttaORM model definitions |
@ottabase/ottaeditor |
EditorJS wrapper with 15+ block plugins |
@ottabase/ottarenderer |
EditorJS block renderer for display |
@ottabase/ottaupload |
File uploads to R2 or Cloudflare Images |
@ottabase/ottaselect |
Headless select/combobox component |
@ottabase/state |
Global state via Jotai — theme, user, sidebar atoms |
@ottabase/i18n |
i18next wrapper with en, es, fr, de built in |
@ottabase/api |
Type-safe fetch wrapper for internal API calls |
@ottabase/utils |
Tree-shakeable utilities — timezone, string, URL, currency, file helpers |
@ottabase/scripts |
CLI tools — cf:login, cf:setup, cf:validate, clean:*, db:*
|
@ottabase/docs |
Markdown doc viewer component |
@ottabase/migrate |
Migration utilities |
The core idea: *fat * models, not controllers
Business logic lives in the model. One place, auditable, no indirection maze.
export class Todo extends BaseModel {
static entity = 'todos';
static table = todosTable;
async toggle() {
this.set('completed', !this.get('completed'));
return this.save();
}
}
From one model definition you get: auto-migrations, a full REST CRUD API at /api/ottaorm/todos, and TanStack Query
hooks — all generated for free. Adding a field means updating the schema and hitting one endpoint to migrate.
Multi-tenancy and RLS built in from day one
Every query automatically enforces tenant isolation via the OttaORM RLS engine. Provide context once at app init
(organizationId, userId, appId) and queries are scoped automatically. Cross-tenant data leaks are prevented at
the ORM layer — not left as a discipline exercise.
Get running in 5 minutes
git clone https://github.com/thinkdj/ottabase.git
cd ottabase
pnpm install && pnpm build:pkg
cp apps/otta-web/.env.example apps/otta-web/.env.local
# Fill in AUTH_SECRET, MIGRATION_SECRET, BOOTSTRAP_OWNER_SECRET
pnpm dev
Open http://localhost:3003. If the platform isn't bootstrapped, it redirects you to a setup wizard — create tables,
seed roles, create your owner account, finalize. No manual SQL, no curl commands needed.
What I learned building this
Edge-first forces cleaner code. No fs, no child_process, no Node-only APIs. Once that's the constraint, you stop reaching for heavy server-side dependencies and write leaner workers.
Fat models beat service layers for solo projects. When a team of one has to find where a bug lives, "it's in the model" beats "it could be in the service, the controller, the middleware, or the helper."
Multi-tenancy cannot be bolted on. Every project I've tried to retrofit tenant isolation into was a nightmare. Having RLS enforced at the ORM level from the start means I never have to think about it again.
Monorepos pay off fast. Turborepo's incremental build cache means full rebuilds only happen once. After that, changing one package rebuilds only what depends on it.
Links
- Ottabase Repo: github.com/thinkdj/ottabase
- Quick Start: QUICKSTART.md
- Architecture: ARCHITECTURE.md
Discussions: GitHub Discussions
Homepage (yeah, why not): ottabase.com
Would love your feedback, especially on the fat model pattern vs service layers debate, and anything you'd want added to the package list. Happy to answer questions in the comments.
PS: I'm serious about package additions

Top comments (0)