Imported from nerveband/givebar (
AGENTS.md). Install upstream withnpx skills add nerveband/givebar. Copyright stays with the author.
Givebar — Agent & Contributor Contract
Overview & Mission
Givebar is a live fundraising bar chart and stage presentation suite for high-stakes nonprofit galas and benefit appeals ($100k–$2M+).
1. Architectural & Financial Invariants
- Append-Only Event Ledger: Source of truth is an immutable SQLite event stream (
create,amend,void,restore,match_apply,match_release). Total is a deterministic fold (foldLedger). Only rehearsal purge and the admin reset delete ledger rows, and both snapshot the database first. - Staging Delay: A manual gift is excluded from the audience chart until
stage_delay_ms(8 s) after recording. Exclusion is by donation, so corrections inside the window are honoured. Bloomerang and verified source receipts (succeeded Zeffy online payments and authenticated paid custom webhooks) skip the window; EveryAction retains it. Stage and control projections use the same eligibility rules. Bloomerang polls every 5 s until its optional slow-sync time, then every 10 minutes. Zeffy reconciles every minute plus signed webhooks; EveryAction uses incremental export jobs. Failures back off at least 30 s; HTTP 429 delays also apply to manual sync. - Amount bounds:
amount_centsis a safe integer between 1 andMAX_AMOUNT_CENTS($100,000,000) on record and amend (isValidAmountCents); the route rejects anything else with 400. - Major Gift Guardrail: The server rejects gifts at or above
major_gift_threshold_centswith 428 unlessconfirmed_major_gift: true; amended amounts crossing the threshold need it too. - Duplicate Guards: Physical card serials are unique among active gifts (409
CARD_COLLISION). Manual gifts with the same normalised donor name and amount within 10 minutes requireconfirmed_duplicate: true; outbox replay measures this window fromqueued_at. A knowndonation_idis idempotent. Imports use stable provider transaction identity throughsource_receipt, not cross-platform donor matching. Custom webhooks use source-scoped transaction IDs and monotonically increasing revisions. Malformed rows are reported indonation_source.last_error; EveryAction retains individual retry IDs while advancing complete export windows. - No-Backward Chart Rule:
odometer_floor_centsratchets up whenever any projection computes the wall figure; deletes and downward corrections hold the audience total steady. Pause (is_frozen) holds the figure and hides the feed. - Strict Privacy Shield: The chart feed carries display name, amount, and time only. Team notes, operator names, legal names of anonymous donors, card numbers, and pronunciation never leave the operator role. The presenter sees names, pronunciation, and table numbers (named donors only), never notes or operators. Public feeds carry an opaque per-gift key (
publicKey), never the ledger or import id. - Pure Matching Grant Fold: Matching funds are derived deterministically; voids and amendments emit compensating
match_releaseevents. A gift corrected after matching closes keeps the match it earned (scaled down if the gift shrank), never a fresh one. - Recovery floors: purge, re-sync, and restore set
odometer_floor_centsto 0 and letgetStageStateratchet from the staged view, so held and in-window gifts stay off the wall. Sample milestones and quick amounts are seeded into a brand-new database only.
2. Access Model
- Sign-in only: every page and
/api/*route requires a session except/api/controlfor authentication and/api/webhooks/:sourceIdfor independently authenticated deliveries.index.tsenforces this. Custom webhooks require a source bearer secret; Zeffy requires a signed raw body and fresh timestamp. Browser writes retain same-origin checks. Signed-out pages rendersignin.htmland return to the requested page after login./signin,/css,/js, and/assetsremain available without a session. Projection privacy filtering is unchanged. - Financial display contract: Home reads
total_raised_cents,goal_cents, andactive_donation_countfrom the public live ledger projection (role=emcee). Validate required figures before rendering: missing or invalid data is unavailable, never a zero orNaN%. Zero goals have no percentage; progress above the goal remains meaningful. - Revision-checked corrections: the operator form sends
expected_seq(the gift'slatest_seqwhen the dialog opened);amendDonationrejects a stale form withStaleEditError(409STALE_EDIT) so a note-only edit can never undo a colleague's amount or anonymity change. An amend event carries resolved values: an empty pronunciation or table clears it. - Browser outbox: every manual gift is written to
localStorage(givebar_outbox) before its request leaves, withqueued_at; every change is a read-merge-write bydonation_idso tabs never overwrite each other; entries stuck insendingfor 60 s are replayed; each waiting gift can be discarded on its own. - Every JSON write is same-origin (a foreign
Originheader is a 403), bodies are capped (64 KB; 4 MB for Settings saves carrying an image), and pages sendX-Frame-Options: SAMEORIGIN. - Images uploaded in Settings are stored inline in
event_statebut every projection carries/api/asset/<name>?v=<settings_seq>instead of the bytes (withAssetUrls); the Settings form omits an unchanged asset URL on save. - Named operator accounts only (
operator_account, rolesadminandoperator), HttpOnly session cookies (Securewhenever the request arrived over HTTPS, directly or viaX-Forwarded-Proto), single-use email invites. No shared PINs. The client never relies on secure-context-only APIs (crypto.randomUUIDhas agetRandomValuesfallback) so a plain-HTTP laptop on the venue network still works. - Accounts may have a unique, case-insensitive sign-in email. Email and username must not identify different accounts; both use the same PIN and login-attempt budget. Only administrators assign or remove account emails.
- Sessions do not expire on the server. PIN changes, resets, and invite redemption preserve existing sessions. Explicit logout revokes the current session; administrator disabling revokes all account sessions. Persistent cookies are renewed daily during authenticated use, subject to browser retention. Invite PIN-setup permission is single-use, time-limited, and bound to the redeeming session, never granted account-wide.
- Operators: record, edit, delete, and restore gifts; stage messages; pause/resume the chart; team notes; CSV.
- Bulk import (
/import, operators and administrators): a client-side sheet (jspreadsheet CE, vendored underclient/js/vendorandclient/css/vendor, refreshed withbun run vendor) that records each row through the ordinaryPUT /api/donation/:idwith a client-minted id. No bulk endpoint exists: every guard (amount bounds, major gift, possible duplicate, card collision) applies per row, a held row is resolved in the sheet, and a re-run is idempotent. Rows are drafted inlocalStorage(givebar_import_draft) until cleared. - Administrators additionally: Settings, Team and backups (
/team: accounts, sign-in links, invites, snapshots, restore), Testing (rehearsal gifts, purge, chart re-sync), reset, Fundraising import configuration. - Presence identity comes from the session; heartbeats require a session.
3. Design System
- Phosphor icon paths, never emoji in UI buttons, badges, tables, or modals.
- No native dialogs: every confirmation, prompt, and notice goes through
GivebarSession.confirm,prompt, andtoastinclient/js/session.js.window.confirm/prompt/alertare prohibited. - OKLCH token architecture with a brand layer (
--brand-hue,--brand-chroma,--brand-accent,--brand-radius). - ≥ 44 px touch targets on operator surfaces; 56 px in the donation dialog.
- Tabular numerals on the odometer and every financial figure.
4. Zero-Code In-App Settings Rules
- Non-developers configure event settings and Sources in Settings (
/settings); accounts and backups live in Team and backups (/team). Sources are Bloomerang Fundraising, Zeffy, Bonterra EveryAction, and Custom webhook. Saving a source leaves it disabled; existing migrated Bloomerang state is preserved. Disabling never removes gifts. - Event and source configuration persists in SQLite (
event_state,milestone,ask_tier,donation_source). Never introduce external event-settings JSON. Only credentials live in private mode-600 files underGIVEBAR_SOURCE_SECRETS_DIR, outside the repository. Custom webhook secrets are stored as hashes and shown once. Native Zeffy/EveryAction names default to anonymous until a documented public-consent mapping exists.
5. Deployment & Infrastructure
- Production: the manually run
givebarcontainer on the wavedepth host (root@172.245.248.17), bind-mounted/etc/dokploy/applications/givebar/dataat/app/data, reached through Traefik atgivebar.wavedepth.com. Deploy withscripts/deploy-wavedepth.sh. - Schema: fresh databases use version 18. Upgrades from 15, 16, and 17 preserve ledger IDs, accounts, sessions, and the existing Bloomerang connection and receipts. Versions 15 and 16 also retain their session/schedule upgrades. Older databases require a compatible backup or fresh database. Restore and reset disable sources and invalidate in-flight imports; restoring an event does not restore rotated webhook secrets or provider credential files.
- Backups:
VACUUM INTOsnapshots indata/backupsevery 5 minutes when anything changed, plus pre-purge, pre-reset, pre-restore, and pre-deploy snapshots. Never copygivebar.sqliteby hand while the server runs. - Verification: after changes run
bun testonce; judge observable ledger, privacy, staging, and role behaviour. Documentation-only work needs no test run. - Approval boundary: source edits do not authorise deployment or changes to live financial data.