Ottr Kiln — multi-tenant headless CMS
Pick a template, provision a tenant with its own Postgres schema, schema builder, page builder, GraphQL and REST APIs, storefront and checkout.
Background
An agency platform. Build a site once, deploy it per client, manage all of them from one admin. A tenant gets a schema builder for custom collections, a visual page builder over a block library, a form builder, a theme studio, generated API docs, a typed SDK, a storefront renderer and checkout.
Forty-nine vertical templates cover the businesses an agency in Nepal actually meets: kirana, agrovet, trekking, rafting, futsal, homestay, study-abroad, sweets, clinic, pharmacy and so on.
The problem
Every client site at an agency starts the same and diverges the same way: a custom collection here, a checkout there, and by month three it is a fork nobody wants to maintain. Shared-table multi-tenancy makes per-client backup, restore and export painful. A database per tenant makes migrations and cost painful.
Approach
- One Postgres schema per tenant in one database. The public schema holds the tenant registry, billing and audit. Middleware resolves the tenant from subdomain or custom domain and sets search_path on a request-scoped Drizzle client. A migration runner loops over schemas.
- Per-tenant backup and restore as a BullMQ job with an admin page, and one-command provisioning that also wires a Caddy domain and TLS.
- A schema engine that drives everything downstream: collections with nested types become GraphQL and REST endpoints, per-tenant API docs and a generated SDK from kiln-codegen.
- Real-time collaboration in the builders with a Yjs server and presence.
- A worker with thirteen job types: webhook delivery, scheduled publishing, sitemaps, tenant backups, billing cron, audit partition, prune and export, audit anomaly detection with notifications, API-key rotation, translation and chatbot pruning.
- Security by default: envelope encryption with a key-encryption key in kiln-crypto, Stripe webhook signature verification with its own spec, a tenant chatbot behind a safe-endpoint guard, GDPR export and import.
- AI where it removes work: OpenAI, Anthropic, Gemini or Ollama for content enrichment, schema inference from a description, translation, and a logo-to-palette pipeline that themes each tenant automatically.
Architecture
Outcome
- Deployable under kiln.ottr.app and api.ottr.app with GHCR images, Caddy and staging and production compose files.
- A full architecture document ships with the repo.
- Successor direction for the sites side of Ottr, while Omni-Ledger stays the commerce engine.
Lessons
- Schema-per-tenant with search_path is the sweet spot for tens to low hundreds of tenants: isolation for free, one connection pool, and backup per client is a pg_dump of a schema.
- Generate the SDK and the docs from the schema on day one. It is the difference between a platform and a pile of sites.