Architecture
One core library is the entire contract. Everything else — the MCP server, the CLI, the future console — is a thin shell that cannot drift from it.
How Ledgenter is put together, at a high level.
There's one set of rules at the center, and everything else is built on top of it: the interface agents use, the command-line tool, and the human web view all speak to that same core. So there's a single definition of how everything works — the pieces can't drift apart or quietly contradict each other.
Two ideas keep your data safe and consistent. Every change is checked and recorded in one all-or-nothing step, so you never end up with a half-finished update. And the same request sent twice is recognized and folded together, so a retry after a network hiccup never creates duplicates.
The short version: one rulebook, applied everywhere, with safety built into the foundation rather than bolted on afterward.
The shape of the system
Agent (any MCP host) ──MCP stdio──► @ledgenter/mcp ──┐ Cron / scripts ──shell──────► @ledgenter/cli ──┼──► @ledgenter/core ──► Supabase Humans (browser) ──console (planned) ─────────┘ │ Postgres · RLS │ pgvector · cron validate → execute → normalize edge functions
The monorepo has three published packages and one backend:
@ledgenter/core— the single source of truth. It owns the validation schemas, the domain APIs, the auth exchange, run-context resolution, and the error envelope. Both other packages are generated from it, never alongside it.@ledgenter/mcp— a stdio MCP server that maps 54 flat, snake_case tools 1:1 onto core methods. Every call flows through one dispatch chokepoint that produces the result envelope and the telemetry record — even unknown-tool and validation failures are observed.@ledgenter/cli— the same operations for cron and scripts:--jsonoutput, stable sysexits-style exit codes, and a retryable-gated--soft-failfor unattended ticks.- Supabase — Postgres with forced row-level security, pgvector for semantic search, pg_cron for maintenance (reapers and sweepers), and edge functions for the auth exchange and embedding pipeline.
One contract, derived everywhere
Every input shape is defined once as a zod schema in core. The MCP server derives its JSON-Schema tool definitions from those schemas; the CLI derives its flags from them. There is no second copy to drift. The wire contract is snake_case and strict — an unknown key fails loudly instead of being silently dropped, a failure mode this system was specifically hardened against.
Every domain function follows one shape: validate → execute → normalize. Nothing throws across the domain boundary. The result is always an envelope:
{ "ok": true, ... } // success: the data, flat
{ "ok": false, "error": {
"code": "VALIDATION", // stable taxonomy
"message": "task is blocked",
"hint": "2 open blocking dependencies must be done first…",
"retryable": false // safe-to-retry signal, used by --soft-fail
} }
The hint is a first-class design surface: it is written for an agent to read
and recover from. A status-machine violation names the allowed transitions; a dependency
cycle returns the offending edges; a verification failure names the first unmet criterion.
Reads under RLS, writes through RPCs
This is the system's non-negotiable data rule, and the reason it stays consistent under concurrent agents:
| Path | Why | |
|---|---|---|
| Reads | PostgREST select directly on tables and views |
Row-level security scopes every row to the caller's tenant in the database itself. Filters, keyset pagination, and derived state (like task readiness) are computed server-side, so page one is never a lie. |
| Writes | Postgres functions (RPCs), SECURITY DEFINER |
Validate → idempotency check → write → record activity → emit notifications, all in one transaction. A write either fully happens with its paper trail, or doesn't happen at all. Direct table DML is revoked from the agent role. |
Concurrency safety lives in the database, where it can't be bypassed: claim-next uses
FOR UPDATE SKIP LOCKED so contending agents skip rather than block; same-key
registration races converge via ON CONFLICT; cycle checks take advisory locks;
optimistic preconditions (expected_status, expected_version) turn
stale writes into hinted errors instead of silent clobbers.
Auth: a key exchange, not a database login
Agent machines never hold a database role or a service key. Each actor has one Ledgenter API key; core exchanges it at an edge function for a short-lived (~15 minute) JWT carrying the tenant and actor identity, cached and refreshed just before expiry. Everything downstream — RLS policies, RPC attribution, activity records — derives identity from those JWT claims, never from client-supplied parameters.
LEDGENTER_API_KEY ──POST──► auth-exchange (edge fn) ──► JWT {tenant_id, actor_id, scopes} // 15 min TTL, refreshed ~2 min early // a revoked key fails the next exchange — loudly, even under --soft-fail
Scopes are coarse by design in v1: write gates every mutation
(require_scope inside each RPC, proven by tests that a read-only key can select
but never write); the tenant boundary does the heavy lifting
(chapter 5).
Idempotency: retries are free
Every write RPC begins by registering an idempotency key. Replay the same key and you get
the original result back, flagged idempotent_replay: true — no
duplicate row, no double side effects. Core derives a key from the call's content when the
caller doesn't supply one, folding in the current actor and
run so that two agents (or two parallel runs) writing identical content
never collapse onto one row — a phantom-success class of bug this system gates against
adversarially.
Ambient context: the run and the repo
Core resolves a per-process run context once at startup: a run key (minted or inherited from the environment), the parent run, the recurring-series key, and the git context of the working directory (remote, branch, HEAD — credentials stripped, and never the absolute path). Every request carries the run key in a header; the run is lazily registered before the first write, so even a crashed one-call session leaves an attributable episode. Chapter 6 covers the full run model.
Designed degradation
Optional subsystems fail soft, never hard:
- Embeddings are a typed seam. Unconfigured or failing, semantic search degrades to lexical matching; knowledge writes return immediately and embed in the background. An embedding outage can never block a write.
- Telemetry is observation, not control flow — if it can't record, the tool call still succeeds.
- Run registration is best-effort with retry; a registration failure never blocks the agent's actual work.
What CI refuses to merge
The architecture is enforced, not aspirational. Every push runs:
- Schema-drift gate — generated types from the live schema must match the committed ones; a migration without regenerated types fails the build.
- RLS lint — every public table must carry forced RLS and a tenant policy (27 tables today; service-only tables are explicitly allowlisted).
- Isolation suite — 145 pgTAP assertions prove cross-tenant invisibility, raw-DML denial, and fail-closed behavior on empty claims.
- Adversarial gates — 7 integrity invariants and 13 concurrency probes attack the live backend daily: registration races, idempotency collapses, cursor independence, credential and path leak attempts (chapter 9).
Where to read deeper. Each subsystem has its own chapter — the entities, the security model, runs & loops, and verification & integrity — and the tool reference documents every input.