Skip to content

Development guide

Local setup, run, and test for the Monark app. For production deploy, see deploy-checklist.md. For the deeper "how do I do X correctly" conventions, see the guidelines under docs/agents/ (schema changes, testing, module authoring, i18n, user docs).

Prerequisites

  • Node ≥ 22 : .nvmrc pins 22 ; nvm use picks it up.
  • pnpm 10 : run corepack enable once and you get the pinned pnpm@10.13.1.
  • Docker (Docker Desktop / OrbStack / colima) ; required for the local Supabase stack and for integration-test containers.
  • The Supabase CLI ships as a dev dependency ; you invoke it as pnpm exec supabase (no separate install).

Quick start

corepack enable
nvm use                 # Node 22
pnpm bootstrap

pnpm bootstrap does the whole first-run in order : a preflight (Node / pnpm / Docker reachable), copies each .env.example.env (root, packages/db, services/api, services/web ; never overwriting an existing .env), pnpm install, pnpm exec supabase start, wires the copied .env files to that stack, and pnpm db:migrate. Add --no-supabase to skip Docker/Supabase and bring your own Postgres (set DATABASE_URL / DIRECT_URL yourself).

The wiring step fills blank values only ; DATABASE_URL / DIRECT_URL and the Supabase URL + keys read back from supabase status -o env, plus locally generated TOTP_ENCRYPTION_KEY / SECRETS_ENCRYPTION_KEY / CRON_SECRET. A value you have already set is never touched, so re-running bootstrap on a configured install is a no-op. What it deliberately leaves blank is everything non-local : SMTP credentials, a Sentry DSN, and your BRANDING_* identity.

Then run :

pnpm exec supabase start   # if it isn't already up
pnpm dev

Web → http://localhost:3000, api → http://localhost:4000.

Environment

bootstrap copies the templates and fills the local ones ; you fill in the rest. Each .env.example documents every variable inline ; the load-bearing ones :

  • services/api/.env : SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY / SUPABASE_SECRET_KEY, TOTP_ENCRYPTION_KEY, SECRETS_ENCRYPTION_KEY, CRON_SECRET, SMTP_URL (defaults to the local Inbucket at smtp://localhost:54325), INITIAL_ORG_SLUG / INITIAL_ORG_NAME (the single-tenant bootstrap org), WEB_ORIGIN / APP_URL, PORT (4000).
  • services/web/.env : NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, NEXT_PUBLIC_API_URL (http://localhost:4000), and the server-only SUPABASE_URL / SUPABASE_SECRET_KEY (never NEXT_PUBLIC_* ; the browser must never see the secret key).
  • packages/db/.env : DATABASE_URL + DIRECT_URL must be duplicated here : the Prisma CLI reads env from the schema's own package, not from services/api.

bootstrap generates the three at-rest secrets (TOTP_ENCRYPTION_KEY, SECRETS_ENCRYPTION_KEY, CRON_SECRET) for you locally. To mint one yourself (rotating a key, or filling a production env) use :

node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"

Quote any value starting with #. Node's --env-file parser reads an unquoted leading # as a comment, so INITIAL_ORG_PRIMARY_COLOR=#2563EB silently resolves to an empty string ; write INITIAL_ORG_PRIMARY_COLOR="#2563EB". The same applies to BRANDING_PRIMARY / BRANDING_ACCENT.

Never commit a real value. With SMTP_URL unset, outbound mail is logged, not sent.

Run

pnpm dev runs both services concurrently via Turbo (persistent, un-cached) : web on :3000 (next dev --turbopack) and api on :4000 (tsx watch). The local Supabase stack must be up first ; pnpm exec supabase start is idempotent, so it's a fast no-op when already running. If the api logs Can't reach database server at localhost:54322, the stack is down.

Local services + ports :

ServicePort
web3000
api4000
Supabase API (SUPABASE_URL)54321
Postgres54322
Supabase Studio54323
Mail : Inbucket web UI (browse caught email)54324
Mail : Inbucket SMTP listener (SMTP_URL)54325

Dev-tool shortcuts open the local consoles :

  • pnpm dev:tools:mail ; Inbucket (caught email) at :54324.
  • pnpm dev:tools:supabase ; Supabase Studio.
  • pnpm dev:tools:db ; Prisma Studio.
  • pnpm dev:tools:all ; all three.

Database

The Prisma schema is assembled by codegen, so pnpm gen runs before you migrate (it also regenerates the event union + tRPC router). Scripts run per-package via pnpm --filter @monark/db <script> ; the root exposes only db:migrate and db:reset :

ScriptDoes
db:generateRegenerate the Prisma client.
db:migrateprisma migrate deploy ; apply committed migrations (prod-style).
db:migrate:devprisma migrate dev ; create + apply a new migration in dev.
db:resetDrop + re-apply + reseed.
db:studioPrisma Studio GUI.
db:seedSeed (a no-op placeholder today).

Typical change flow : edit base.prisma (core) or a module's prisma/<module>.prisma fragment → pnpm genpnpm --filter @monark/db db:migrate:dev. Watch the GIN-index drift gotcha when creating a migration ; see schema-changes.

Test

CommandRuns
pnpm testUnit tests across every package.
pnpm test:integrationIntegration tests (serialized ; each boots a throwaway Postgres testcontainer ; Docker must be running).
pnpm test:e2ePlaywright end-to-end (full stack : Supabase + api + web).
pnpm test:coverage / pnpm test:integration:coveragepnpm coverage:mergeCoverage, fused and checked against the per-package floors.

Run a single package's integration suite with pnpm --filter <pkg> test:integration (e.g. pnpm --filter @monark/calendar test:integration). Integration and e2e suites only run when Docker (and, for e2e, the full stack) is available. See testing for how the harness works and how to add a test.

Feature flags on a fresh install

Extended modules ship behind flags that default off, so a freshly bootstrapped install 404s on /wiki and friends until they're turned on. For local dev :

pnpm enable:dev-flags

That sets global overrides for the flags a developer normally wants on (wiki.enabled, chat.*, public-api.*, data-models.query-language, data-models.public-forms) without touching the registered defaults ; see tools/enable-dev-flags.ts. In a real deploy, overrides are DB rows against the FeatureFlag table ; there is no admin UI for them yet.

Pre-PR gate

Run what CI runs, in order ; gen first so a stale generated file doesn't fail typecheck :

pnpm gen && pnpm typecheck && pnpm lint && pnpm test && pnpm check:tiers && pnpm check:modules && pnpm check:i18n && pnpm check:mcp

Git hooks already run prettier + eslint on commit and pnpm lint on push. pnpm check:migrations (migrations reproduce the schema) needs a SHADOW_DATABASE_URL pointing at an empty Postgres, so run it separately when you've touched migrations.

Deploy

Production runs the web on Vercel and the api + cron jobs on Render, against a managed Supabase project. The full step-by-step (secrets, env vars, smoke test, custom domains) is in deploy-checklist.md.

Doing more

The task-shaped conventions live under docs/agents/ : module-authoring, schema-changes, testing, i18n, and user-doc. The architecture + layout reference is architecture.md ; the authoritative as-is picture is platform-overview.md.