Contracts
Shared API schemas and types using effect/Schema for validation and contract boundaries.
Contracts (packages/contracts)
The contracts package defines shared API schemas and types that are consumed by both the API server and client applications. All validation is done with effect/Schema. Zod is banned from the project.
Purpose
Contracts provide a single source of truth for request/response payload shapes, enum types and value sets (such as subscription statuses, invitation statuses, and member roles), and shared type definitions imported by domain and API layers.
Usage
Contracts are imported by the API routes for request validation and by the domain layer for type definitions:
// Used in domain layer
import { type SubscriptionStatus, subscriptionStatuses } from '@tx-agent-kit/contracts'
// Used in API route validation
import { createOrganizationRequestSchema } from '@tx-agent-kit/contracts'Schema-First Boundaries
The project enforces schema-first validation at all system boundaries. Rather than using raw types, data crossing trust boundaries (API input, database results) must be decoded through effect/Schema:
import { Schema } from 'effect'
export const subscriptionStatusSchema = Schema.Literal('active', 'inactive', 'trialing', 'past_due', 'canceled', 'paused', 'unpaid')
export type SubscriptionStatus = Schema.Schema.Type<typeof subscriptionStatusSchema>
export const subscriptionStatuses = ['active', 'inactive', 'trialing', 'past_due', 'canceled', 'paused', 'unpaid'] as constGovernance
effect/Schema is the only validation library allowed; Zod imports will fail lint checks. Contract types are consumed by packages/core domain layers through re-exports. The API server's openapi.json is generated from these contract definitions, ensuring the spec always matches the runtime validation.
Domain Event Contracts
Domain event types are registered centrally in packages/contracts/src/literals.ts. Two arrays govern the event system:
| Export | Purpose |
|---|---|
domainEventTypes | Registered event type strings following the <aggregate>.<past-tense-verb> convention (e.g., organization.created) |
domainEventAggregateTypes | Registered aggregate types that can emit domain events |
Every domain event must have its type string listed in domainEventTypes before it can be written to the outbox. The aggregate type must also appear in domainEventAggregateTypes. These arrays are the canonical registry consumed by the domain layer, the outbox repository, and structural enforcement scripts.
import { domainEventTypes, domainEventAggregateTypes, domainEventStatuses } from '@tx-agent-kit/contracts'domainEventStatuses defines the allowed lifecycle states for outbox events: pending | processing | published | failed.
Naming Derivation
The enforcement script scripts/lint/enforce-domain-event-contracts.mjs derives payload type names from the event type string using dot-split PascalCase conversion:
| Event Type | Interface Expected | Schema Expected |
|---|---|---|
organization.created | OrganizationCreatedEventPayload | OrganizationCreatedEventPayloadSchema |
billing.subscription_updated | BillingSubscriptionUpdatedEventPayload | BillingSubscriptionUpdatedEventPayloadSchema |
Each segment is PascalCased individually, then concatenated. This convention is enforced at lint time — adding a new event type requires creating both the interface and schema with the derived names.
Data Retention Contracts
The contracts package also defines which tables are subject to automated retention policies:
export const retentionTableNames = [
'auth_login_sessions',
'auth_login_refresh_tokens',
'auth_login_oidc_states',
'password_reset_tokens',
'auth_login_audit_events',
'subscription_events',
'domain_events',
'invitations',
] as const
export type RetentionTableName = (typeof retentionTableNames)[number]Every table in retentionTableNames must have a corresponding entry in the generated retention_settings reconcile schema. The structural enforcement script scripts/lint/enforce-domain-event-contracts.mjs validates this parity at lint time.