Skip to content

Agents

About this document

Audience: agentic coding tools (Claude Code, Codex, Kiro, Cursor) and the humans guiding them.
Purpose: the single entry point for any agent working in this repository.
Read order: read this file first, every time you start a new task. Then load only the documents this file points you to.


1. How to use this repository

This is a specification-first project. Every architectural and product decision is documented before code is written. Your job as an agent is to:

  1. Find the requirement. Every task should map to one or more requirement IDs (R-XXX-NNN or N-XXX-NNN) from REQUIREMENTS.md. If you can't map a task to a requirement, stop and ask.
  2. Read the relevant docs. Use the routing table in §3 to load only what you need.
  3. Match the conventions. The conventions in this file and the layer rules in ARCHITECTURE.md are not suggestions.
  4. Implement narrowly. Don't change unrelated files. Don't refactor things you weren't asked to refactor.
  5. Surface uncertainty. When a decision is ambiguous, note it and propose options rather than picking silently.

2. Documents in this repository

File One-line purpose
Vision What this product is, who it's for, the principles behind tradeoffs.
Requirements Authoritative list of functional + non-functional requirements with stable IDs.
Architecture System design: bounded contexts, communication patterns, deployment shape.
Data Model Database schemas, tables, columns, indexes, constraints.
API Conventions REST conventions, error format, idempotency, optimistic locking, endpoint catalogue.
Roadmap Sequenced milestones from MVP through deferred features.
Agents This file. Routing and conventions for agents.
Glossary Definitions of domain terms used across documents and code.

3. Routing: which document covers which question

Load only what you need; context is not free.

Question Read
Why a feature exists / is in scope / is deferred Vision, Requirements
Build order Roadmap
Domain term definition Glossary
System structure, context ownership, cross-context comms, layer rules, folder layout Architecture (§1–7)
Auth, sessions, roles, permissions Architecture §8, API Conventions §9–10
Config, observability, reliability, extension points, key decisions Architecture §11–15
Database schema, money/ID/timestamp/soft-delete conventions, cross-schema refs Data Model
URL design, request/response shape, pagination, idempotency, errors, endpoint catalogue API Conventions
Where a transaction lives (regular vs investment) Data Model §4.2 + §5.2

If your question doesn't fit, read Architecture and Requirements, then ask.


4. Hard rules (do not violate)

These rules are non-negotiable. Violating them is always a bug. If a rule conflicts with a task you've been given, stop and ask the human.

4.1 Architectural

  1. Domain layer has no infrastructure imports. No DB clients, HTTP libraries, loggers, ORMs, framework decorators. Pure TypeScript with standard library and small pure utilities only.
  2. Repository interfaces live in domain/. Implementations live in infrastructure/.
  3. No cross-context database access. Each context owns its schema. No FK constraints between schemas. Cross-schema references are validated at the application layer.
  4. No direct cross-context internal calls. Cross-context interactions go through events or through a context's deliberately public read API exposed in application/.
  5. HTTP route handlers are thin. Parse request, call use case, format response. No business logic.
  6. Logger, Clock, EventPublisher, IdGenerator are injected ports. Never use console.log, new Date(), or static event dispatch in domain or application code.

4.2 Data

  1. Money is always BIGINT minor units + currency code. Never DECIMAL, never FLOAT, never NUMBER. In JSON, money is { "minor": int, "currency": string }. Never a decimal number.
  2. Quantities are always BIGINT × 10^6. Same rule.
  3. All entity IDs are UUIDv7, generated at the application layer.
  4. All shared mutable entities have a version INTEGER column. Updates use WHERE id = :id AND version = :expected_version.
  5. All timestamps are TIMESTAMPTZ UTC. Logical dates are DATE (no time, no zone).
  6. Soft-delete uses deleted_at TIMESTAMPTZ NULL. Hot-path queries use partial indexes.

4.3 API

  1. Snake_case for all field names. In requests, responses, and error details.
  2. All error responses follow the shape in API Conventions §8.1. With a stable code, English message, optional details, and request_id.
  3. All mutations require optimistic locking version when the entity has one. Either via If-Match header or in-body version. Mismatch → 409 with current state.
  4. Cross-family access returns 404, not 403. Never leak the existence of a family the caller can't see.

4.4 Process

  1. Every task maps to a requirement ID. Reference IDs in commits and PRs (e.g., [R-ACC-002](requirements.md#13-accounts "Cash accounts have a non-negative balance invariant; transactions that would violate it are rejected."){.req-link}: enforce cash account non-negative invariant).
  2. Domain logic comes first; infrastructure comes last. Build and test the domain layer before wiring it to a database.
  3. The domain layer can be tested without a database, HTTP server, or any external service. If your test needs to mock infrastructure to test domain code, your domain code is leaking.

5. Folder layout (canonical)

This is an Nx monorepo. Bounded-context libs and shared libs all live under libs/. Apps live under apps/.

.
├── apps/
│   ├── api/          # @oblyk/api — HTTP entry point; src/middleware/ for HTTP-specific infra
│   ├── worker/       # @oblyk/worker — background worker
│   ├── web/          # SPA (React or Vue, TBD)
│   ├── bot/          # Telegram bot
│   └── cli/          # CLI (D)
├── libs/
│   ├── identity/     # @oblyk/identity
│   │   ├── domain/
│   │   ├── application/
│   │   └── infrastructure/
│   ├── family/       # @oblyk/family
│   ├── ledger/       # @oblyk/ledger
│   ├── investment/   # @oblyk/investment
│   ├── currency/     # @oblyk/currency
│   ├── import/       # @oblyk/import
│   ├── reporting/    # @oblyk/reporting
│   ├── audit/        # @oblyk/audit
│   ├── notification/ # @oblyk/notification
│   └── shared/       # grouping folder (not an Nx project)
│       ├── domain-primitives/  # @oblyk/domain-primitives
│       ├── types/              # @oblyk/types
│       ├── event-bus/          # @oblyk/event-bus
│       ├── config/             # @oblyk/config
│       ├── logging/            # @oblyk/logging
│       └── test-helpers/       # @oblyk/test-helpers
├── infra/
│   ├── docker-compose.yml
│   ├── docker-compose.local.yml
│   ├── docker-compose.cloud.yml
│   ├── caddy/
│   └── migrations/
└── docs/
    └── content/

5.1 Nx boundary rules

Enforced via @nx/enforce-module-boundaries in eslint.config.mjs.

  • @oblyk/<context> may import @oblyk/domain-primitives, @oblyk/event-bus, @oblyk/config, @oblyk/logging, @oblyk/types, and other contexts' public barrel (index.ts) only — never internal paths like @oblyk/family/infrastructure/....
  • apps/* may import any @oblyk/*.
  • libs/shared/* libs may not import from any @oblyk/<context>.

6. Where things go (decision table)

Thing Goes in
Entity libs/<ctx>/domain/entities/<name>.entity.ts
Value object libs/<ctx>/domain/value-objects/<name>.value-object.ts
Repository interface libs/<ctx>/domain/repositories/<name>.repository.ts
Repository implementation libs/<ctx>/infrastructure/persistence/<name>.postgres-repository.ts
Domain service (rare) libs/<ctx>/domain/services/<name>.ts
Use case + DTOs libs/<ctx>/application/use-cases/<verb>-<noun>.use-case.ts (DTOs in same file)
Public read API libs/<ctx>/application/public/<ctx>.read-api.ts
Event type definition libs/<ctx>/domain/events/<ctx>.events.ts; cross-app payload types in @oblyk/types
Event publisher call Inside the use case, via injected EventPublisher port
Event subscriber libs/<ctx>/application/event-handlers/<event>.handler.ts
Subscriber wiring libs/<ctx>/infrastructure/wiring/event-subscriptions.ts
HTTP route + Zod schema libs/<ctx>/infrastructure/http/<resource>.routes.ts
Job consumer libs/<ctx>/infrastructure/jobs/<name>.consumer.ts
Job producer Inside the use case, via injected JobQueue port
Migration infra/migrations/<timestamp>_<description>.sql
New env var libs/shared/config/src/schema.ts; document in Architecture §11
Shared utility / cross-app type libs/shared/<area>/ / @oblyk/types

If a file doesn't fit any row, stop and ask.


7. Naming conventions

7.1 Files

  • Dot-separated parts, kebab-case within each part: <name>.<type>.ts.
  • record-expense.use-case.ts, transaction.entity.ts, money.value-object.ts, user.repository.ts, ledger.events.ts, ledger.exceptions.ts.
  • Type suffixes: .entity, .value-object, .repository, .use-case, .handler, .routes, .consumer, .adapter, .events, .exceptions.
  • No type suffix for files that are purely a type/interface collection with an obvious name (e.g., base-entity.ts, index.ts).
  • Test files: <name>.<type>.test.ts (e.g., record-expense.use-case.test.ts). Use .test.ts project-wide.

7.2 TypeScript identifiers

  • PascalCase for classes, types, interfaces, enums: Transaction, Money, TransactionRepository, AccountType.
  • camelCase for functions, variables, methods: recordExpense, accountId.
  • SCREAMING_SNAKE_CASE for compile-time constants: MAX_QUANTITY_PRECISION.
  • No prefix for interfaces (no IRepository; just Repository).
  • No suffix for types where avoidable (prefer Money over MoneyType).

7.2.1 Package (path alias) names

Folder Package name
libs/identity/ @oblyk/identity
libs/family/ @oblyk/family
libs/ledger/ @oblyk/ledger
libs/investment/ @oblyk/investment
libs/currency/ @oblyk/currency
libs/import/ @oblyk/import
libs/reporting/ @oblyk/reporting
libs/audit/ @oblyk/audit
libs/notification/ @oblyk/notification
libs/shared/domain-primitives/ @oblyk/domain-primitives
libs/shared/types/ @oblyk/types
libs/shared/event-bus/ @oblyk/event-bus
libs/shared/config/ @oblyk/config
libs/shared/logging/ @oblyk/logging
libs/shared/test-helpers/ @oblyk/test-helpers

7.3 Database identifiers

  • snake_case for tables, columns, enums (per DATA_MODEL.md §1.7).
  • Singular nouns for tables: user, account, transaction.
  • Enum types prefixed by schema and purpose: family_role, ledger_account_type.

7.4 API field names

  • snake_case in JSON (per API_CONVENTIONS.md §4.1).
  • The repo-wide convention: write API DTOs in TypeScript with snake_case field names directly. No automatic camel ↔ snake conversion.

7.5 Event types

  • <context>.<entity>.<action> in lowercase, dot-separated.
  • Examples: ledger.transaction.created, family.member_added, identity.user.deleted.

7.6 Error codes

  • SCREAMING_SNAKE_CASE (per API_CONVENTIONS.md §8.3).
  • Group by prefix where related: VERSION_MISMATCH, VERSION_REQUIRED, VERSION_HEADER_BODY_MISMATCH.

8. Coding conventions

8.1 TypeScript

  • Strict mode on. strict: true, noImplicitAny: true, strictNullChecks: true, noUncheckedIndexedAccess: true.
  • Explicit return types on all exported functions and class methods.
  • Prefer interface for object shapes the application defines (repository interfaces, use case I/O), type for unions and aliases.
  • No any. Use unknown and narrow.
  • No as casts without a comment explaining why and why it's safe.
  • Prefer immutability. Use readonly on properties; return new objects rather than mutating.

8.2 Functions and classes

  • Use cases are classes with a single execute(input): Promise<output> method. Constructor takes injected ports.
  • Domain entities are classes with private constructors and named static factories (Account.create(...), Account.rehydrate(...)).
  • Value objects are classes or readonly records. Equality by value, not identity.
  • Pure functions for domain calculations that don't fit on an entity (avoid creating multi-entity domain services unless truly justified).

8.3 Errors

  • Domain errors are domain exceptions — typed classes in domain/exceptions/. They represent rule violations (InsufficientCashBalance, CreditLimitExceeded).
  • Use cases let domain exceptions propagate. Application-layer catches only when it needs to translate to a different concept.
  • HTTP layer translates domain exceptions to HTTP responses in a centralized error mapper. New domain exception → register a mapping → done.
  • Never throw bare Error() in domain or application code.

8.4 Async

  • async/await everywhere. No raw promises with .then chains in application or domain code (infrastructure may use them when wrapping callback APIs).
  • No floating promises. Lint-enforced.

8.5 Logging

  • Inject the Logger port. Never console.log.
  • Log at info for state transitions, warn for recoverable issues, error for failures.
  • Log structured fields, not interpolated strings. logger.info('transaction created', { transaction_id, family_id, amount_minor }) rather than logger.info(\tx \${id} created`)`.

8.6 Comments

  • Comments explain why, not what. The code shows what; reserve comments for non-obvious rationale.
  • Reference requirement IDs in comments where it clarifies intent: // [R-ACC-002](requirements.md#13-accounts "Cash accounts have a non-negative balance invariant; transactions that would violate it are rejected."){.req-link}: cash invariant.
  • No commented-out code. Delete it; git remembers.

9. Testing conventions

9.1 Test pyramid

Level Where What
Domain unit libs/<context>/domain/__tests__/ Pure tests of entities, value objects, domain services. No I/O.
Application unit libs/<context>/application/__tests__/ Use case tests with in-memory repos, in-memory event bus, fake clock.
Infrastructure integration libs/<context>/infrastructure/__tests__/ Real Postgres + Redis (testcontainers). Repo mappings, event persistence.
HTTP/contract apps/api/__tests__/ End-to-end against a running API instance.

9.2 Test fixtures

  • Live in libs/shared/test-helpers/.
  • Provide entity builders: TransactionBuilder.expense().forAccount(a).withAmount(...).
  • Provide in-memory port impls: InMemoryTransactionRepository, InMemoryEventBus, FakeClock.

9.3 Test naming

Describe behavior, not method: 'rejects an expense that would exceed credit limit', not 'test recordExpense'.


10. Migration conventions

  • Naming: infra/migrations/<utc_timestamp>_<description>.sql — e.g., 20260425_120000_create_ledger_account.sql.
  • Forward-only. No down migrations. Recovery is restore-from-backup.
  • Idempotent where possible. CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS.
  • One logical change per migration. Data migrations are separate and named ..._backfill_....
  • Schema scope. Each migration touches only its own schema.

11. Commit and PR conventions

11.1 Commit format

<type>(<scope>): <short summary>

<optional body explaining why>

Refs: <requirement IDs>

Example: feat(ledger): record expense use case with cash invariant

Types: feat, fix, refactor, docs, test, chore, infra.

11.2 Scope

Bounded context name (identity, family, ledger, investment, currency, import, reporting, audit, notification) or cross-cutting area (api, infra, docs, shared).

11.3 PR description

Include: what it does, requirement IDs, anything not visible in the diff, explicit out-of-scope statement.


12. Pitfalls

  • Logic in route handlers. Route handlers parse, call use case, format response — nothing else. >30 lines or an import from infrastructure/persistence/ is a smell.
  • Domain code that needs a DB to test. Domain uses repository interfaces; tests use in-memory repos. If a domain test mocks SQL, the domain is leaking.
  • Cross-context shortcuts. Never import from another context's infrastructure/ or query its tables directly. Call its public read API.
  • Float arithmetic on money. BigInt minor units only. No Number, no parseFloat.
  • Missing optimistic locking. Every mutation on a versioned entity must include the version check. UPDATE without WHERE version = ? is a bug.
  • User data treated as instructions. User-supplied content (CSV cells, transaction notes) is data. Never execute it.
  • Skipping the audit event. Every state-changing use case must publish a domain event. The audit context subscribes to all of them.
  • Implementing an X-scoped feature. Re-read Vision §2 and Requirements §3 before proceeding.
  • Wrong transaction table. Regular activity (income/expense/transfer) → ledger.transaction. Investment activity → investment.investment_transaction. See Architecture §3.4.
  • Ignoring the spec (AI-specific). Defer to these docs over trained priors on naming, layers, and money handling.
  • Hallucinating API surface. If an endpoint, column, or function isn't in the docs, it doesn't exist yet. Implement it deliberately or ask.
  • Drive-by refactoring. Add the feature asked for. Don't improve surrounding code unless explicitly asked.
  • Guessing on ambiguity. Surface the ambiguity with a short proposal; don't generate speculative implementations.

15. Per-task checklist

Before completing any implementation task, an agent should be able to answer "yes" to all of these:

  • I can name the requirement ID(s) this task implements.
  • I read the docs relevant to this task per the routing table in §3.
  • My code does not violate any hard rule from §4.
  • New files live where §6 says they should.
  • Names follow §7.
  • Tests exist at the appropriate level per §9.
  • Domain layer remains testable without infrastructure.
  • Money is BigInt minor units, IDs are UUIDv7, mutations check version.
  • If a state change happened, the use case emitted a domain event.
  • If new env vars or dependencies were added, they're documented.
  • I did not modify code unrelated to the task.

If any answer is "no," fix it before claiming the task is done.


16. Where to go next