Kwisple
Browse the docs

The platform repository › Getting around

Local setup

Prerequisites, infrastructure containers, environment variables, database setup and the commands that run the dashboard, web app, worker, agent and starters on your machine.

Everything runs on your machine: Postgres and S3-compatible storage in Docker, the apps with pnpm dev. Nothing here talks to production. Read Monorepo overview first if you do not know the layout yet.

Prerequisites

RequirementWhy
Node >=22engines in the root package.json; the production image uses Node 22 too
pnpm 11.24.0pinned in packageManager; enable it with corepack
Docker with ComposePostgres, RustFS (S3) and Mailpit come from infra/docker-compose.yml
Chrome or Chromium (optional)only for pnpm lighthouse and the Playwright e2e tests
corepack enable
corepack prepare pnpm@11.24.0 --activate
node --version   # v22 or newer

1. Install

git clone <the platform repository> kwisple
cd kwisple
pnpm install

Install scripts run only for the packages listed under allowBuilds in pnpm-workspace.yaml (@biomejs/biome, @resvg/resvg-js, sharp, esbuild). If sharp or resvg fail to install, that is the place to look.

2. Infrastructure

pnpm infra:up      # docker compose -f infra/docker-compose.yml up -d
pnpm infra:down    # stop it again

The compose project is called kwisple and starts three containers:

ServiceImageHost port(s)Notes
postgrespostgres:17-alpine5433 → 5432database rva_studio, data in infra/data/postgres
storagerustfs/rustfs:latest9100 → 9000 (S3 API), 9101 → 9001 (console)MinIO-compatible API; data in infra/data/storage. Host ports 9100/9101 because 9000 is often taken locally (Homebrew PHP-FPM).
mailaxllent/mailpit:v1.271125 → 1025 (SMTP), 8125 → 8025 (inbox)catches every mail; nothing leaves your machine. Inbox on http://localhost:8125.

Create the assets bucket once (public read + CORS for the Studio canvas):

pnpm infra:init    # pnpm --filter @kwisple/storage init-bucket

3. Environment file

cp .env.example .env

Every app reads the same .env at the repository root (the Next apps through dotenv, the worker and agent through --env-file-if-exists=../../.env). turbo.json lists the variables it passes through to tasks (globalPassThroughEnv); a new variable must be added there too.

Fill in the values yourself; never commit .env. The variables, by group:

Database and auth

VariableMeaning
DATABASE_URLPostgres connection string. The local default points at the Docker database on port 5433.
BETTER_AUTH_SECRETSigns sessions. >= 32 characters (openssl rand -base64 32); production refuses to start otherwise, development warns.
BETTER_AUTH_URLThe dashboard's own URL (http://localhost:4100 locally).

URLs

VariableMeaning
DASHBOARD_URLPublic dashboard URL (also baked into the web app at build time for CSP).
WEB_URLPublic URL of apps/web; the dashboard calls <WEB_URL>/api/revalidate.
PLATFORM_DOMAINSites get <slug>.<PLATFORM_DOMAIN> before they have a domain (locally sites.localhost:4101).
DEV_SITE_SLUGDevelopment only: the site apps/web serves on a bare localhost host instead of a 404.

Object storage

VariableMeaning
STORAGE_ENDPOINTS3 endpoint (locally the RustFS API on port 9100).
STORAGE_REGIONRegion string (auto for RustFS/R2).
STORAGE_BUCKETBucket name.
STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEYCredentials (locally the RustFS keys from infra/docker-compose.yml).
STORAGE_PUBLIC_URLPublic origin of stored objects; next/image only allows this host. Read at build time.

Secrets shared by dashboard and web

VariableMeaning
REVALIDATE_SECRETBearer token the dashboard uses on apps/web /api/revalidate. Identical in both apps, >= 32 characters.
PREVIEW_SECRETSigns live-preview tokens (dashboard signs, web verifies). Empty = derived from BETTER_AUTH_SECRET (dev only; production logs a warning).

Assistant and agent

VariableMeaning
ASSISTANT_URLWhere the dashboard sends kwisple.assistant/v1 tasks. Empty = assistant features hidden. Locally http://localhost:4110.
ASSISTANT_TOKENShared bearer token between dashboard and agent.
ANTHROPIC_API_KEYRead only by apps/agent. Never by the dashboard or sites.
AGENT_MODELModel id the agent uses (default claude-opus-5-5).
AGENT_PORTAgent port (default 4110).
AGENT_MCP_URLWhere the agent reaches the dashboard's MCP endpoint for the chat (default <DASHBOARD_URL>/api/mcp).

Background jobs

VariableMeaning
JOBS_INLINE1 = the dashboard server runs the job handlers itself; 0 = a separate worker does. Empty = on outside production, off in production.

Client IP, proxies and auth hardening

VariableMeaning
TRUST_PROXY1 = trust forwarded headers (only behind a proxy you control).
TRUSTED_IP_HEADERWhich header carries the client IP (x-forwarded-for, cf-connecting-ip, x-real-ip).
TRUSTED_PROXY_HOPSExtra proxies to skip in an x-forwarded-for chain.
TRUSTED_PROXIESBetter Auth: proxy IPs/CIDRs for x-forwarded-for.
ALLOW_SIGNUPOpen self-service sign-up. Empty = on in development, off in production. The first account of an empty database is always allowed.
AUTH_RATE_LIMIT_STORAGEmemory (default) or database (needs Better Auth's rate_limit table).
AUTH_RATE_LIMIT1 enables Better Auth's rate limits in development.
OUTBOUND_ALLOW_PRIVATEWebhooks to private/loopback targets: empty = allowed in development, blocked in production; 0/1 force.

Optional integrations (empty = feature hidden or manual mode)

VariableMeaning
CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKENPlatform-managed Cloudflare Pages hosting for coded sites.
HOSTING_SECRET_KEYEncrypts stored deploy hooks and webhook secrets (>= 32 random bytes; falls back to a key derived from BETTER_AUTH_SECRET).
HOSTING_FAKETest hosting provider: on outside production; 1/0 force.
META_APP_ID, META_APP_SECRET, META_GRAPH_VERSIONAutomatic posting to Facebook/Instagram.
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, LINKEDIN_VERSIONAutomatic posting to LinkedIn pages.
SOCIAL_TOKEN_KEYEncrypts social tokens at rest (falls back to BETTER_AUTH_SECRET).
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, MAIL_FROMOutgoing mail. Empty SMTP_HOST = no mail (invitations show a link to pass on). Locally point at Mailpit: host 127.0.0.1, port 1125.
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRETStripe Billing for agency subscriptions; empty = manual mode.
STRIPE_CONNECT_WEBHOOK_SECRET, STRIPE_CONNECT_FEE_PERCENTStripe Connect for client packages.

Database tuning and debugging

VariableMeaning
DATABASE_POOL_MAXConnections per process (default 10).
DATABASE_PREPAREfalse behind a transaction-mode pooler (PgBouncer).
DB_DEBUG1 logs every SQL query (count queries per page with pnpm preview).

4. Database schema and seed

pnpm db:migrate    # drizzle-kit migrate: applies packages/db/drizzle/*.sql in order
pnpm db:seed       # pnpm --filter @kwisple/content seed

pnpm db:migrate is what production runs. pnpm db:push (drizzle-kit push) diffs the Drizzle schema straight into the database without writing a migration file; use it only for throw-away experiments on a local database and never in production, because the next pnpm db:generate would then produce a migration that production still needs but your database already has. The normal flow for a schema change is in Adding things.

pnpm db:seed runs packages/content/scripts/seed.ts: one agency ("Mijn bureau"), one demo client ("Demo Bakkerij") with a brand kit and a hosted site on the standard schema (home, over ons, contact). It seeds no users.

Other database scripts:

ScriptWhat
pnpm db:generatedrizzle-kit generate: writes a migration from schema changes
pnpm db:studiodrizzle-kit studio: browse the database in the browser

5. Run the apps

pnpm dev

pnpm dev runs turbo run dev for @kwisple/dashboard (4100), @kwisple/web (4101) and @kwisple/agent (4110) side by side in Turborepo's TUI. Open http://localhost:4100.

CommandStarts
pnpm devdashboard, web app and agent
pnpm --filter @kwisple/dashboard devonly the dashboard
pnpm --filter @kwisple/web devonly the web app
pnpm agentonly the agent (tsx watch src/server.ts, needs ANTHROPIC_API_KEY and ASSISTANT_TOKEN)
node tools/assistant-stub/server.mjsa rule-based assistant on 4110 (no model, no key) for testing the flow
pnpm --filter @kwisple/dashboard workerthe pg-boss worker as its own process
pnpm starterthe Next.js client-site starter on 4102
pnpm starter:astrothe Astro starter on 4104
pnpm sitethe product's own website on 4105
pnpm build && pnpm previewproduction builds, then next start for dashboard and web

The worker

In development you normally do not start the worker: JOBS_INLINE is empty, so inlineWorkersEnabled() (apps/dashboard/src/worker/inline.ts) returns true outside production and the dashboard server registers every job handler in its own process at boot (src/instrumentation.ts). Start the standalone worker when you want to see job output separately or test the production shape:

JOBS_INLINE=0 pnpm --filter @kwisple/dashboard dev     # in one terminal
pnpm --filter @kwisple/dashboard worker                # in another

The worker process sets JOBS_INLINE=0 for itself so it never starts a second lazy set of handlers. See Jobs and the worker.

Production mode locally

Development mode compiles each page on first visit (1.5–3.5 s) and is not representative. To click through the product at real speed:

pnpm build
pnpm preview

pnpm preview is turbo run start for the dashboard and the web app on the same ports. DB_DEBUG=1 pnpm preview logs every query.

6. Log in

Register through the dashboard: http://localhost:4100/register. The first account of an empty database becomes platform admin and owner of the first agency (the seeded "Mijn bureau"); ALLOW_SIGNUP is on in development, so later accounts can register too.

For testing with every kind of role there are two seed scripts in apps/dashboard/scripts. Each creates accounts with one fixed password that is written in the script itself; read it there, it is not repeated here.

ScriptCreates
pnpm --filter @kwisple/dashboard exec tsx scripts/seed-test-accounts.tsfive accounts on the seeded "Demo Bakkerij" client: an agency admin and member, a client owner, admin and member (addresses end in @test.localhost). Run pnpm db:seed first.
pnpm --filter @kwisple/dashboard exec tsx --env-file=../../.env scripts/seed-personas.tsthe persona environment: agency "Persona Bureau" with clients of different sizes, plus agencies with a stopped and an almost-ended subscription.

Both refuse to run with NODE_ENV=production and are safe to re-run.

Mail: with SMTP_HOST=127.0.0.1 and SMTP_PORT=1125 every invitation, approval and form notification lands in Mailpit on http://localhost:8125.

7. Working on a client site locally

The Next starter (pnpm starter) reads content from your local platform through the delivery API. Create a site of mode headless in the dashboard, make a token under Website → Instellingen → Ontwikkeling and put it in the starter's .env; kwisple dev pushes cms.config.ts while you work. The details are on the site side of this documentation (Quick start, Environment variables and tokens).

Hosted (legacy) sites render on http://<slug>.sites.localhost:4101/ (Chrome resolves *.localhost to loopback) or on http://localhost:4101/ when DEV_SITE_SLUG names the site.

Common local pitfalls

SymptomCause and fix
useT throws, or a new message key is missing although it is in nl.jsonThe catalogue types are generated from nl.json at build; after adding messages restart the dev server (and make sure the key exists in fr.json and en.json, or pnpm check:i18n fails).
A page's client component says its namespace was not providedAdd the namespace to the nearest layout's <ClientMessages namespaces={[...]}>.
Port 4100/4101 already in useAnother dev server is running; pnpm dev does not pick a free port. Stop it or run one app with pnpm --filter … dev.
Port 9000 conflictsNot an issue: the compose file maps RustFS to 9100/9101 on purpose. Check STORAGE_ENDPOINT uses 9100.
Uploads fail with a bucket errorRun pnpm infra:init once after the first pnpm infra:up.
Jobs never run (renders stay "bezig", thumbnails missing)JOBS_INLINE=0 is set but no worker is running. Unset it, or start pnpm --filter @kwisple/dashboard worker.
A job added in this session is not picked up by a long-running dev serverInline workers register handlers at boot; restart the dev server (or, for site.deploy, the code calls ensureInlineJob itself).
Publishing says the web app could not be revalidatedapps/web is not running or WEB_URL/REVALIDATE_SECRET differ between the apps.
The webhook to a local starter is refusedIn development private targets are allowed unless OUTBOUND_ALLOW_PRIVATE=0; check that flag.
drizzle-kit complains about DATABASE_URLpackages/db/drizzle.config.ts reads ../../.env; run the db:* scripts from the repository root.
Production build refuses to startBETTER_AUTH_SECRET or REVALIDATE_SECRET shorter than 32 characters (@kwisple/core/secrets).
Database-backed tests are skippedThey check select 1 at start and skip when Postgres is unreachable; run pnpm infra:up.

Day-to-day checks

Before you commit, run what CI runs (details in Testing and checks):

pnpm lint
pnpm check:i18n
pnpm check:actions
pnpm typecheck
pnpm test

Last updated 2026-10-30