Appearance
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)
- Add
sections/<type>.liquidto the theme bundle (edit via the code editor, or the theme library underpackages/builder-core/src/theme/library/<theme>/). For a platform (first-party) section instead, add aSectionDeftopackages/builder-render/src/sections.ts. - Declare a
{% schema %}block:settings(your text setting) + acceptedblockstypes (image) +max_blocks. The{% schema %}is stripped by regex before render — it never executes. - In a template's
sections: [], reference the section bytype(and give itblocks: [{ type:'image', … }]). - In the Liquid, read
section.settings.*and loopsection.blocksswitching onblock.type, readingblock.settings.*(note: stored asdata, exposed assettings). - Escape output — pipe merchant values through
escape,safe_url, orsanitize_html. A data-bound section also needs adataSourceKey(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
- Add the filter to
packages/builder-render/src/engine.ts(custom filter + add its name toFILTER_ALLOWLIST). - Add the identical filter to
worker.mjsby hand (the untrusted engine is a hand-copied twin). - Add a parity case to
packages/builder-render/src/__tests__/liquid-render.test.tsasserting 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
- Add the handler under
apps/admin-api/src/routes/*and register it insrc/app.ts. - Pick the guard:
requireMembership(any member) orrequireRole('owner')(destructive). If the route has no:id(store-less), adddenyNarrowedScope(guardrail #12). - Add a
resource:verblabel insrc/middleware/scopes.tsso the auditactionis meaningful. - 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:
- Migration — a new forward-only SQL file in
db/migrations/(e.g. add a column or atenantsjsonb key). - Repo — thread the field through
packages/data-repo(read/write viaforTenant). - Route — extend the
PATCH /stores/:idhandler (apps/admin-api) with validation; it's owner/member-gated and audited. - Client — add/extend the typed method in
apps/admin-web/src/common/api.ts. - Form — the field UI in
apps/admin-web/src/features/settings/store-settings.tsx. - Refresh — call the right
reloadafter 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
- Add the type to
DATA_SOURCE_TYPESinpackages/builder-core/src/page-builder/doc.ts. - Handle it in the resolver dispatch —
resolve-shopkit.ts(real) andresolve.tsStubResolver(samples) — mapping it to a client method and returning{ value, tags }. - Emit the right cache tags (
prod:*,col:*, …) so a change purges the right pages.
Change → which test
| You changed… | Run |
|---|---|
| a Liquid filter / the render | packages/builder-render/src/__tests__/liquid-render.test.ts (parity) |
| theme compose / publish / rebase | packages/builder-core/src/__tests__ |
| an admin route / auth | apps/admin-api/src/__tests__ (+ cross-tenant-authz.test.ts) |
| domains | apps/admin-api/src/__tests__/services/domains.test.ts |
| cart / checkout / KwikPass | packages/storefront-integrations/src/__tests__ |
| an origin handler / storefront (bundle) | apps/origin/src/__tests__ — needs MinIO (BUNDLE_S3_ENDPOINT) |
| the admin SPA | bun run --cwd apps/admin-web test (Vitest, Node env — not a DOM) |
| tenancy end-to-end | bun run prove / prove:s1 (needs the live stack) |
Run one file with node --import tsx --import ./tests/bootstrap.ts --test <path> — see Setup.