tx-agent-kit
Guides

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-run

This 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 invoice

This creates the domain slice under packages/core/src/domains/billing/:

packages/core/src/domains/billing/
  events.ts                       # PUBLIC cross-domain event contract (the ONLY file siblings may import)
  domain/billing-domain.ts        # Entity types, value objects, pure rules
  domain/billing-events.ts        # Event payload shapes (re-exported from events.ts)
  ports/billing-ports.ts          # Repository port with Effect Context.Tag
  application/billing-service.ts  # Use-case orchestration service
  adapters/billing-adapters.ts    # Adapter implementations

The events.ts file at the domain root is structurally significant — it is the ONLY file other domains may import from this one. Everything else is private to the slice. See DDD Pattern → Cross-domain boundaries for the rules and enforcement.

If you also need database schema artifacts:

pnpm scaffold:crud --domain billing --entity invoice --with-db

This additionally generates the following artifacts:

FileContents
packages/infra/db/src/effect-schemas/invoices.tsEffect schema with InvoicesRowSchema and InvoicesRowShape
packages/infra/db/src/factories/invoices.factory.tsTest 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 const

Use 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
  organizationId: 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/infra/db/src/schema.ts:

export const invoices = pgTable('invoices', {
  id: uuid('id').primaryKey().defaultRandom(),
  organizationId: uuid('organization_id').notNull().references(() => organizations.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/infra/db/src/effect-schemas/invoices.ts, a factory in packages/infra/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/infra/db/src/repositories/:

// packages/infra/db/src/repositories/invoice-repository.ts
// Must import effect-schemas for row decoding
// Must use provideDB() for query execution
// Must not use .then() Promise chaining

Step 7: Expose API Routes

Add route handlers in apps/api/src/routes/ and regenerate the OpenAPI spec:

pnpm openapi:generate

Step 8: Regenerate API Client

Update the web and mobile typed API hooks:

pnpm api:client:generate

Step 9: Validate

Run the full check suite:

pnpm lint && pnpm type-check && pnpm test

This validates all invariants: table parity, kind markers, layer direction, test colocation, and more.

On this page