Envelope ledger — how the money side works
Event hosts who want to see whether hosting is making or losing money, and anyone auditing the books
Last updated 25 May 2026
What this is
Running events costs real money. Wines come out of your cellar, you pay for food up front, convives pay you back over weeks — sometimes in advance, sometimes after. A single "bank balance" number hides all of that. The envelope ledger splits it into four numbers that each tell a different part of the story, plus an append-only transaction log that explains how each number got to where it is.
This guide is for hosts (today, just me — but the feature is built to scale). If you just want to see whether last month's events were profitable, skip to The four numbers. If you're about to record a transaction and want to know you're doing it right, skip to Transaction kinds.
The four numbers
Every host has four numbers that together describe their position in the hobby at any point in time.
- Cash. Money you're actually holding — in your bank, in your pocket — that came from envelope activity. Grows when a convive hands you money (top-up or post-event payment). Shrinks when you pay for wine, food, or shared expenses up front.
- Cellar. The total cost basis of bottles currently sitting in your cellar. Each bottle has its own per-bottle price recorded at purchase time, so a wine bought on discount in November and again at full price in March contributes two different numbers. Drawn live from your bottle inventory; drops when a bottle is consumed at an event or drunk at home.
- Receivables. What convives collectively owe you (or you owe them). This is a net number with two sides broken out:
- Owed to host — sum of convives whose balance is negative (they owe you).
- Owed by host — sum of convives whose balance is positive (you're holding credit for them — money they topped up but haven't consumed yet).
- Capital. Lifetime sunk cost in the hobby — how much of your own money has been put in. Grows with explicit capital injections and gifts received; doesn't move when events happen.
On top of these, two derived numbers:
- Net equity = cash + cellar + owed-to-host − owed-by-host. "What's my position worth right now?"
- Lifetime P&L = net equity − capital. "Has this been worth it?" Positive means hosting has paid back more than you've put in; negative is the normal state for a wine enthusiast.
Why cash and receivables aren't the same as profit
The thing to understand, once, before any of this makes sense: events move equity, not cash.
When a convive who's topped up attends a 3000 UAH event:
- They paid you three months ago. Cash moved then, not tonight.
- Tonight's settlement reduces their credit by 3000. Receivables.owed-by-host drops 3000.
- The wine drunk — say 1000 UAH cost basis — leaves your cellar.
- Equity moved by +3000 (liability reduced) − 1000 (cellar drained) = +2000 profit.
- Cash didn't move at all.
So a thriving events cash pile isn't what tells you hosting is profitable — lifetime P&L does. The gap between cash and receivables.net grows with every profitable event and shrinks with every unprofitable one.
At the moment the ledger was set up (2026-04-24), by design, cash == receivables.net exactly. We defined opening cash as "the amount convives have collectively prepaid" and opening capital as "the cellar value at reset." From that starting point, every event either widens or narrows that gap, and the cumulative widening is your profit.
The transaction log
Every cash or capital movement is a row in host_transactions. Rows are append-only: a database trigger blocks UPDATE on the financial fields (amount, kind, entity links, occurred_at) after insert. If a row is wrong, you don't edit it — you post a compensating row with kind = adjustment that points at the original via corrects_transaction_id. Both rows stay visible forever; the correction is linked, not hidden.
Why this matters: when you're looking at a balance that seems wrong six months from now, you want to see both the wrong entry and the fix. Traditional "edit the bad row" workflows throw away that audit trail. Envelope keeps it.
Descriptions and metadata (non-financial fields) remain editable — typos in a description shouldn't require ceremony.
Transaction kinds
Use the right kind. The projection library routes each kind to the right number (or numbers) automatically.
| Kind | When | Moves cash? | Moves capital? |
|---|---|---|---|
top_up | Convive hands you money to pre-fund future events | +cash | — |
event_payment_in | Convive pays for a specific event (usually after settlement) | +cash | — |
wine_purchase | You buy a bottle for the cellar | −cash | — |
event_expense_paid | You paid food/water/service for an event up front | −cash | — |
capital_inject | You put personal money into the hobby pot (top-up of your own side) | +cash | +capital |
capital_withdraw | You take personal money back out (rare) | −cash | −capital |
external_sale | You sold a bottle outside the platform | +cash | — |
gift_in | You received a bottle as a gift | — | +capital |
gift_out | You gave a bottle away | — | −capital |
wine_consumed_personal | You drank a bottle at home | — | — |
opening_balance | One-time seed row at ledger setup; metadata.axis routes to cash or capital | depends | depends |
adjustment | Compensating correction to an earlier row; metadata.axis tells the projection which number it corrects | depends | depends |
Two kinds — top_up and event_payment_in — are paired writes. They always create two rows atomically: one in convive_ledger (so the convive sees their balance update) and one in host_transactions (so your cash updates). A Postgres function handles the atomicity — if either row fails, neither lands.
The dashboard, and its siblings
/profile/host/envelope is the main view. Four stat cards up top (cash, cellar, receivables, capital); below them the bigger numbers — net equity and lifetime P&L — with the invariant status indicator; then receivables broken out into owed-to-you and owed-by-you; then an append-only activity log. Corrections appear inline with a "↪ corrects #N" link, so a wrong row and its fix always show together.
The invariant indicator reads "holds exactly" right after setup. Once you settle an event, it starts showing a delta. That delta is good — it's the accumulated event margin showing up exactly where it should.
Two siblings share the same underlying data from different angles:
/profile/host/finance— long-running analytics: monthly P&L (event charges plus outside sales), revenue per event, cumulative cash flow, project expenses, cellar value over time. Wine capital splits recovery into event-cost and outside-sales channels so net wine investment lines up with cellar value. Use it to answer "how has hosting been going over the last year."/profile/host/receivables— per-convive balance detail, the place to edit an individual row. Use it when a convive has a question about their balance or you need to correct one specific entry.
Envelope is the now view (with the audit trail). Finance is the trends view. Ledger is the rows view.
Planning vs execution
Adding wines, shared expenses, or personal orders to an event is planning, not spending. The event manager is a sandbox — you draft a menu, rough-cost a ticket price, remove a placeholder expense, add the real one. No cash moves until the event settles. That's the whole point of settlement: it's the moment you commit to the event's financial reality.
The only cellar-touching action that moves money mid-planning is buying a bottle — because that bottle is real, paid for, sitting somewhere. That's execution, not planning.
Common flows
You buy wine for the cellar. Add the bottle via /profile/cellar. Cash drops by what you paid (converted to UAH at the purchase-date rate if the bottle is in another currency); cellar grows by the bottle's cost basis. Equity unchanged — asset reshuffled. Automatic — no separate envelope entry needed.
You update a bottle's price. If you change the price on an existing bottle, a compensating adjustment lands automatically, pointing at the original wine_purchase row. If the envelope write fails, the bottle's price is reverted so the two sides stay consistent.
You remove a bottle. A reversal adjustment is posted; cash returns to pre-purchase level.
Pre-settlement: planning the event. Add/edit/remove shared expenses and personal orders as much as you want. Nothing hits the envelope. Use placeholders to plan ticket price, swap them for real expenses when the event actually happens.
Event settles. confirmSettlement does two things atomically:
- Creates
convive_ledgercharge rows for every attendee (with pays_for redistribution applied) - Emits
event_expense_paidhost_transactions rows for every shared expense and personal order on the event (tagged with the settlement_batch)
Cash drops by the total event cost. Receivables move as charges land. Net equity shifts by the event margin.
Event unsettles. undoSettlement emits compensating adjustment rows (pointing at each of the settlement's expense rows), then deletes the convive_ledger charges. Both original + reversal rows stay in the host_transactions log; the audit trail is intact. Blocked if any event_payment_in rows exist — real cash received can't be walked back by deleting charges.
A convive tops up. Record via /profile/host/receivables → Record payment (envelope-routed automatically) or /profile/host/envelope → Record → Top-up. Either way the paired convive_ledger + host_transactions rows land atomically.
A convive pays after the event. Same routing. Via /profile/host/receivables if the payment is event-linked via the entry metadata; otherwise via /profile/host/envelope → Record → Event payment.
You notice a wrong amount. Don't edit the old row — it's immutable. Post recordAdjustment with a signed delta. Both rows stay; the fix is linked via corrects_transaction_id.
Observability
The integrity script verifies the whole thing:
bun run scripts/verify-ledger-integrity.ts
Checks that paired rows actually match, adjustments point at real rows, opening invariant still holds, projections compute consistently, no NULL host_id rows. Green output means the books are internally consistent.
For a quick "what do the four numbers say right now":
bun run scripts/verify-host-envelope.ts
Both are read-only and safe to run anytime.
When the numbers don't match
If the integrity script flags drift, read the output carefully — it names the specific row or condition. The usual causes:
- Paired row missing — someone wrote to
convive_ledgerwithout the matchinghost_transactionsrow. Fix: figure out what real-world event happened, emit the missing side as anadjustment. - Adjustment without axis — a row was inserted directly (not via a builder) without
metadata.axis. The projection silently ignores it. Fix: emit a second adjustment with the right axis that nets out the first. - Cash ≡ receivables invariant broken by more than event margins — something else moved either cash or receivables without moving the other. Most commonly, an admin-manual
convive_ledgerentry that should have been a real payment. Fix: reconcile withadjustmentrows on the cash side.
The goal isn't for every number to look clean forever — real money gets messy. The goal is that every mess is traceable to specific rows, and every fix is another row on top.