Skip to content
Basenet
🇳🇱 Nederlands
Esc
↑↓navigate↵open⌘Jpreview

Application architecture

Transport adapters, command/query handlers, permission gates, transactions, audit, repositories, providers, and failure boundaries.

The application layer is the shared use-case boundary between delivery mechanisms and infrastructure. A server action, REST route, or MCP tool may authenticate differently, but it must not grow its own version of validation, permission-resource construction, or persistence rules.

Request-to-effect path

Caption: transports resolve authentication and map typed results; handlers own use-case semantics and can() gates; repositories own PostgreSQL writes and command-side audit. emitMutationCommitted() is the stronger mutation + outbox transaction for capabilities that have adopted it. It is not yet the producer path for every existing worker job, so delivery semantics must be described per call site.

Responsibilities by layer

Layer Owns Must not own Source anchor
Transport Session/OAuth scope, request parsing, localized labels, HTTP/status mapping, revalidatePath, transport metadata A second authorization or persistence implementation apps/web/src/lib/actions/tasks.ts, apps/web/app/api/v1/tasks/route.ts, apps/mcp-server/src/mcp.ts
Command handler Normalization required by the use case, can() calls, permission-resource construction, race-to-typed-outcome mapping, injected side effects Next.js, cookies, React, concrete provider construction packages/application/src/commands/shared.ts, packages/application/src/commands/tasks.ts, packages/application/src/commands/matters.ts
Query handler Permission-filtered projections, not_found versus forbidden, list filtering Transport DTO/status mapping packages/application/src/queries/tasks.ts, packages/application/src/queries/list/runner.ts
Repository Drizzle/PostgreSQL operations, row locks, transaction-scoped audit Request/session concerns packages/db/src/repos/tasks.ts, packages/db/src/client.ts
Provider port External capability behind an interface; deterministic substitute in tests Domain authorization or tenant selection packages/integrations/src/contract.ts, packages/integrations/src/factory.ts

Command and query contracts

Commands receive a CommandContext containing a tenant handle, actor, and policy. They return discriminated unions such as created, invalid, forbidden, and not_found; genuine infrastructure faults still throw. Queries reuse the same context rather than introducing a near-identical read context.

The task handlers demonstrate two security-sensitive distinctions:

  • getTaskQuery() refuses an individually unreadable record with forbidden.
  • listTasksQuery() filters unreadable rows and excludes them from total; a list must not leak the existence or count of hidden records.
  • Matter-derived department and access-level scope is constructed once in the application query and reused by task commands.
  • A selected relation or matter ID is input, not authority; handlers reload it through the readable/active boundary before use.

Transaction, audit, and outbox boundaries

withTenantTransaction() pins one connection, rolls back on throw, and joins an existing transaction instead of nesting BEGIN/COMMIT. Repositories such as createTask() mutate and append audit in that same boundary. commitMutationWithOutbox() adds a pending job row atomically and derives an idempotency key from capability, resource, and version. The dispatcher performs queue I/O after commit, with at-least-once delivery and inbox deduplication; it never promises exactly-once execution.

Provider or index calls made directly after a repository commit are a separate failure boundary: a search outage can leave committed source data requiring reconciliation. Do not imply that such an external call rolls the database mutation back.

Invariants

  • Transport authentication selects an actor; only the shared can() decision authorizes the resource action.
  • Permission-resource construction belongs with the use case, not in every transport.
  • A business mutation and its command-side audit either commit together or roll back together.
  • Provider and queue network I/O never runs while the transaction that created an outbox row is open.
  • Lists filter hidden rows; get-by-id endpoints return a structured refusal.
  • Typed business outcomes cross the application boundary; unexpected infrastructure faults remain observable failures.

Failure modes to design for

Failure Expected behavior Evidence
Forged or stale related-record ID Reload through the active/readable boundary and return invalid or not_found packages/application/src/commands/matters.ts
Parent matter deleted between gate and insert Repository lock/check fails; command maps the known race to not_found packages/application/src/commands/tasks.ts, packages/db/src/repos/tasks.ts
Transaction throws after mutation or audit Whole pinned transaction rolls back packages/db/src/client.ts, packages/application/src/events/mutation-event.test.ts
Queue publish fails Pending outbox remains retryable or becomes dead after the configured threshold packages/db/src/repos/outbox.ts
Post-commit index call fails Source-of-truth row remains committed; reconciliation/retry must repair the derived index packages/application/src/commands/tasks.ts, apps/workers/src/jobs/matter-search-reconcile.ts

Focused validation

pnpm --filter @blinqx/application test -- --maxWorkers=2
pnpm --filter @blinqx/application typecheck
pnpm --filter @blinqx/db test -- --maxWorkers=2

Source anchors

  • Application contracts: packages/application/src/commands/shared.ts, packages/application/src/commands/tasks.ts, packages/application/src/queries/tasks.ts
  • Transport adapters: apps/web/src/lib/actions/tasks.ts, apps/web/app/api/v1/tasks/route.ts, apps/mcp-server/src/mcp.ts
  • Transactions and audit: packages/db/src/client.ts, packages/db/src/repos/tasks.ts
  • Transactional events: packages/application/src/events/mutation-event.ts, packages/db/src/repos/outbox.ts

See also: Architecture · Tenant isolation · Workers and integrations

Was this page helpful?