← All work
Platform, 2026

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.

Role
Architect and lead developer
Context
Own product, Ottr Technology
Scale
345 commits, ~540 source files, 14 packages, 49 templates, May – Sep 2026
acme.ottr.appor custom domainTenant middlewareresolve → search_pathNestJS APIGraphQL + REST + docsGenerated SDKkiln-codegenAdmin · buildersschema · page · form · themeYjs servercollaborationWorker13 BullMQ jobspublic schematenants · billing · audittenant_atenant_btenant_nBackup / restoreper schema
Kiln: the tenant is resolved from the domain and search_path is set per request; one schema per tenant in one database.

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

  1. 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.
  2. 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.
  3. 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.
  4. Real-time collaboration in the builders with a Yjs server and presence.
  5. 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.
  6. 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.
  7. 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

apps/apiNestJS, code-first GraphQL, Swagger/OpenAPI 3.1, Drizzle.
apps/adminNext.js 15, shadcn/ui, TipTap, builders and Theme Studio.
apps/previewTenant storefront renderer with product, order and order-lookup routes.
apps/worker, apps/yjs-serverBullMQ jobs; Yjs collaboration server.
packages/kiln-*ai, blocks, codegen, config, core, crypto, db, email, graphql, plugin-runtime, storage, theme, ui, webhooks.
cli/ottr-kiln, plugins/forms, templates/CLI, plugin runtime with marketplace, 49 vertical templates.

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.

Stack

TypeScriptNestJSApollo GraphQLDrizzlePostgreSQLRedisBullMQNext.jsshadcn/uiBetter-AuthYjsMinIOOpenTelemetryStripeeSewaCaddy