App architecture
A Beignet app keeps production code in a small set of predictable places, and
each folder owns one kind of decision. This page is the map: what lives where,
where each production concern goes, and the dependency direction
beignet lint enforces. For the first-hour loop, use
Quickstart; for the guided tour of one feature, use
Build your first feature.
What goes where
| Path | Responsibility |
|---|---|
features/<feature>/contracts.ts | HTTP surface: method, path, params, request body, headers, responses, metadata, and catalog errors |
features/<feature>/schemas.ts | Shared DTO and validation schemas that contracts, use cases, ports, client modules, and tests may import |
features/<feature>/routes.ts | Feature route group that maps contracts to use cases |
features/<feature>/use-cases/ | Application workflows with input and output validation |
features/<feature>/domain/ | Feature-owned entities, value objects, and domain events |
features/<feature>/client/ | Feature-owned client data-fetching functions, React Query hooks, and other browser-safe feature helpers |
features/<feature>/components/ | Feature-owned UI |
features/<feature>/policy.ts | Feature-owned authorization rules |
features/<feature>/ports.ts | Feature-specific dependency interfaces such as repositories |
features/<feature>/notifications/, uploads/, jobs/, listeners/, schedules/, tasks/, seeds/ | Feature-owned workflow artifacts, added by generators when needed |
features/<feature>/tests/ | Feature behavior tests, with shared factories in tests/factories/ |
features/shared/errors.ts | Application error catalog and route-owned error schemas |
features/shared/domain/ | Shared-kernel domain concepts used across features |
server/routes.ts | Central route registry and OpenAPI contract list |
server/context.ts | Shared context blueprint reused by the runtime server and route tests |
server/index.ts | Runtime wiring: context, hooks, providers, and error mapping |
server/providers.ts | Beignet lifecycle providers installed at server startup |
server/listeners.ts, server/notifications.ts, server/tasks.ts, server/outbox.ts, server/schedules.ts | App-owned workflow registries and CLI contexts for listeners, queued notification delivery, tasks, outbox draining, and schedules |
server/runtime-integrity.ts | Optional boot check comparing app-declared workflow artifacts against runtime registries |
app/api/ | Thin Next.js route files that call createApiRoute(getServer) or focused route helpers |
app-context.ts | Shared request context type used by handlers, hooks, and use cases |
ports/ | App-wide dependency interfaces shared across features |
infra/ | Concrete adapters and default port wiring, including infra/app-ports.ts |
lib/ | Small app helpers: env.ts, auth.ts, server-only context helpers, and the use-case.ts builder |
client/ | Typed Beignet client and frontend adapter factories |
The CLI starter scaffolds the subset of this structure it ships: contracts,
schemas, routes, use cases, components, ports, infra, server composition, and
the client. Workflow-tier artifacts such as jobs, listeners, notifications,
schedules, uploads, the outbox registry, and operational tasks appear when
beignet make generators add them, along with their lib/ builders and
server registries. beignet doctor treats their absence as fine and their
misplacement as drift.
Production concern map
When you know what you need to build, this table says where it goes:
| Concern | Put it here | Read next |
|---|---|---|
| Endpoint shape | features/<feature>/contracts.ts | Contracts |
| Feature route wiring | features/<feature>/routes.ts | Server |
| Request routing | server/routes.ts, server/index.ts, and app/api/ | Routes and server |
| Request lifecycle behavior | server hooks | Request lifecycle, Hooks |
| Business workflow | features/<feature>/use-cases/ | Use cases |
| Business authorization | features/<feature>/policy.ts or app-owned policy helpers | Authorization |
| Persistence and transactions | feature repository ports plus ctx.ports.uow.transaction(...) | Database and transactions |
| Audit/activity logging | ctx.ports.audit plus request actor, tenant, and requestId | Audit and activity logging |
| Cached reads and invalidation | ctx.ports.cache from infra/ or a cache provider | Cache |
| Object storage | ctx.ports.storage from infra/ or a storage provider | Storage |
| Uploads | features/<feature>/uploads/, StoragePort, and app-owned attachment records | Uploads |
| Domain events | features/<feature>/domain/events/, feature listeners, Unit of Work event recorder | Events |
| Background work | ctx.ports.jobs and job definitions | Jobs |
| Scheduled work | features/<feature>/schedules/ and a cron/provider trigger | Schedules |
ctx.ports.mailer and mail provider adapters | ||
| Notifications | features/<feature>/notifications/, optional server/notifications.ts, and ctx.ports.notifications | Notifications |
| Structured logging | ctx.ports.logger and request logging hooks | Logging |
| Error reporting | ctx.ports.errorReporter and error reporting hooks | Error reporting |
| Rate limiting | contract metadata plus rate limit hooks | Rate limiting |
| Provider startup and teardown | server/providers.ts | Providers |
| Env vars and deployment config | lib/env.ts | Config, Deployment |
| App errors | features/shared/errors.ts and contract .errors(...) | Errors |
| OpenAPI route | app/api/openapi/route.ts | OpenAPI |
| Dev-only request inspection | app/api/devtools/[[...path]]/route.ts | Devtools |
| UI data fetching | client/, features/<feature>/client/, features/<feature>/components/, React Query | React, React Query |
Dependency direction
The rule is simple: transport, application behavior, and infrastructure do not
own each other. A feature owns its contracts, route group, use cases, policy,
domain model, UI, and feature-specific ports. App-wide ports describe
dependencies shared across features. Infra implements those ports. Domain
concepts that are genuinely shared across features live in
features/shared/domain/.
beignet lint enforces the most important directions. Domain and use cases
cannot import infra, UI, route, client, provider, or framework code. Feature
domain cannot import another feature's domain unless it comes from
features/shared/domain. Route files cannot import infra or UI, and infra
adapters cannot import UI, routes, server modules, or clients. Contract files
and everything reachable from client/ or "use client" modules are also
checked as client-safe import graphs that must not reach server-only code; see
the CLI reference for the full lint rules.
Use boundary markers as side-effect imports when a module outside a canonical server-only folder still must stay out of client bundles:
import "@beignet/core/server-only";Two placement notes that follow from the rule:
- Small feature-root helper modules are allowed — for example a
features/issues/history.tswith pure helpers shared across the feature's use cases. Keep them pure: as soon as a helper needs a dependency, give it a port. - Test placement follows ownership: feature behavior tests live in
features/<feature>/tests/, while infra and server modules may keep adjacent*.test.tsfiles beside the module they exercise, such as a repository test next to its adapter.
Feature-owned UI
Product UI lives with the feature it serves, in
features/<feature>/components/. Feature-specific client data-fetching
functions, React Query query/mutation options, invalidation helpers, hooks, and
browser-safe helpers may live beside the feature in
features/<feature>/client/. Shared client wiring — the typed Beignet client,
React Query helper, React Hook Form helper, upload client, and QueryClient
provider — lives in client/. Feature components import those shared helpers,
their feature's client helpers, and their feature's contracts, then call
endpoints through React Query and form adapters.
Server-only request context helpers that bridge Server Components to Beignet's
request-scoped AppContext should live outside client/, usually in
lib/server-context.ts with an @beignet/core/server-only marker. It is
appropriate for layouts and Server Components to read request metadata,
ctx.auth, and ctx.tenant from that context for redirects and shell state.
Feature data and business workflows should still go through use cases.
Server-only React Query prefetch helpers may live in
lib/server-react-query.ts and consume that context. Keep the
contract-derived query key from rq(contract).queryOptions(...), but call the
use case directly only for thin { contract, useCase } route bindings whose
output matches the contract success body.
components/ may contain React Server Components and Client Components.
Server Components can call server-only modules when they are not reachable
from a client root. Client Components and anything they import cannot reach
use cases, route groups, infra adapters, server modules, provider packages,
or app-context.ts — keep server-only workflows behind route groups and
explicit server entrypoints. See React for the component patterns.
Server composition
Features keep route wiring local in features/<feature>/routes.ts, and the
server composes them at the boundary: server/routes.ts owns the central
route list, server/context.ts declares the context blueprint once for the
runtime and route tests, server/index.ts assembles ports, providers, hooks,
and routes, and a catch-all app/api/[[...path]]/route.ts exposes
createApiRoute(getServer). That convention gives the CLI one source of truth
for route inspection, generation, OpenAPI wiring, and drift checks while
keeping provider startup out of Next build-time route imports. The code for
each piece is on Routes and server.
Custom paths
Use beignet.config.ts when your app keeps the same architecture under
different paths:
import { defineConfig } from "@beignet/cli/config";
export default defineConfig({
paths: {
appContext: "src/app-context.ts",
contracts: "src/features",
features: "src/features",
routes: "src/app/api",
server: "src/core/server/index.ts",
listeners: "src/core/server/listeners.ts",
},
});Config changes where the CLI looks and writes. It does not replace the
architecture: feature contracts still define the HTTP boundary, the server
still registers route groups, and application code still belongs behind use
cases and ports. When appContext moves under a source root such as
src/app-context.ts, client setup follows that root at src/client/index.ts.
The config can also declare app-owned names for Beignet operational database tables when they differ from provider defaults:
import { defineConfig } from "@beignet/cli/config";
export default defineConfig({
database: {
tables: {
audit: "audit_events",
},
schemaSources: ["@acme/db/schema"],
},
});doctor uses those table names when it checks Drizzle-backed audit,
idempotency, and outbox wiring. Runtime code still needs matching provider
tableName options where setup statements and ports are created.
Use database.schemaSources when Drizzle table definitions live outside the
app's canonical database files, such as a shared workspace package. Entries can
be app-relative files or directories, @/ app paths, or package specifiers.