Skip to content

Recipes — common changes

Step-by-step for the changes juniors pick up most. Each ends with what to run to prove it. Read the linked chapter first; obey the guardrails.

Add a theme section (with a setting + a block)

  1. Add sections/<type>.liquid to the theme bundle (edit via the code editor, or the theme library under packages/builder-core/src/theme/library/<theme>/). For a platform (first-party) section instead, add a SectionDef to packages/builder-render/src/sections.ts.
  2. Declare a {% schema %} block: settings (your text setting) + accepted blocks types (image) + max_blocks. The {% schema %} is stripped by regex before render — it never executes.
  3. In a template's sections: [], reference the section by type (and give it blocks: [{ type:'image', … }]).
  4. In the Liquid, read section.settings.* and loop section.blocks switching on block.type, reading block.settings.* (note: stored as data, exposed as settings).
  5. Escape output — pipe merchant values through escape, safe_url, or sanitize_html. A data-bound section also needs a dataSourceKey (it renders empty without one).

Rendering & themes, Render internals. Verify:packages/builder-core/src/__tests__ (section-schema) + a manual customizer check via bun run dev.

Add a Liquid filter ⚠️ two copies

  1. Add the filter to packages/builder-render/src/engine.ts (custom filter + add its name to FILTER_ALLOWLIST).
  2. Add the identical filter to worker.mjs by hand (the untrusted engine is a hand-copied twin).
  3. Add a parity case to packages/builder-render/src/__tests__/liquid-render.test.ts asserting both engines render it byte-identically.

Render engine & isolate. This is guardrail #4 — skipping steps 2–3 is how the "prices 100× too high" bug happened. Verify: the parity test.

Add an admin API route

  1. Add the handler under apps/admin-api/src/routes/* and register it in src/app.ts.
  2. Pick the guard: requireMembership (any member) or requireRole('owner') (destructive). If the route has no :id (store-less), add denyNarrowedScope (guardrail #12).
  3. Add a resource:verb label in src/middleware/scopes.ts so the audit action is meaningful.
  4. If the SPA calls it, add a typed method to apps/admin-web/src/common/api.ts.

Control plane & AI, Control-plane internals. Verify: apps/admin-api/src/__tests__ (real Postgres).

Add a field to Store Settings (end-to-end)

There is no single Settings module — a new field is a full round-trip:

  1. Migration — a new forward-only SQL file in db/migrations/ (e.g. add a column or a tenants jsonb key).
  2. Repo — thread the field through packages/data-repo (read/write via forTenant).
  3. Route — extend the PATCH /stores/:id handler (apps/admin-api) with validation; it's owner/member-gated and audited.
  4. Client — add/extend the typed method in apps/admin-web/src/common/api.ts.
  5. Form — the field UI in apps/admin-web/src/features/settings/store-settings.tsx.
  6. Refresh — call the right reload after save, or the UI goes stale (there is no shared cache).

Admin web, Data model. Verify: the admin-api route test + bun run --cwd apps/admin-web test.

Register a commerce data source

  1. Add the type to DATA_SOURCE_TYPES in packages/builder-core/src/page-builder/doc.ts.
  2. Handle it in the resolver dispatch — resolve-shopkit.ts (real) and resolve.ts StubResolver (samples) — mapping it to a client method and returning { value, tags }.
  3. Emit the right cache tags (prod:*, col:*, …) so a change purges the right pages.

Commerce data & resolver.

Change → which test

You changed…Run
a Liquid filter / the renderpackages/builder-render/src/__tests__/liquid-render.test.ts (parity)
theme compose / publish / rebasepackages/builder-core/src/__tests__
an admin route / authapps/admin-api/src/__tests__ (+ cross-tenant-authz.test.ts)
domainsapps/admin-api/src/__tests__/services/domains.test.ts
cart / checkout / KwikPasspackages/storefront-integrations/src/__tests__
an origin handler / storefront (bundle)apps/origin/src/__tests__needs MinIO (BUNDLE_S3_ENDPOINT)
the admin SPAbun run --cwd apps/admin-web test (Vitest, Node env — not a DOM)
tenancy end-to-endbun run prove / prove:s1 (needs the live stack)

Run one file with node --import tsx --import ./tests/bootstrap.ts --test <path> — see Setup.