2 min read
Guardrails and sweeps
This page summarises the guardrail sweeps. The source of truth stays at
docs/guardrails.md
in the repo, next to the sweep source it documents.
bun run test:sweeps runs a set of vitest “sweeps” over the whole repo — static checks
that catch drift across 1,400+ files, cheaper than a code review. CI runs them after
bun run lint and bun run typecheck, before bun run test.
The ratchet pattern
Every sweep (except dead-code) is a ratchet: a typed ALLOWLIST next to it lists every
violation that existed the day the sweep was added, each with a one-line reason. The sweep
fails the moment someone adds a new violation that isn’t on the list.
- Fix a violation by removing its allow-list entry.
- Allow-list a new, deliberate exception only with a real one-line reason — not just to make the sweep pass.
The sweeps
| Sweep | What it checks |
|---|---|
service-layer | Direct @commerce/db / drizzle-orm imports are only allowed in packages/core, packages/db, packages/api/src/services, scripts/. |
client-imports-server | A 'use client' module must not value-import server-only packages (@commerce/core, @commerce/db, @commerce/auth, storage/payments/shipping connectors, @commerce/api). |
no-orphans | An exported function/const/class in packages/*/src with no importer anywhere is an orphan. |
dead-code | Fixed list of paths/dependencies that were deliberately removed and must never come back — not a ratchet. |
dangerous-html | Every dangerouslySetInnerHTML usage must be on the allow-list, with a reason it’s safe. |
money-format | Every Intl.NumberFormat(...) call site outside the canonical money module is on the allow-list. |
hardcoded-colours | Hex colour literals in UI .tsx files outside the allow-list — use CSS variables/tokens instead. |
source-of-truth-header | New files under packages/core/src and packages/plugins/src must start with a SOURCE OF TRUTH header comment. |
pinned-actions | Every uses: in .github/workflows is pinned to a full commit SHA with a # vX.Y.Z comment. No exceptions. |
deploy-compose | deploy/docker-compose.prod.yml passes every app setting from packages/config/src/env.ts to the apps (and deploy/env.production.example documents it), and only worker and backup get outbound internet besides the web-facing apps. |
storefront-action-store | No storefront server action takes a store id from its caller; the store comes from the hostname (requestStoreId). |
turbo-cache-graph | The cached test/typecheck tasks see the plugin packages that core reaches through commerce.config.ts: the root //#plugin-graph task lists exactly what the root configs import, and every package built on core depends on it. |
admin-routes | No new hardcoded admin path in an href or redirect/push/replace under apps/admin/src; use routes from lib/routes.ts. Allow-list is a count per file that may only go down. |
site-config | The brand name appears only in siteConfig (@commerce/config/site), never as a literal elsewhere. |
Cross-store leak test
packages/api/test/cross-store-leak.test.ts runs with the api suite (bun run test, needs
Postgres). It seeds two stores with a row in every table that has a store_id (read from the
schema, so new tables are covered), then, signed in as store A, calls every oRPC procedure
and every storefront data loader with store A’s ids and again with store B’s ids. It fails if
a response shows anything of store B, if any of B’s rows changed, or if A’s rows end up
pointing at B’s. Procedures that call an outside service are skipped by name, with a reason.
Details in docs/guardrails.md.
Running
bun run test:sweeps
bun run test:sweeps -- scripts/sweeps/service-layer.test.ts # one sweepFor the full write-up of each sweep, the fix/allow-list guidance, and the server-only
package and router idiom notes, read docs/guardrails.md in the repo.