Appearance
Start here — setup, run & debug
New to the codebase and picking up a ticket? This page gets you from a fresh clone to a running store you can change and debug. (To understand the system, read the book from Chapter 1; this is the practical companion.)
Prerequisites
- Docker (for Postgres + MinIO), Bun (package manager / task runner), Node 22 (the test + dev runner).
- The repo's own
README.mdat the root is the canonical setup note — start there if anything here drifts.
Run the whole stack
bash
bun install # install workspace deps
bun run dev # dev/all.ts — the one command that boots everythingbun run dev (dev/all.ts) does it all: docker compose up (Postgres :5433, MinIO :9000) → migrate → ensure the bucket → then three processes, all with RATIO_LOCAL=true:
| Service | Port | What |
|---|---|---|
| storefront | :8080 / :9090 | edge Worker (:8080) + origin (:9090) |
| admin-api | :8787 | the control plane |
| admin-web | :5173 | the merchant/admin SPA (Vite) |
Ctrl-C stops the three services; the DB container stays up (bun run db:down to drop it). If Postgres was created by an older major version, dev/all.ts recreates the volume for you.
Create a store and view it
There are no seeded stores — a store comes from onboarding:
- Open the SPA at
http://localhost:5173and sign in (dev Clerk). - Create a store via the onboarding wizard (
/stores/new) — it publishes a live store on the default theme. (Scriptable alternative:scripts/onboard.ts— see Ops scripts.) - View its storefront on the edge (:8080). Local multi-tenancy is keyed by host, so use either the store's
<id>.localhost:8080alias or the localhost-only overridehttp://localhost:8080/?store=<tenantId>(the?store=override is gated to localhost — see Edge internals). - Make a trivial theme edit in the code editor, publish, and refresh the storefront.
bun run reset:localtruncates app tables for a clean re-onboard;bun run recoverre-heals the base library after a DB/object-store wipe.
Validate before you push
PRs get no CI — you are the pre-merge gate. Run the same four checks CI runs:
bash
bun run typecheck && bun run lint && bun run format:check && bun run testHusky runs lint-staged (eslint --fix + prettier) on commit. See Testing & CI for the full picture, including the two test runners.
Two traps CI won't save you from
- PRs run zero CI (push-to-
mainonly) — a broken change passes review if you didn't run the checks locally. - CI has no MinIO, so ~8 object-store tests skip silently (green ≠ they ran). Run them locally with MinIO up and
BUNDLE_S3_ENDPOINTset. And CI Postgres is :5432, local Docker is :5433 — readDATABASE_URL, never hardcode a port.
Run one test (not the whole suite)
Backend/package tests use node:test (not Bun), so a single file is:
bash
DATABASE_URL=postgres://poc:poc@localhost:5433/s2poc_test \
node --import tsx --import ./tests/bootstrap.ts --test packages/builder-render/src/__tests__/liquid-render.test.tsbootstrap.ts must be --import'd (it configures the DB pool + sets EDGE_SECRET; without it a test fails closed on DATABASE_URL/the edge secret). Object-store tests also need BUNDLE_S3_ENDPOINT=http://localhost:9000 (+ BUNDLE_S3_BUCKET/KEY/SECRET). The SPA suite is bun run --cwd apps/admin-web test (Vitest).
Debug — is this page cached or rendered?
Storefront pages are cacheable, so "it didn't change" is usually a cache question:
x-ratio-stale: 1on a response = the edge served a stale copy (origin was down/slow). Its absence + a fresh body = a real render or a fresh cache hit./__statson the origin (:9090) is a render counter; the edge (:8080) has/__admin/stats(cache hits). Hit a page 5× and watch: a cacheable page should show 1 render + 4 hits (this is whatbun run prove:s1asserts).- A page that forgot
s-maxageis silently never cached — every request hits the origin (a cost/perf regression, no error). See Rendering & themes · the render cycle.
Reproduce a per-shopper flow (cart / account / checkout)
These paths are no-store and per-shopper, so they bypass the cache and the prove harnesses — reproduce them by sending the right cookie to the origin/edge:
/accountis login-gated: without a KwikPass cookie it 302s to/. To hit the signed-in path, send the access cookiert_kp_at=<token>(its value is the GoKwik access token). See Commerce internals · account./cartreads the httpOnly server cart cookiert_cart; add-to-cart isPOST /cart/add.
Logs
Every service logs one-line JSON ({ lvl, svc, time, reqId, … }); the edge emits one access log per request (tenant, status, stale, ms, pathname). To trace a failing local request, grab its reqId from the response's x-request-id and grep the service output for that id. Full model: Observability.