Structure
This page shows how the repository is laid out and helps you decide where new code belongs.
Use this page to find your way around the repository and to decide where a new module, block or integration should go. The harness checks most of these rules, so putting a file in the right place also keeps the build passing.
The repository has twenty top-level directories and no monorepo packages. Everything ships from one project. A storefront page can therefore import a domain rule directly, without crossing a package boundary.
The tree below describes what each directory contains. The sections after it cover the conventions the harness enforces and where new code belongs.
app/
(web)/ storefront routes — pages only, no components
admin/ admin routes — pages only, no components
ai/ v1/ route handlers + services/ for the admin AI generators
api/ public API, webhooks, MCP, oRPC, Inngest, health
components/
common/ shared primitives (button, dialog, calendar, …)
web/ storefront components, including blocks/renderers/
admin/ admin components, one folder per section
data-table/ the TanStack Table wrapper used by every admin list
shared/ components used by both surfaces
server/
admin/<module>/ actions.ts · queries.ts · crud.ts · schema.ts · payloads.ts
web/ storefront read paths and the page resolver
ai/ the gated agent tool kit and registry
customers/ find-or-reconcile for the signed-in customer
messaging/ conversations and alert delivery
shared/ settings read model, facets, scarcity, policy windows
router.ts the oRPC root router
lib/ domain logic — cache.ts, blocks/, permissions/, crud/, orders/, …
services/ infrastructure — db.ts, stripe.ts, redis.ts, s3.ts, resend.ts, integrations/
functions/ Inngest functions: crons and domain-event handlers
contracts/ shared primitive and operation schemas (id, email, pagination, sorting)
config/ site, theme, pricing, fonts and activity-registry configuration
emails/ React Email templates
events/ types.ts (the domain event union) and emitter.ts
hooks/ client hooks
utils/ pure helpers — money, decimal, try-catch, search-params
types/ ambient and shared type declarations
prisma/
schema.prisma the schema — the only source of truth for it
migrations/ the history
seed.ts the seed, run by bun run db:seed
scripts/
check-*.ts the harness: 25 discipline checks and ratchets
test/ integration setup and factories
docs/ architecture and migration notes
assets/ icon sources, compiled by bun run icons
public/ static files served as-is
patches/ bun patch files (currently zsa@0.6.0)Key conventions
Treat this layout as a rule, not a suggestion. A script checks most of it. If you put a file in the wrong place, the build fails, and the mistake can't become a pattern others copy.
Route folders contain routes only
app/ contains Next.js convention files and nothing else:
page.tsx
layout.tsx
loading.tsx
error.tsx
route handlersUI for a specific route goes in components/admin/<section>/ or components/web/<section>/. No folder in app/ contains a _components/ folder.
For the same reason, scripts/check-discipline.ts rejects a route page.tsx that renders a list inline. A route file that renders a list has started to contain components.
Every mutation is a permission-wrapped server action
Actions go in server/<area>/<module>/actions.ts. Each one declares its permission with modulePermission or withAnyPermission from lib/permissions/server.
Actions save data through the module's CRUD class, which extends BaseCRUD. That class handles soft delete, activity logging and cache invalidation in one step. Callers don't have to remember three separate things.
Reads are separate from writes
Queries go next to the actions in queries.ts. Each one is wrapped in unstable_cache and tagged with a value from CACHE_TAGS.
The tag connects reads and writes. A write clears the same tag that the read was cached under.
One module, one folder
A module keeps its actions, queries, CRUD class, zod schemas and Prisma payloads together. For example, all the coupon code is in server/admin/coupons/.
Boundaries are enforced automatically
Three scripts fail the build when an import crosses a boundary the wrong way:
scripts/check-client-server-boundary.ts
scripts/check-theme-boundaries.ts
scripts/check-client-bundle-server-only.tsSee The harness.
Where to put a new thing
- A storefront section is a block kind. Add a registry entry in
lib/blocks/registry.tsand a renderer incomponents/web/blocks/renderers/. - An admin entity needs two folders,
server/admin/<module>/andcomponents/admin/<module>/. See Modules. - A reaction to an event is a handler in
functions/. See Events. - A third-party integration goes in
services/integrations/, behind the existing interface. - A shared rule or format goes in
lib/if it is a domain rule, or inutils/if it is a pure helper. Each one gets a single home, and everything else imports it from there.
When you add a file, don't ask where it might fit. Ask this instead:
Which folder already owns this job, and which existing file nearby does the same kind of work?