Built by the creator of tx|Primitives for memory, tasks & orchestrationVisit tx docs
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/
  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 implementations

If you also need database schema artifacts:

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

This additionally generates the following artifacts:

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