← Back to Work
Project · Payments & Billing

Payment Gateway — Stripe and Authorize.net Behind One Billing API

A provider-agnostic subscription billing gateway in TypeScript: one API, one webhook pipeline, and one set of React billing widgets, with Stripe and Authorize.net plugged in as interchangeable providers.

Focus: a framework-free billing core, verified and de-duplicated webhooks, a single entitlement rule for paywalls and grace periods, drop-in React checkout and billing screens, and a demo app that exercises every billing state.

Core
TypeScript (ESM + CJS)
Providers
Stripe, Authorize.net
UI kit
React 18 / 19
Demo app
Fastify, Vite, Keycloak

What it does

Every app that sells a subscription ends up writing the same code: a checkout form, a webhook handler, a table that tracks who has paid, a paywall, a “your card was declined” banner, and a billing page. That code is usually written against one processor and has to be rewritten when the business changes processors or needs a second one.

This gateway writes it once. The app talks to a single BillingProvider interface (create customer, start checkout, cancel, resume, retry payment, update card, list invoices, verify webhooks), and each processor is an adapter behind it. Switching from Stripe to Authorize.net is a configuration change, not a rewrite. The checkout screens, billing dashboard, and webhook inbox in the screenshots below are the same code running against each provider.

It ships as three packages: @lonesome/gateway (the core), @lonesome/gateway-server (email notifications, retries, and reconciliation loops), and @lonesome/gateway-react (checkout and billing widgets). A demo app built around them shows the whole subscription lifecycle end to end.

How the generic gateway works

  • One event vocabulary. Each provider's native webhooks are translated into a small set of normalized events, such as subscription.activated, subscription.payment_failed, subscription.payment_action_required, subscription.canceled, invoice.paid, and payment_method.updated. Decline codes become normalized reasons like card_declined, insufficient_funds, or authentication_required. Knowledge of a provider's native events lives in one file per provider.
  • One webhook pipeline. Every webhook is signature-verified, recorded and de-duplicated on provider and event ID, applied to the subscription record, then handed to app hooks. A failing hook is logged and never makes the provider retry a webhook that was already applied.
  • One entitlement rule. A single pure function decides whether a user has access and which banner they see: active, in a grace period after a failed payment, waiting on 3D Secure, canceled but paid through period end, or paywalled. The UI never guesses at billing state.
  • Webhooks are the source of truth. The billing status endpoint never calls the provider live. A reconcile pass at startup and on a schedule catches anything a missed webhook left out of sync, and pages update live over server-sent events.
  • A framework-free core. The gateway imports no database driver or web framework, and a test enforces it. Apps plug in their own storage through a repository interface, and a shared test suite checks that storage handles duplicate webhook deliveries safely.
  • Card data stays off the server. Card fields are handled by Stripe Elements or Authorize.net Accept.js in the browser, so the raw card number never reaches the app's server (PCI SAQ A-EP).

Stripe and Authorize.net

The two processors work very differently, and the adapters absorb the differences so the app doesn't have to.

  • Stripe: hosted Checkout or an inline form backed by SetupIntents, card updates through Stripe Elements, 3D Secure handled through requires_action, idempotency keys on charges and refunds, and webhooks verified with Stripe's signing secret. Invoices and receipts come from Stripe.
  • Authorize.net: Accept.js tokenizes the card in the browser, then the server creates a CIM payment profile, charges the first period, and starts an ARB recurring subscription. Webhooks are verified with HMAC-SHA512 and a timing-safe compare. Where Authorize.net lacks a feature the gateway fills the gap: it schedules cancel-at-period-end itself, raises a 3D Secure “action required” event when a retry needs it, and generates PDF and email receipts because there are no hosted invoices. Money is parsed as integer cents, never floating point.

Screenshots

Stage 1

Paywall & Checkout (Stripe)

A signed-in user without a subscription hits the paywall. The checkout form is built from the React kit's card components (billing address, cardholder, and card fields), and card data goes straight to Stripe. Once the activation webhook lands, access unlocks and the saved card is shown.

Stage 2

Billing Dashboard

The plan, renewal date, cancel action, payment method, and billing history with receipts are all drop-in React widgets. The demo shows the entitlement result underneath, so you can see exactly why the user has access and which banner they would get.

Stage 3

Webhook Inbox & Dev Tools

Every webhook the gateway verified is listed with its normalized event, the raw payload, and whether it was applied or ignored. In demo mode, dev tools fire declines, insufficient funds, 3D Secure, card updates, scheduled and immediate cancels, dunning, and grace-period expiry through the same pipeline, so every billing state can be tested in seconds.

Stage 4

Same App on Authorize.net

Switch the provider and the same screens run on Authorize.net: Accept.js tokenizes the card, the server creates the CIM profile and ARB subscription, and Authorize.net's webhooks arrive as the same normalized subscription.activated and payment_method.updated events.

Adding another provider

The gateway was built to take more processors. A new one is a folder with an adapter class and an event mapper, a config schema, and one line in the provider factory. Features a processor doesn't support raise a clear “unsupported” error instead of failing silently, and every provider has to pass the same shared contract tests as Stripe and Authorize.net. The app, React widgets, and webhook pipeline don't change.

If your business runs on a different processor, such as Braintree, PayPal, Square, Adyen, or a regional gateway, I can add it as another adapter.

Stack

TypeScript core built for both ESM and CommonJS on Node 18+, with zod-validated config and the Stripe and Authorize.net SDKs loaded only when used. React 18/19 widget kit with TanStack Query, plus a headless API for custom layouts. The demo app runs on Fastify, SQLite, Vite, and React, with Keycloak sign-in and Mailpit for email. Tests cover the core, server companion, and React kit, and CI packs each package and checks it loads from both ESM and CommonJS.