Skip to Content
ConceptsArchitecture

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, items updates
  • Returns the post-change subscription state, which Unchurn reads back before marking a session complete

Trust boundaries

Three boundaries, three pieces of evidence:

BoundaryWhat’s checkedFailure mode
Browser → Your serverYour auth (cookies, sessions, JWT — whatever you already use)401, no token issued
Browser → UnchurnHMAC signature on the token401, no Stripe call made
Unchurn → StripeStripe Connect token validitySession 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

Last updated on