Root-cause fix for 'correctness depended on post-deploy manual audits'.
Separate psql-driven suite (pnpm test:integration, own vitest config,
integration/ outside __tests__ so the hermetic pnpm test gate is
untouched; self-skips without a DB). 18 tests run against real Postgres:
- 045 class_signup_tx/class_cancel_tx: RETURNS TABLE->array shape the
actions destructure, capacity/waitlist/dedupe, and the per-session
advisory lock under TWO concurrent connections (D11 proven, never
over-enrolls).
- 042 equipment exclusion constraint (overlap 23P01; cancelled/adjacent
ok). 044 self-change trigger (role + payment cols blocked; service
bypass). 043 members_update WITH CHECK (cross-space move blocked).
Disproves the pass-52 flagged risk (signup rpc-shape). Non-runtime;
default pnpm test still 532/26 hermetic; build clean.
Deferred deeper-audit P1. signUpForClass (count->decide->insert) and
cancelMySignup (cancel->recount->promote) were non-atomic: concurrent
signups at the capacity boundary over-enrolled and concurrent cancels
double-promoted waitlisters (dynamic capacity can't be a DB constraint).
Migration 045 adds class_signup_tx/class_cancel_tx (SECURITY DEFINER,
pg_advisory_xact_lock per session) doing the decision+write atomically;
actions call the RPCs and keep all pre-checks. Removed the now-unused
effectiveCapacity/computeSignupStatus/pickPromotion imports +
isUniqueViolation (pure rules still live + tested in classes-logic).
528 tests; build clean.
prevent_member_self_role_change covered role/status/space but not the
financial columns; RLS members_update lets a member UPDATE their own
row, so payment_status/dues_paid_until/last_paid_at/last_payment_at/
stripe_customer_id/payment_note/joined_at were self-settable via
PostgREST to forge a dues-good signal. Migration 044 adds them to the
blocked set; service-client (auth.uid() NULL) + privileged-on-another-
member writes still bypass (Stripe webhook unaffected). Schema + maps.
UPDATE policy was USING-only: a privileged caller could mutate a member
row into a space they don't administer (no post-image re-check).
Migration 043 adds the mirrored WITH CHECK. Self column escalation
(space_id/role/status) remains blocked by the trigger (D9). Schema +
DB maps updated.
reserveEquipment was check-then-insert with no DB guard: two concurrent
requests both passed the in-memory overlap check and double-booked a
machine. Migration 042 adds btree_gist + a GiST EXCLUDE constraint
(equipment_id =, tstzrange && ) WHERE status='reserved', making the DB
the arbiter; the action maps the 23P01 violation to the existing
friendly overlap message. App-side check kept as a fast pre-check.
Schema + DB maps updated.
New getMyPayments: payments SELECT is treasurer-scoped (RLS), so the
self-view uses the validated service client strictly scoped to the
caller's space_id + member_id (same convention as getMyBilling /
getMyNotifications). Read-only list (amount, date, platform, status)
in the Membership tab. Docs updated. 521 tests; build clean.
PayPal client_secret (and any secret-named integration field) was stored
plaintext in integrations.config, unlike Stripe/door which use the
AES-256-GCM vault. A DB read or backup leaked it in cleartext.
- lib/secrets/vault.ts: canonical generic storeSecret/readSecret +
isSecretConfigField (the proven Stripe vault logic, generalized).
- saveIntegration: secret-named fields go to the vault, only a *_ref is
kept in config; blank submit preserves the ref (write-only like
Stripe); legacy plaintext auto-migrates into the vault on next save.
- paypal/sync reads client_secret from the vault, transitional fallback
to legacy plaintext until the next save.
- Docs updated (API_REFERENCE, ARCHITECTURE 10.3). 502 tests.
Independent audit verdict was NOT SAFE; all findings fixed:
- dispatcher Idempotency-Key is now per-attempt (${id}:${attempts}),
not row id alone, so a transient-failure response cached by Resend
cannot permanently suppress every retry and silently lose an email.
- enqueueDues is fully try/catch wrapped so a notifications-table or
render failure can never throw into / wedge the Stripe money path.
- dues_lapsed dedupe falls back to the subscription id when periodEnd
is null (canceled/deleted subs), so future lapses no longer collapse
to one key and get suppressed. New test; suite 499.
- documented the dispatcher's once/min concurrency assumption.
Migration 040 (mirrored in schema.sql Section 25 + the CREATE TYPE
list): ALTER TYPE payment_platform ADD VALUE 'stripe' (only safe path;
not used as a literal in the same migration); member_billing (member<->
Stripe customer/subscription/status/period; RLS SELECT = admin/board/
treasurer, NO client write policy — webhook/service-client only);
stripe_webhook_events idempotency ledger (PK = Stripe event id, service-
only, no policy). Per-space OWN keys: secrets->vault, config->
integrations.config. types + DATABASE_SCHEMA + DB_SCHEMA_MAP.
Docs: API_REFERENCE + ARCHITECTURE updated to the owner-chosen
email-match association model (incl. anonymous, tradeoff documented) and
listMemberSubmissions; HANDOFF pass-32; corrected pass-31 header.
Landing: tightened the <=600px breakpoint (nav/section/card padding) and
added a <=400px block so the nav fits ~320px without overflow. CSS-only
inside media queries; desktop unchanged.
Data-only, idempotent (NULL member_id only): links each prior unlinked
form_submission to the earliest-joined matching space_member by
(space_id, lower(email)=lower(submitter_email)). Not mirrored in
schema.sql (fresh DB has no submissions). Documented in
DATABASE_SCHEMA + DB_SCHEMA_MAP (migration range bumped to 039).
Project tree + section 7 module paths now reflect the per-domain
lib/actions/* reality (no monolithic lib/actions.ts); explicit note that
/settings admin secret loading is by-design same-space management;
getAuthMember single-space invariant documented in code. HANDOFF pass-31.
space_visits: one row per visit (check-in/out, is_host, in/out notes);
partial UNIQUE (space_id,member_id) WHERE checked_out_at IS NULL = one
open visit per member. RLS additive/default-deny: SELECT = any space
member (presence is social), NO client write policy (validated
self-resolved service-client actions only; immutable history). No new
permission code; org-wide attendance is all-members by product decision.
spaces.host_requires_card (bool, default true) gates host check-in.
Mirrored in schema.sql, types, DATABASE_SCHEMA, DB_SCHEMA_MAP.