Architecture
Unchurn has four moving parts: the widget (in your customer’s browser), your server (which mints tokens), the Unchurn backend (which talks to Stripe), and Stripe itself. This page maps how they fit together.
The four-component picture
What each component owns
The widget (browser)
- Shows the cancel flow UI (offer cards, decline buttons, confirmation screens)
- Holds zero credentials beyond the short-lived signed token
- Cannot talk to Stripe directly; everything goes through Unchurn’s backend
- Ships as three npm entry points (
@unchurn.dev/widget,/react,/nextjs) plus a CDN IIFE
Your server
- Authenticates the customer (your existing auth — Clerk, NextAuth, Supabase, whatever)
- Maps the customer to their Stripe subscription ID
- Mints the signed token via
createUnchurnHandler - (Roadmap) Will receive an outbound webhook when a subscription goes to manual cancellation. Today, manual cancellation requests are surfaced in your dashboard and via customer confirmation email; outbound webhook delivery is not yet implemented.
Your server never talks to Stripe in the cancel flow. That’s on purpose — you don’t have to wire Stripe SDK calls into your cancel button, and you don’t have to think about retries.
The Unchurn backend
- Verifies signed tokens
- Reads the subscription from Stripe (via the merchant’s Stripe Connect link)
- Runs eligibility checks per offer
- Makes the Stripe change when an offer is accepted
- Records every session, decline, and acceptance in the dashboard
- Stamps an audit timestamp (
merchant_manual_cancellation_notified_at) when a session goes to manual cancellation, so your team can track requests in the dashboard. Outbound webhook delivery to your server is on the roadmap.
Stripe
- Source of truth for subscription state
- Receives the actual changes:
pause_collection,discounts,cancel_at_period_end,trial_end,itemsupdates - Returns the post-change subscription state, which Unchurn reads back before marking a session complete
Trust boundaries
Three boundaries, three pieces of evidence:
| Boundary | What’s checked | Failure mode |
|---|---|---|
| Browser → Your server | Your auth (cookies, sessions, JWT — whatever you already use) | 401, no token issued |
| Browser → Unchurn | HMAC signature on the token | 401, no Stripe call made |
| Unchurn → Stripe | Stripe Connect token validity | Session marked errored, you’re notified |
A captured token is replayable until it expires (5 minutes by default). Keep TTLs short, rotate UNCHURN_SECRET from your dashboard if you suspect a leak, and don’t log tokens to your application logs.
Stripe Connect, not API keys
Unchurn connects to your Stripe account via Stripe Connect OAuth, not by you pasting API keys into our dashboard. This means:
- You can revoke Unchurn’s access from your Stripe dashboard at any time, instantly
- Test mode and live mode are connected separately — connecting one does not connect the other
- We never receive or store your Stripe API keys. We store the connected-account ID (
acct_*) and act on your behalf using Stripe’s standard Connect mechanism
Unchurn requests the standard read_write Connect permission, which is what Stripe Connect grants to integrating apps. We use it only for subscription reads and the retention changes documented in The five offers. You can audit and revoke this access from your Stripe dashboard.
The Connect handshake happens once during merchant onboarding. No renewal is needed unless you deauthorize Unchurn from Stripe.
What Unchurn does not store
- Your Stripe API keys (we use Stripe Connect’s connected-account ID instead)
- Your customer’s payment method details
- Your customer’s email or PII beyond what’s needed to show the flow
What we do store: the connected-account ID for each mode, the session ID, the subscription ID, the offer evaluations, the outcome, and any feedback the customer gives during the flow.
Deployment shape
Unchurn runs on Vercel with Postgres for sessions and configuration, and edge-cached static assets for the CDN widget bundle. Your server runs wherever your app runs — there’s no Unchurn-specific infrastructure on your side beyond the one route handler that mints tokens. The widget bundle is ~40 KB gzipped and loads on its own, so it doesn’t affect your time-to-interactive.
Where to go from here
- How the cancel flow works — the session lifecycle in detail
- The five offers — what each offer changes on Stripe
- Quickstart — get this running in under 10 minutes