Adding a Domain
Full walkthrough of the golden path for scaffolding a new CRUD domain slice.
This guide walks through the golden path for adding a new domain to the system. The scaffold CLI generates the boilerplate; you fill in the business logic.
Step 1: Dry Run
Preview what the scaffold will create without writing any files:
pnpm scaffold:crud --domain billing --entity invoice --dry-runThis shows the full list of files that would be generated, their paths, and the template content. Review this output to confirm the domain and entity names are correct.
Step 2: Generate
Run the scaffold for real:
pnpm scaffold:crud --domain billing --entity invoiceThis creates the domain slice under packages/core/src/domains/billing/:
packages/core/src/domains/billing/
domain/billing-domain.ts # Entity types, value objects, pure rules
ports/billing-ports.ts # Repository port with Effect Context.Tag
application/billing-service.ts # Use-case orchestration service
adapters/billing-adapters.ts # Adapter implementationsIf you also need database schema artifacts:
pnpm scaffold:crud --domain billing --entity invoice --with-dbThis additionally generates the following artifacts:
| File | Contents |
|---|---|
packages/db/src/effect-schemas/invoices.ts | Effect schema with InvoicesRowSchema and InvoicesRowShape |
packages/db/src/factories/invoices.factory.ts | Test factory with createInvoicesFactory |
Step 3: Add Contracts
Define the shared API types in packages/contracts:
// packages/contracts/src/billing.ts
import { Schema } from 'effect'
export const InvoiceStatusSchema = Schema.Literal('draft', 'sent', 'paid', 'cancelled')
export type InvoiceStatus = Schema.Schema.Type<typeof InvoiceStatusSchema>
export const invoiceStatuses = ['draft', 'sent', 'paid', 'cancelled'] as constUse effect/Schema exclusively. Zod is banned.
Step 4: Implement Domain Logic
Fill in the generated domain file with entity types, validation rules, and pure business logic:
// packages/core/src/domains/billing/domain/billing-domain.ts
export interface InvoiceRecord {
id: string
workspaceId: string
amount: number
status: InvoiceStatus
createdAt: Date
}
export const isValidInvoiceAmount = (amount: number): boolean =>
amount > 0 && Number.isFinite(amount)Remember: no Date.now(), new Date(), or Math.random() in domain code.
Step 5: Update Database Schema
If this domain requires persistence, update packages/db/src/schema.ts:
export const invoices = pgTable('invoices', {
id: uuid('id').primaryKey().defaultRandom(),
workspaceId: uuid('workspace_id').notNull().references(() => workspaces.id),
amount: integer('amount').notNull(),
status: varchar('status', { length: 50 }).notNull().default('draft'),
createdAt: timestamp('created_at').notNull().defaultNow()
})Then ensure parity artifacts exist: an Effect schema in packages/db/src/effect-schemas/invoices.ts, a factory in packages/db/src/factories/invoices.factory.ts, and both re-exported from their respective index.ts files.
Step 6: Add Repository
Implement the concrete repository in packages/db/src/repositories/:
// packages/db/src/repositories/invoice-repository.ts
// Must import effect-schemas for row decoding
// Must use provideDB() for query execution
// Must not use .then() Promise chainingStep 7: Expose API Routes
Add route handlers in apps/api/src/routes/ and regenerate the OpenAPI spec:
pnpm openapi:generateStep 8: Regenerate API Client
Update the web and mobile typed API hooks:
pnpm api:client:generateStep 9: Validate
Run the full check suite:
pnpm lint && pnpm type-check && pnpm testThis validates all invariants: table parity, kind markers, layer direction, test colocation, and more.