Litestore · Developer guide
Codebase

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 handlers

UI 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.ts

See The harness.

Where to put a new thing

  • A storefront section is a block kind. Add a registry entry in lib/blocks/registry.ts and a renderer in components/web/blocks/renderers/.
  • An admin entity needs two folders, server/admin/<module>/ and components/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 in utils/ 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?

Next Steps

On this page