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

PathResponsibility
features/<feature>/contracts.tsHTTP surface: method, path, params, request body, headers, responses, metadata, and catalog errors
features/<feature>/schemas.tsShared DTO and validation schemas that contracts, use cases, ports, client modules, and tests may import
features/<feature>/routes.tsFeature 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.tsFeature-owned authorization rules
features/<feature>/ports.tsFeature-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.tsApplication error catalog and route-owned error schemas
features/shared/domain/Shared-kernel domain concepts used across features
server/routes.tsCentral route registry and OpenAPI contract list
server/context.tsShared context blueprint reused by the runtime server and route tests
server/index.tsRuntime wiring: context, hooks, providers, and error mapping
server/providers.tsBeignet lifecycle providers installed at server startup
server/listeners.ts, server/notifications.ts, server/tasks.ts, server/outbox.ts, server/schedules.tsApp-owned workflow registries and CLI contexts for listeners, queued notification delivery, tasks, outbox draining, and schedules
server/runtime-integrity.tsOptional 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.tsShared 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:

ConcernPut it hereRead next
Endpoint shapefeatures/<feature>/contracts.tsContracts
Feature route wiringfeatures/<feature>/routes.tsServer
Request routingserver/routes.ts, server/index.ts, and app/api/Routes and server
Request lifecycle behaviorserver hooksRequest lifecycle, Hooks
Business workflowfeatures/<feature>/use-cases/Use cases
Business authorizationfeatures/<feature>/policy.ts or app-owned policy helpersAuthorization
Persistence and transactionsfeature repository ports plus ctx.ports.uow.transaction(...)Database and transactions
Audit/activity loggingctx.ports.audit plus request actor, tenant, and requestIdAudit and activity logging
Cached reads and invalidationctx.ports.cache from infra/ or a cache providerCache
Object storagectx.ports.storage from infra/ or a storage providerStorage
Uploadsfeatures/<feature>/uploads/, StoragePort, and app-owned attachment recordsUploads
Domain eventsfeatures/<feature>/domain/events/, feature listeners, Unit of Work event recorderEvents
Background workctx.ports.jobs and job definitionsJobs
Scheduled workfeatures/<feature>/schedules/ and a cron/provider triggerSchedules
Mailctx.ports.mailer and mail provider adaptersMail
Notificationsfeatures/<feature>/notifications/, optional server/notifications.ts, and ctx.ports.notificationsNotifications
Structured loggingctx.ports.logger and request logging hooksLogging
Error reportingctx.ports.errorReporter and error reporting hooksError reporting
Rate limitingcontract metadata plus rate limit hooksRate limiting
Provider startup and teardownserver/providers.tsProviders
Env vars and deployment configlib/env.tsConfig, Deployment
App errorsfeatures/shared/errors.ts and contract .errors(...)Errors
OpenAPI routeapp/api/openapi/route.tsOpenAPI
Dev-only request inspectionapp/api/devtools/[[...path]]/route.tsDevtools
UI data fetchingclient/, features/<feature>/client/, features/<feature>/components/, React QueryReact, 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:

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.