Files
Moheeb Zara 8edc9da611 docs: audit corrections, coverage pages, screenshots, and em-dash removal
Finalizes the docs section after an adversarial audit (code review + per-page
fact-check + coherence critic) and two content requests.

- Accuracy: fixed 10 invented cross-link slugs, the admin-vs-board gating claims
  the verifiers caught (permissions matrix, /settings, invites, onboarding,
  connect-payments, proposal withdraw, Stripe reconciliation, member fields,
  door.operate), and 2 issues in the new pages (project area-at-creation, forum
  thread-delete role).
- Coverage: added 5 how-to pages so every module is documented (recurring Stripe
  dues, tasks & chores, projects, comms & forum, ops knowledge base + secrets
  vault). 34 pages total.
- Screenshots: 12 captioned app screenshots under public/docs-media/, embedded
  as figures. Each tutorial now has 2-3 step screenshots.
- Style: removed all em-dashes/en-dashes/ellipses from the docs (natural ASCII
  punctuation).
- Handoff (docs/HANDOFF-audit-cleanup.md) updated with the current state.

typecheck 0 · lint 0 errors · build ok (all 34 /docs pages prerendered)
2026-08-05 03:15:07 -07:00

6.5 KiB

The platform reaches the outside world through three server endpoints: a notification dispatcher cron that drains the email outbox, two inbound door transports (a poll and a webhook), and the Resend email transport behind them. All three are session-less and authenticated by a shared secret or a per-connection bearer secret, never by a logged-in user. This page documents their contracts, environment variables, and behavior.

Environment variables

These control delivery and cron authentication. The ones that govern integrations are:

Variable Purpose Unset behavior
RESEND_API_KEY Resend HTTP API key for transactional email. Outbox still fills; each row is recorded failed with "transport not configured" (non-retryable).
EMAIL_FROM From address for all platform email, e.g. HeatSync Labs <[email protected]>. Domain must be verified in Resend (SPF + DKIM). Same as unset RESEND_API_KEY: sends no-op as failed.
CRON_SECRET Shared secret guarding both cron endpoints. Generate with openssl rand -hex 32. Both cron routes return 503 and never run.
SENTRY_DSN Optional Sentry-compatible error backend; both cron dispatchers and the webhook report to it. Capture seam no-ops.

The two cron endpoints reuse the same CRON_SECRET. The door webhook does not use it, it authenticates on a per-connection secret from the vault.

Notification dispatcher, POST /api/cron/notifications

Transactional email is an outbox (the notifications table). The Stripe webhook and other server actions only enqueue rows; nothing sends inline. This endpoint drains the outbox and is meant to be hit once a minute by the droplet crontab.

  • Auth: Authorization: Bearer <CRON_SECRET>, compared in constant time. Returns 503 if CRON_SECRET is unset, 401 on mismatch.
  • Selects up to 200 oldest pending, channel = 'email' rows under the attempt cap, then round-robin fair-drains up to 20 across spaces so one tenant's burst cannot head-of-line-block others.
  • Sends are spaced ~220 ms apart (~4.5/sec) to stay under Resend's 5 req/sec limit.
  • Idempotent and safe to overlap: there is no row lock, but each send carries a per-attempt Idempotency-Key of ${id}:${attempts}, so concurrent runs of the same attempt dedupe at Resend.
  • Response body: { scanned, sent, failed, retried, skipped }.

Outbox row lifecycle

Status Meaning
pending Awaiting send or awaiting retry.
sent Delivered to Resend; sent_at set.
failed Terminal: permanent (config/validation) error, or attempt budget exhausted.
skipped Terminal: the recipient muted this category (never sent, no Resend call).

A row retries on transient failures (429, 5xx, network) up to MAX_NOTIFICATION_ATTEMPTS (5) before going to failed. Every status write is guarded on status = 'pending', so a losing overlapping run is a no-op.

Notification preferences and muting

Members opt out of muteable categories from their profile at /me; the toggles write per-member rows the dispatcher reads. Billing notices are membership-critical and can never be muted.

Category Muteable Example types
billing No dues_renewed, dues_payment_failed, dues_lapsed
bookings Yes booking_confirmed, booking_cancelled
classes Yes class_signup_registered, class_signup_waitlisted, class_signup_promoted, class_session_cancelled
forms Yes form_submission_received
admin_alerts Yes form_submission_admin

The model is opt-out: an absent preference row means enabled. A type with no category mapping is treated as always-on (fail-open). When the dispatcher marks an email skipped, the notification still appears unread in the member's in-app inbox on /me, the read_at marker is independent of email delivery.

Door inbound, poll vs. webhook

A space that runs a door controller can pull real entry/denied events into the access log through one of two transports, both configured per connection on /door/manage (requires the door.manage permission, see Access control and Connect a door). Both share one ingest core; ingested rows dedupe on (connection_id, dedupe_key).

Poll, POST /api/cron/door-ingest

For native-HeatSync controllers only. A once-a-minute crontab call reads each enabled connection's ?z log through the hardened outbound executor and matches each card to a member.

  • Auth: same CRON_SECRET bearer as the notification cron. 503 if unset, 401 on mismatch.
  • Polls only connections with is_enabled, inbound_enabled, and adapter = 'native_heatsync', up to 50 per run, concurrently.
  • Idempotent: overlapping or missed minutes are harmless.

Webhook, POST /api/door/inbound/[connection]

For generic (non-HeatSync) controllers or a LAN relay that pushes normalized events. This is the reliable transport: each event carries a caller-supplied stable id, so retries are idempotent.

  • [connection] is the connection's UUID; a malformed id returns a generic 404.
  • Auth: Authorization: Bearer <inbound secret>, the per-connection secret from the AES-256-GCM vault (inbound_secret_ref), distinct from the outbound door password. A missing connection, disabled inbound, or bad secret all return the same 401. The webhook URL is shown on /door/manage once inbound is enabled.
  • Rate limited to 120 requests/minute per connection (429 over that). Bodies over 64 KB are rejected 413.

Request body:

{
  "events": [
    {
      "id": "stable-caller-id",
      "card_uid": "optional-string",
      "card_number": "optional-digits",
      "result": "granted",
      "occurred_at": "2026-08-05T14:03:00Z"
    }
  ]
}

events holds 1-100 items. result is one of granted, denied, unknown. Provide card_uid and/or card_number (digits only) to resolve the event to a member. Valid requests return { "received": true, ... } with per-batch insert counts; malformed JSON or a schema failure returns 400.

Email transport (Resend)

lib/email/send.ts is the single transport seam. It talks to Resend's HTTP API (https://api.resend.com/emails) with fetch, no SDK, so a self-hosted deploy can swap it for SMTP without touching any caller. It classifies each failure as retryable (429, 5xx, network) or terminal (unset config, invalid recipient, other 4xx), which is what drives the outbox retry-vs-failed decision above.