Accounting Engine
Document ingestion and accounting are decoupled through a Postgres queue. Nothing in the import path calls accounting directly; it enqueues, and a long-running workflow drains the queue.The two phases
The decoupling buys independent scaling, resilient retries, and idempotency: a failed accounting run does not roll back the import.The queue
accounting_work_queue carries version_hint, state, run_count, last_error,
priority, and document_type alongside the document and org.
syncAccountingQueueWorkflow (workflows/accounting/sync-accounting-queue-workflow.ts:48)
drains it:
- Leasing —
fetchJobsusesFOR UPDATE SKIP LOCKEDand setsstate = 'leased'(:66), so parallel lanes never collide. - Pipelining — the next batch is prefetched while the current one processes (
:65). - Bounded concurrency — 6 documents per batch activity; total in-flight is
laneCount × 6. The comment at:44-45says to raise this only after observing direct-pool utilisation. - Heartbeating — the batch activity heartbeats after every concurrency wave, under a
30-minute
startToCloseTimeout(:37-39). continueAsNewonly when Temporal suggests it (:106-107), and the prefetched leased batch is drained first so those rows are not stranded (:108-110).- Stale-lease recovery — if draining fails, leases recover via the stale-lease clause.
- Cancellation propagates deliberately (
:82); swallowing it would let a lane run on.
recreateAccountingImpactViaQueueWorkflow, runs on its own queue
(recreate-accounting-impact-batch) for bulk re-derivation.
Inside createAccountingImpact
packages/core/src/services/create-accounting-impact.ts:121 — the single function all
accounting converges on. It runs inside a transaction and touches five domains.
Two things worth noting from the order: idempotency is checked first, so a redelivered
job is cheap; and an actor is resolved before any write, satisfying the attribution
rule.
The posting matrix
Rules are looked up by dimension label and trigger event slug, then cached.getPostingMatrixRulesCached is called once per dimension per document — debit, credit,
and each custom dimension (create-accounting-impact.ts:602-619). This is a hot read path
and is Redis-cached; see Redis Caching.
Related workflows
Deferred revenue
deferred-revenue (17 files) produces deferred_revenue_schedules, and
revenue_recognition_entries links each recognised amount to a general_ledger row.
revenue_recognition_entries has no org_id — it is tenant-scoped only through
schedule_id and general_ledger_id. Any query against it must join a parent to stay
tenant-safe. See Data Model.
Notes
The queue workflow is self-healing.worker.ts:62 calls
ensureAccountingQueueRunning at startup, using a no-op ping signal (:23, :55) to
detect whether the long-lived workflow is already active.
Connection pooling is the real constraint. The accounting process uses a lazy
prismaDirect pool capped at ACCOUNTING_QUEUE_LANES * 6 + 4 per Machine — 28 at four
lanes. WORKER_GROUPS.md normalises Supabase pooler URLs to transaction mode on port
6543 for this client to avoid Supavisor’s session-client cap.
CONA-986/987 governs batch sizing. The in-code comment ties the 6-per-batch limit to
those tickets — check them before tuning.
DATEV is an export format, not an integration. datevExportWorkflow generates files;
no CONA code calls a DATEV API.