Craftkitdocs

Overview

High-level system diagram and component responsibilities.

01 — System Architecture Overview

At a glance

Craftkit is a TypeScript monorepo of two deployable apps and several shared packages, coordinated by Turborepo. The apps deploy independently to suit their runtime profiles.

┌──────────────────────────────────────────────────────────────────┐
│                          Marketing site                          │
│                          (apps/web · /)                          │
└──────────────────────────────────────────────────────────────────┘
                                │
┌──────────────────────────────────────────────────────────────────┐
│                          Dashboard (auth)                        │
│                  (apps/web · /dashboard/...)                     │
│   · Sign-up / login (better-auth)                                │
│   · Projects, templates, renders, API keys, webhooks             │
│   · Embed: keys, origins, catalogs, sessions, sandbox            │
└──────────────────────────────────────────────────────────────────┘
                                │
┌──────────────────────────────────────────────────────────────────┐
│                         Public REST API                          │
│                   (apps/web · /v1/...)                           │
│   · POST /v1/templates/:slug/render   (API-key auth)             │
│   · GET  /v1/renders/:id              (poll)                     │
│   · POST /v1/hooks/:token             (inbound, per-template)    │
│   · POST /v1/embed/sessions           (mint embed JWT)           │
│   · POST /v1/embed/sessions/refresh   (renew)                    │
└──────────────────────────────────────────────────────────────────┘
                                │
                       enqueue render jobs
                                ▼
┌──────────────────────────────────────────────────────────────────┐
│                          Render worker                           │
│                       (apps/render-worker)                       │
│   · BullMQ consumer                                              │
│   · Handlebars compile · Puppeteer print · S3 upload            │
│   · Outgoing webhooks with HMAC                                  │
└──────────────────────────────────────────────────────────────────┘
                                │
┌──────────────────────────────────────────────────────────────────┐
│                         Embed iframe page                        │
│                 (apps/web · /embed/builder)                      │
│   · No-chrome layout                                             │
│   · JWT-validated                                                │
│   · Catalog-aware variable picker                                │
│   · postMessage protocol with parent window                     │
└──────────────────────────────────────────────────────────────────┘

Apps

apps/web

A Next.js 15 (App Router) application that owns:

  • The marketing site (/)
  • The authenticated dashboard (/dashboard/...)
  • The embed iframe page (/embed/builder)
  • The public REST API (/v1/...)
  • The internal tRPC layer (used by dashboard server actions)

Deploys to Vercel or any Node runtime. Stateless except for the database.

apps/render-worker

A standalone Node process that consumes BullMQ jobs and renders PDFs via Puppeteer. Kept separate from the web app because:

  1. Puppeteer's memory profile (~300 MB resident per worker) doesn't fit a serverless function.
  2. Render concurrency tuning is independent from request concurrency.
  3. Worker can be scaled horizontally or replaced (e.g., with @react-pdf/renderer) without touching the API.

Deploys to Railway, Fly.io, or any Docker-friendly host.

Packages

Package Purpose
@craftkit/db Drizzle schema, migrations, typed client. Exports both node-postgres (worker) and postgres-js (web) flavors via the same schema.
@craftkit/render-core Pure (framework-agnostic) renderer: Tiptap doc → variable manifest → Zod/JSON Schema → Handlebars-compiled HTML. Used by both web (server actions on save) and worker (render time).
@craftkit/schema Shared Zod schemas: variable manifest, render request, webhook envelope. Single source of truth for API contracts.
@craftkit/sdk Public TypeScript client (@craftkit/sdk) — what customers npm install. Zero deps.
@craftkit/embed Public embed SDK — drop-in <script> partners use to mount the iframe.
@craftkit/ui Shared shadcn primitives + brand tokens + cn() helper.
@craftkit/tsconfig TS preset configs: next.json, node.json, react-library.json.

External infrastructure

Component Dev Production
Database Postgres 16 (Docker) Neon / Supabase / managed PG
Queue Redis 7 (Docker) Upstash Redis
Object storage MinIO (Docker, S3-compat) Cloudflare R2
PDF engine Puppeteer + bundled Chromium Puppeteer + @sparticuz/chromium
Email (v0.2+) Resend / Postmark via partner BYO keys same
AI (v0.2+) OpenAI / Anthropic / Together via provider abstraction same

Request flow — synchronous render API

1. Customer POSTs /v1/templates/:slug/render (Bearer ck_live_…)
2. Auth middleware resolves API key → project_id
3. Load template (slug + project) and its current_version
4. Validate request body against version's auto-generated Zod schema
5. Insert renders row (status=queued)
6. Enqueue BullMQ job with render_id
7. Respond 202 { id, status, poll_url }

Request flow — async worker

1. Worker pulls job from queue
2. Load render → template_version → input data
3. Compile: render-core renders Handlebars(compiledHtml, data) → final HTML
4. Print: Puppeteer launches a page, sets HTML, prints PDF
5. Upload PDF to S3/R2, generate signed URL
6. Update renders row (status=succeeded, asset_url)
7. Enqueue webhook deliveries (separate queue)
8. Webhook deliverer POSTs to each registered webhook with HMAC signature

Boundaries

  • No shared mutable state. Everything is in Postgres + Redis + S3. Workers can be killed and restarted at any time.
  • No partner data crosses boundaries. Embed iframe has no read access to partner DB; partner backend never reads Craftkit's internal state except via documented API.
  • No tenant data leakage between projects. Every query is scoped by project_id at the data-access layer.

Configuration & secrets

All runtime config flows through environment variables documented in engineering/02-environment-variables.md. Secrets are never stored in code. JWT signing keys are stored in DB (rotated via admin tools), API keys are hashed at rest with the prefix retained for identification.

Anti-residue policy

Craftkit was inspired by patterns from a separate codebase, but zero identifiers, vocabulary, schemas, or values from that codebase appear here. See architecture/07-anti-residue.md. The check is enforced by scripts/anti-residue-check.sh in CI.


Last revised: 2026-05-02