DEV Community

Deepak Thomas
Deepak Thomas

Posted on

I built a Cloudflare-first batteries-included SaaS OSS framework for Solo Founders - Ottabase

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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


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)