The repository at runtime

Chitragupt is a migration engine and a runtime ORM. The generated repository adapter isn't a thin SQL wrapper — it carries the behaviors that make persistence correct: the transaction boundary, tenant isolation, the outbox write, concurrency control, and the translation of database errors into typed application errors. You author entities and operations; the adapter does all of this without a line of persistence code from you.

The Unit of Work — one transaction per operation

Every command or use-case step runs inside a Unit of Work — a single database transaction the framework opens before the handler runs and commits when it succeeds (rolling back on any error). The generated adapter routes all of its reads and writes onto that transaction, so a multi-step operation is atomic by construction. The UoW spans two connection pools — the least-privileged app pool for request-path work, and the elevated system pool for the rare run_as_sudo write — but a normal operation lives entirely on the app pool, RLS-enforced.

Tenant isolation is injected per request

This is what makes RLS actually work at runtime. When the UoW opens its transaction, it sets the caller's identity as transaction-local session variables — app.tenant_id from the authenticated caller, plus one app.caller_<axis> per caller axis — before any query runs:

SET LOCAL app.tenant_id = '';   -- from the authenticated caller

Every RLS policy reads current_setting('app.tenant_id'), so the moment the transaction is scoped, every read and write in it is filtered to the caller's tenant automatically. Because it's SET LOCAL, it resets at commit — no leakage between requests. A guard expression that references caller.role lowers to current_setting('app.caller_role'), so authorization expressions read the same injected identity.

The transactional outbox write

When a command emits a domain event, the adapter writes that event into the outbox table on the same transaction as the aggregate write — so the state change and the announcement commit or roll back together. This is the mechanism behind the transactional outbox: there is no window where an order is placed but OrderPlaced was lost, because both rows land in one commit. The outbox write refuses to run outside a UoW — the atomicity guarantee is enforced, not hoped for.

Concurrency, soft-delete, and audit — opt-in, handled for you

Several per-entity behaviors are the adapter's job, turned on by declaring them on the entity:

  • Optimistic locking — when an entity carries a version field, Update bumps the version in SET and adds AND version = $expected to the WHERE. If a concurrent write already moved the version, zero rows match and the update fails with a typed conflict rather than silently clobbering — lost-update protection with no locking code.
  • Soft-delete — when an entity opts into soft-delete, reads transparently add AND deleted_at IS NULL and Delete becomes UPDATE … SET deleted_at = now(). The row is retained but invisible, and the framework even generates the partial unique index (UNIQUE (…) WHERE deleted_at IS NULL) so a "deleted" slug can be reused.
  • Audit timestamps — Chowk's aggregates declare audit timestamps, and the adapter treats created_at/updated_at as database-managed: they're server-stamped on write and returned into the struct, never bound from client input. The timestamps are authoritative because the database sets them.

Invariants enforced inside the write

A persistence invariant is a database CHECK, but the adapter also enforces invariants at write time: after the INSERT/UPDATE succeeds but before the method returns — still inside the transaction — it evaluates the entity's invariants, so a violation rolls the whole operation back. Chowk's StockCoversReservations can never be left violated: the CHECK refuses it at the database, and the framework surfaces the failure as the declared error.

Database errors become typed application errors

The last thing the adapter does is translate. A raw PostgreSQL error is a SQLSTATE code; the generated code maps each to the framework's typed errors so a caller — and a generated client — gets a meaningful, stable failure instead of a 500:

PostgreSQL conditionSQLSTATEBecomes
Unique violation23505AlreadyExists (409)
Check violation (invariant/spec)23514a validation error (422)
Foreign-key violation23503an invalid-reference error (422)
No row for a required readnot-found (404)

So when Chowk's StockCoversReservations CHECK rejects an over-reservation, the caller doesn't see a database stack trace — they see the typed InsufficientStock 409 the model declared. The persistence layer speaks the domain's error language, because the adapter maps it there.

The whole picture

A single command handler, wrapped by the generated adapter, runs: open the UoW transaction → inject the caller's tenant/identity (RLS now active) → apply the guarded write → enforce invariants → write any emitted event to the outbox → commit — or, on any failure, roll all of it back and return a typed error. Every one of those steps is generated from your model. That is what it means for Chitragupt to be the ORM: the correctness of persistence is a property of the declaration, not of hand-written data-access code.