Appearance
The big shift — AWS/NoSQL → Supabase/Postgres
Read this one
Most of Tonder's finances complexity exists to work around DynamoDB's lack of multi-item ACID transactions and MongoDB's role as a query mirror. Postgres removes the need for almost all of that machinery. Do not port the workarounds — port the intent.
What Tonder does, and why
Tonder's engine is event-driven on AWS:
- DynamoDB holds accounts, journals, ledger entries, fee rules, config, snapshots, idempotency.
- MongoDB mirrors journals / fee-rules / config for rich queries and aggregation pipelines.
- EventBridge → SQS FIFO (
TransactionEventsQueue,MessageGroupId = businessId) delivers transaction events in per-business order. AccountUpdateQueue(FIFO,MessageGroupId = account.id) serializes balance updates because DynamoDB can only do atomic single-itemADD, and running balances must be applied in order. Aversioncounter provides optimistic concurrency.maxReceiveCount = 160because losing a balance update permanently corrupts the ledger.- An
IdempotencyTable(120s TTL) + aprocess_id-indexdedup guard + a 2-day cutoff prevent double-processing of redelivered SQS messages. - CloudWatch crons drive rolling-reserve release (02:00 UTC) and daily ledger (10:00 UTC).
- Step Functions drive bulk reprocess.
What Vecnet does instead
| Tonder primitive | Reason it exists | Vecnet (Supabase) replacement |
|---|---|---|
DynamoDB single-table (PK/SK) | NoSQL key access | Postgres relational tables — proper PKs, FKs, indexes |
| MongoDB query mirror + pipelines | Rich querying DynamoDB can't do | Gone. Postgres is the query engine; aggregations are SQL |
AccountUpdateQueue FIFO + ADD + version | Atomic multi-row update | A single Postgres transaction — insert journals + entries, UPDATE accounts SET balance = balance + :delta, all-or-nothing, row-locked |
IdempotencyTable + process_id-index + 120s TTL | Guard SQS redelivery | A UNIQUE (process_id, category) constraint + INSERT … ON CONFLICT DO NOTHING. Idempotency is a schema property |
TransactionEventsQueue (EventBridge→SQS FIFO) | Ordered async ingestion | pgmq or a transaction_events table consumed with SELECT … FOR UPDATE SKIP LOCKED; per-entity ordering via pg_advisory_xact_lock(hashtext(entity_id)) |
| CloudWatch crons | Scheduled jobs | pg_cron invoking SQL / Edge Functions |
| Step Functions (bulk reprocess) | Long orchestration | Worker off a bulk_jobs table + pgmq; or an Edge Function loop |
| S3 (XLSX reports) | File storage | Supabase Storage bucket |
| Lambda + Middy + InversifyJS + RxJS | AWS runtime + wiring | Supabase Edge Functions (Deno/TS) + Postgres functions; keep hexagonal layering, drop RxJS — plain async/await |
| Balance via FIFO serialization | Avoid lost updates | Row-level locking (SELECT … FOR UPDATE); balance_before/balance_after/seq from the locked row |
The core simplification, stated plainly
The Vecnet path is one transaction:
sql
BEGIN;
-- idempotent: skip if already processed
INSERT INTO journals (...) VALUES (...) ON CONFLICT (process_id, category) DO NOTHING;
-- if no row inserted, the event was already processed → COMMIT and return.
INSERT INTO ledger_entries (...) VALUES (...); -- the DEBIT/CREDIT pair(s)
-- apply balances atomically, locking each account row
UPDATE accounts SET balance = balance + :delta, version = version + 1, modified_at = now()
WHERE id = :account_id; -- repeated per affected account
-- (balance_before / balance_after / seq derived from the locked rows)
COMMIT;Stricter than Tonder
Double-entry balance (sum(debits) = sum(credits)) is enforced, not merely warned about, via a check in the posting function. Tonder only logs a warning — Vecnet should do better, because Postgres lets us. See the posting function.
Idempotency, in turn, collapses to three constraints — there is no idempotency table at all.