# Snapbar receiver and reconciliation example

These two Node.js 24 files run without npm dependencies. Download `receiver.mjs` and `poller.mjs` from this directory into the same private directory. Node's built-in SQLite currently emits an experimental-feature warning. Pin your tested Node 24 runtime and use a persistent disk; do not run this sample on ephemeral/serverless storage.

## Start with a rehearsal activation

1. Obtain a live key with `read:submissions` for current-state reads and a separate live key with `manage:webhooks` for setup. Keys are account credentials: activation filtering on an endpoint does not restrict what a key can read. Use a separate account for a vendor who must not access other events.
2. Register your HTTPS URL (`https://your-host/snapbar`) for `submission.approved`, `consent_policy: media_only`, and your activation UUID. Store its one-time signing secret before creating a real submission. Registration activates the endpoint immediately. A public HTTPS reverse proxy/tunnel should forward `/snapbar` to `127.0.0.1:3000`; the sample deliberately listens only on loopback.
3. Load these environment variables from your secret store; do not paste real keys into shared logs, URLs, or source control:

   - `SNAPBAR_API_KEY`: the live read-submissions key.
   - `SNAPBAR_ACTIVATION_ID`: one activation UUID.
   - `SNAPBAR_SIGNING_SECRETS`: the registered `whsec_...` secret. During rotation, a comma-separated list accepts both old and new secrets.
   - `SNAPBAR_DB`: an absolute SQLite path in a private persistent directory (default `snapbar.sqlite`). New database files are created mode 0600; protect backups, the directory, and any preexisting file too.
   - `PORT`: optional loopback port, default 3000.

4. Run `node receiver.mjs`. In a second process run `node poller.mjs` using the same environment and database path. Run only one receiver worker per SQLite database; a managed multi-worker deployment needs transactional job leases. Poller and receiver can share this one local database.
5. Using the setup key, call `POST https://integrations.snapbar.com/v1/test/deliveries` with `{"endpoint_id":"YOUR_ENDPOINT_UUID"}`. Signature verification stays enabled because the receiver already knows the registered secret. Expect `outcome: succeeded` and `response_status: 204`. The test uses `test: true`, never inserts a wall photo, and produces no Activity row. Test-mode API keys cannot make this network call.
6. Rehearse a real pending photo (no wall entry), approval (entry), rejection (no entry), duplicate request (one job/business result), outage/retry, and urgent removal. Real ingest requires a live key and spends credits; agree the rehearsal budget first.

## What the receiver does

The receiver caps raw bodies at 256 KiB, checks HMAC-SHA256 against the exact bytes and a five-minute timestamp tolerance, rejects conflicting correlation headers, and durably inserts a job before returning 204. HTTP retries and process restarts do not duplicate the job. A background worker reads the *current* submission before updating the `photos` table, so an old approval payload is not authority to restore hidden media. It stores no lead/contact data in the display index and clears raw job payloads when processed. Pending payloads remain sensitive: protect the SQLite database and monitor backlog, disk space, and `receiver_job_retry` markers.

The sample's business operation is a transactionally persisted photo index. It does not download images, publish a public gallery, email anyone, or pretend an arbitrary external side effect is exactly once. To add downloads or your own renderer, key its writes by submission/media ID, preserve the durable-job retry behavior, and delete corresponding files/thumbnails/CDN copies on removal. Never acknowledge a key before the work is durable. Retain dedupe metadata for your replay horizon; `photos` upserts remain safe if historical events are replayed later.

## Polling and catch-up

`node poller.mjs --once --full` performs one complete approved-only reconciliation. It drains all pages before applying a snapshot, preserves the current index on an incomplete scan, excludes hidden media, and removes missing items only after the full traversal. It records a database revision before each read and preserves newer observations, including durable removal markers after rejection, empty media or a 404. An older scan cannot restore those removed photos. The revisions are shared by the receiver and poller and survive restarts; they do not rely on wall-clock precision. Keep this small per-submission removal metadata while older reads or retries can still arrive. The normal loop runs every 30 seconds, re-reads a five-minute overlap with a fixed lower bound while paging, and performs a full scan at least every five minutes.

The completion watermark alone is not a change feed: later approval, regeneration, hide, and delete need full reconciliation. Full scans are not atomic database snapshots either. Continue accepting durable webhook jobs during a scan, re-check current state before applying them, and repeat complete scans to converge. Five-minute overlap is a practical retry window, not a guarantee against arbitrarily late commits. Match your reconciliation interval to your event's removal requirements. For a strict urgent removal, use the operator procedure below instead of waiting for polling.

The example caps a single scan at 100,000 distinct submissions to bound memory and aborts without deleting anything if that limit is exceeded. Larger accounts should stage scan membership in a database. A 429/503 uses Retry-After; keys and response bodies are not logged.

## Urgent removal and endpoint replacement

Run `node receiver.mjs remove SUBMISSION_UUID` in the receiving system's private directory. This records a durable local suppression and removes the index entry; a stale buffered webhook or later scan cannot re-add it. Then hide/reject/delete the submission in Snapbar as appropriate, remove any copies your application downloaded, invalidate CDN caches, and verify both the wall and direct file URL. The example has no remotely exposed removal endpoint. Restoring a suppressed photo is an intentional operator operation: confirm fresh Snapbar approval, then remove that ID from `suppressed` with local authorized database tooling and run a full reconciliation.

To replace a failed endpoint, register and verify B, preserve A's ID for support, and then delete A. Existing A deliveries do not transfer to B and replay cannot redirect them. Run a complete approved-only reconciliation for current display contents; use Snapbar support for workflows requiring historical lead/event catch-up. This recovers current state, not every historical event. Photos already deleted in Snapbar cannot be reconstructed by this API.

Support: include activation, endpoint, submission, delivery/event IDs and API request IDs. Never include API keys, signing secrets, full guest payloads, or query-bearing receiver URLs.
