Project Structure
One folder per service, one definition per file, and a generated manifest that registers them.
This is the layout the layout lint rules enforce. Each service has a folder. Each resource has its own folder inside resources/, holding its table, its actions and its commands. One definition goes in each file. The folders mirror the URL tree.
my-app/
├── brick.config.ts # paths, dev/build/gen settings, lint
├── src/
│ ├── app.ts # the one brick() call
│ ├── secrets.ts # the only place env is read
│ ├── plugins/ # local plugins, same layout as a package
│ │ └── audit/
│ └── services/
│ ├── orders/
│ │ ├── orders.service.ts # defineService("orders", { database, ... })
│ │ ├── orders.events.ts # event catalog
│ │ ├── actions/ # service-level actions
│ │ │ ├── get-summary.action.ts
│ │ │ └── get-summary.test.ts
│ │ ├── resources/
│ │ │ ├── order/
│ │ │ │ ├── order.resource.ts # resource + crud + child tables
│ │ │ │ ├── order.table.ts
│ │ │ │ ├── actions/
│ │ │ │ │ ├── archive.action.ts
│ │ │ │ │ └── archive.test.ts
│ │ │ │ └── commands/
│ │ │ │ ├── ship.command.ts
│ │ │ │ └── ship.test.ts
│ │ │ └── order-item/
│ │ │ ├── order-item.resource.ts
│ │ │ └── order-item.table.ts
│ │ ├── tables/ # tables with no resource: outbox, join tables
│ │ │ └── outbox.table.ts
│ │ ├── listeners/
│ │ │ └── send-receipt.listener.ts
│ │ ├── workflows/
│ │ │ ├── fulfil-order.workflow.ts
│ │ │ └── book-carrier.step.ts
│ │ ├── tools/
│ │ │ └── find-order.tool.ts
│ │ └── sockets/
│ │ └── live.socket.ts
│ └── billing/
│ ├── billing.service.ts
│ └── listeners/
│ └── on-order-shipped.listener.ts
├── web/ # frontend: imports only @brickkit/core/client
│ └── main.ts
└── _brick/ # generated, not edited
├── services.gen.ts # import manifest of every definition file
└── contract.ts # typed client contractNames are scoped by their folder. A command in resources/order/commands/ is ship.command.ts, not ship-order.command.ts: the folder already says which resource it belongs to. File names are the kebab-case of the definition's name: get-summary.action.ts defines getSummary.
A resource's table sits next to it (order.table.ts). The service-level tables/ folder is only for tables that have no resource, such as the event outbox or a join table.
Service-level folders (actions/, listeners/, workflows/, tools/, sockets/) hold what belongs to the service as a whole. Listeners stay at service level even when they react to one resource's events, because events are part of the service's catalog.
A service never imports another service's tables. It calls the owning service's actions, or listens to its events and keeps its own copy. brick lint enforces this with no-cross-service-tables, which is always an error.
The services manifest
A definition file attaches itself to its service (orders.action({ ... })), so something has to import every file, or the action silently doesn't exist. Hand-written import lists drift, so the CLI generates one.
brick dev, brick build, brick gen and brick lint scan the services folder (default src/services) for files ending in .service, .events, .resource, .table, .action, .command, .tool, .socket, .listener, .workflow or .step, and write <generated>/services.gen.ts (default _brick/services.gen.ts). brick dev rewrites it when a file is added or removed. brick gen manifest writes only the manifest. The file is generated: don't edit it.
// Generated by brick dev, brick build and brick gen. Do not edit.
// Imports every definition file under the services folder, so each one
// registers itself on its service. The app entry imports this file.
import "../src/services/billing/billing.service";
import "../src/services/orders/orders.service";
import "../src/services/orders/resources/order/order.resource";
import "../src/services/billing/listeners/on-order-shipped.listener";
import "../src/services/orders/actions/get-summary.action";
import "../src/services/orders/resources/order/commands/ship.command";
// …
export {};The order is stable: service files first, then resources, then everything else, each sorted by path. The file is rewritten only when its content changes, so its timestamp doesn't move between runs.
The app entry imports the manifest once, before it builds the app:
import { brick } from "@brickkit/core";
import "../_brick/services.gen";
import { orders } from "./services/orders/orders.service";
import { billing } from "./services/billing/billing.service";
export default brick({ services: [orders, billing] });A definition file outside the layout isn't picked up, and the kind-folders lint rule reports it.
The manifest is written only when the project has a brick.config.ts. brick start and brick worker don't write it: generate it in your build step, or commit it.