Repository navigation
feat: Supporter status on the server account, kept by RevenueCat's webhook - #51
Merged
Merged
Conversation
3 of 4 tasks
Owner
Author
|
Review pass (security, correctness, compatibility, privacy). Fixes pushed to this branch:
Verified: |
leviFrosty
force-pushed
the
account-activity
branch
from
October 18, 2026 07:16
d798ec4 to
b9091ef
Compare
leviFrosty
force-pushed
the
revenuecat-link
branch
from
October 18, 2026 07:16
f8af60c to
3c5fce3
Compare
… webhook Links the RevenueCat customer an app is logged in as to its account (POST /accounts/v1/revenuecat-customers), stores only what RevenueCat's REST API says about it, and keeps that current from RevenueCat's webhook (POST /revenuecat/v1/webhook), which reads every linked customer an event names again. isAccountSupporter answers from those records, so bearer Notes Import no longer takes revenueCatAppUserId (witness-work ADR 0029).
A bearer request is the token's account: suspended accounts are refused, Supporter status is the account's, and limits are per account. The legacy body with accountId keeps asking RevenueCat while auth:legacy is on and is 426 upgrade_required once it's off.
REVENUECAT_API_URL (development workers only) points account reads at a local fake that pnpm test:e2e serves; the verify launcher sets a synthetic webhook secret.
No RevenueCat read had a deadline, so a hung RevenueCat held route planning, Notes Import's kickoff, links, and webhook deliveries open. Every read now fails as unreachable after 5 seconds, headers and body together, and the webhook reads the customers an event names all at once.
A subscription renews at its end, but the stored record kept answering 'expired' until its read was a day old, so a renewing Supporter counted as free (Notes Import) and was refused route planning whenever the RENEWAL webhook came late or not at all. A record whose end passed since it was read is now read once more before it answers. Stale records are read together, and one that finds a Supporter answers even if another read failed. Linking puts the customer row back inside its batch, so the hourly prune can't delete it between the read and the link.
Every link reads RevenueCat, and the read tier allowed 600 a minute per IP, enough for one account to spend the worker's shared RevenueCat rate limit and fail everyone's Supporter checks. Links now also count against the register tier (60 a minute) per account.
Sixty real links per run made the suite slow enough on CI that the 1 MiB blob test, running alongside, passed its 5-second timeout.
It takes 2 to 4 seconds on CI and failed the 5-second default twice.
leviFrosty
force-pushed
the
revenuecat-link
branch
from
October 18, 2026 07:19
3c5fce3 to
515450b
Compare
leviFrosty
added a commit
that referenced
this pull request
Oct 18, 2026
- feat: Supporter status on the server account, kept by RevenueCat webhooks (#51) - feat: account activity log and signed-in devices (#50) - feat: attested account sessions with bearer tokens for Buddies and Notes Import (#49) - feat: move attestation to /attestation/v1 and add readable link paths (#47) - feat: use readable payload and signature names in signed envelopes (#46) - docs: trim one-time setup steps from the agent guide (#48)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The server account now decides whether a User is a Supporter (witness-work ADR 0029). The app links the RevenueCat customer it's logged in as to its account. ww-api stores only what RevenueCat's REST API says about that customer, and RevenueCat's webhook keeps it current. Notes Import (bearer) and route planning now answer from the account, not from an id the client sends.
Stacked on #50 (which is on #49). This diff covers only the Supporter link. App PR: leviFrosty/witness-work#780. ADR: witness-work
docs/adr/0029-supporter-status-on-the-server-account.md.Routes
POST /accounts/v1/revenuecat-customers{ appUserId }. Reads the customer from RevenueCat, links it, and answers{ ok, supporter, expiresAt }GET /accounts/v1/supporter{ ok, supporter, expiresAt, customers }POST /revenuecat/v1/webhookAuthorization: Bearer <REVENUECAT_WEBHOOK_AUTHORIZATION>in constant timePOST /route-planning/optimize{ start, stops }: the account's status and limits, and suspended accounts are refused. The legacyaccountIdbody still works whileauth:legacyis on, then gets 426Bearer Notes Import drops
revenueCatAppUserId(ignored if sent) and usesisAccountSupporter. Legacy Notes Import and legacy route planning work exactly as before.How it works
idand the customers it names (app_user_id,original_app_user_id,aliases,transferred_from,transferred_to).supporter_check_failed, Notes Import counts the User as free.0003_revenuecat-customersaddsrevenuecat_customers,account_revenuecat_customersandrevenuecat_webhook_events. The hourly cron prunes old event ids and customers no account links.Authorizationdrop covers the webhook too.REVENUECAT_API_URL, honored on development workers only, points reads at a local fake. The e2e suite serves that fake.Threat model
RevenueCat's SDK treats the app user id as the customer's capability. To claim someone else's purchases you'd need their id (122 random bits, never shown or logged) plus an attested session, and the caps stop a leaked id spreading to many accounts. We considered logging in to RevenueCat as the server account id and rejected it: moving purchases needs a RevenueCat transfer, which would strip Supporter status from devices still on older app versions. Proof from store-signed transactions is deferred; ADR 0029 §2 explains why.
Test plan
pnpm test: 1089 passed. New:src/accounts/supporter.test.ts(26),src/routePlanning/route.bearer.test.ts(9); Notes Import and settlement tests updatedpnpm exec tsc --noEmit,wrangler deploy --dry-run, andwrangler deploy --env dev --dry-rundev.mjs upwithREVENUECAT_API_URL,doctorok,pnpm test:e2e82 passed (3 token-gated skipped), including the newsrc/e2e/supporter.e2e.test.tsww-accounts-dev:wrangler tailshowed only{type, customers}with the Authorization header redacted.Deploy notes
ww-accountsbefore it deploys.REVENUECAT_WEBHOOK_AUTHORIZATIONis already set on prod and dev. Copies are in the Agents vault as "ww-api RevenueCat webhook secret (prod)" and "(dev)".