Noti turns a Notion database into flashcards and schedules reviews with FSRS, the spaced-repetition algorithm Anki also uses. The cards stay in the user's Notion. I built the landing page, the app, the API, billing and the deploys, about 175 commits so far.
Notion allows about three requests per second per connection, so every call to it goes through one pacer per connection, with retries, caps and deadlines.
Auditing my own product
In September I ran a read-only audit of the whole product, with a coding agent doing the legwork against the code, the live deployment and the provider's records. Until then there were no automated tests and no CI.
The worst finding was live. When a renewal payment succeeded, the webhook handler treated the invoice as if it were the subscription and stored the invoice id in place of the subscription id. A later cancellation would then call the payment provider with the wrong id. Two days before the audit, a real subscription update had been rejected outright because it arrived without a user id. Deleting an account could also leave a renewing subscription behind.
Rebuilding billing
- Webhooks are verified with HMAC over the raw bytes, then checked for store, product, variant and live or test mode before anything changes.
- Ownership comes from a signed checkout attempt. Later events map through known subscription and customer ids. An email address alone never decides who owns a subscription.
- Every command to the provider writes a durable record first. If the provider accepts and the local write fails, the user sees "pending" with a link to the billing portal instead of a false success.
- Account deletion checks every known subscription, verifies the cancellation, and only then deletes the user.
- A reconciliation job runs every five minutes in small batches: due webhook events, unresolved commands, subscriptions due for a check, unfinished checkouts and pending deletions. It first ran in production on 8 September. By 30 September it had 6,211 recorded runs, and in the last week the longest gap between two runs was 6 minutes 10 seconds. Two alerts watch it hourly.
Not losing a review
A rating is written to localStorage before the next card
appears, then sent to the server, with sendBeacon when the
tab closes. The server applies it only on top of the revision the browser
saw, so a stale tab cannot overwrite newer progress. Recovery runs from
Supabase pg_cron, which calls the Worker once a minute with a short-lived
HMAC-signed request, because Cloudflare's cron had dispatch failures at
the time.
Tests
About 110 Playwright tests now cover billing, cards, the Notion integration, security, study sessions and analytics. Each run starts a stub of the payment provider, two Workers (one of them without KV), the Vite app and a fresh local Supabase. A core subset also runs on Firefox, WebKit and an emulated Pixel 7. GitHub Actions runs the suite on every pull request and every push to main.
Noti shows the whole job in one place: a product I designed, built, charge money for and keep running on my own.