Payments
What Quickback does natively with Stripe — the subscriptions plugin, services.stripe, and stripe:* inbound webhooks — and what it deliberately leaves to you.
There is no payments pillar. Payments are not a security boundary and not a schema concern, so Quickback ships three concrete pieces rather than an abstraction over billing:
| Piece | What it is | Where |
|---|---|---|
| Subscriptions plugin | Tiers, Checkout, the billing portal, and subscription state kept in sync by Stripe | Subscriptions Plugin |
services.stripe | A configured Stripe client on the services layer, reachable from any action | This page |
stripe:* inbound webhooks | Signature-verified, deduplicated Stripe events dispatched to your handlers | Inbound Webhooks |
Everything else — one-off charges, marketplaces, usage billing — is composed from those three. The one-off checkout recipe shows the composition end to end.
services.stripe
Actions already receive a services object (Executor Parameters).
When a project needs Stripe, a configured client appears on it:
async execute({ services, record }) {
const session = await services.stripe.checkout.sessions.create({ /* … */ });
}No import, no client construction, no key plumbing.
When it is emitted
The client appears when either of these is true:
- the subscriptions plugin is enabled, or
- the webhooks subsystem is on — that is,
providers.databasedeclares awebhooksBinding.
Condition 2 is broader than it may look, and deliberately so. There is no
per-provider inbound webhook configuration surface: the inbound route registers
exactly one provider, Stripe, so any project that turned webhooks on is a
project that can receive Stripe events. The same condition adds stripe to the
generated package.json (pinned at ^22.5.0), so the dependency and the client
can never disagree about whether Stripe is in play.
Accepted over-trigger. A project that only receives Stripe webhooks and
never calls the API still gets the SDK in its bundle — roughly 130 KB gzipped
— because inbound signature verification is hand-rolled on crypto.subtle and
does not need the SDK at all. This was taken deliberately: projects receiving
Stripe events almost always call Stripe from their handlers, and the one-off
checkout recipe depends on the client being there. If a real project hits
Worker bundle limits because of it, that is the trigger to narrow the
condition.
Cloudflare only, and it fails at compile time
Both triggers are Cloudflare-only surfaces, and the non-Cloudflare
createServices() takes no environment to read a key from. So a project that
wants Stripe on another runtime is a compile error, not a silently missing
client. The error names which of the two triggers pulled Stripe in.
Lazy, memoized, and fail-closed on the secret
The client is built on first access and reused for the rest of the request. A request that never touches Stripe pays nothing; one that touches it repeatedly builds the client once.
If STRIPE_SECRET_KEY is unset, the first access throws — it never yields a
half-configured client that fails later at the network boundary:
services.stripe requires the STRIPE_SECRET_KEY secret, which is not set.
Set it with `wrangler secret put STRIPE_SECRET_KEY` for a deployed Worker,
or add STRIPE_SECRET_KEY to .dev.vars for local development.Nothing checks the key before that. The Worker starts fine without it and fails on the first call that reaches Stripe — including one made from a queue handler, where the failure surfaces as a retried message rather than an HTTP response. See the dev loop for how much of that is worth improving, and how little the env guard covers.
STRIPE_SECRET_KEY is added to the generated Env type only when the
subscriptions plugin is enabled. A webhooks-only project gets
services.stripe but no Env entry for the key — the client reads it off the
environment regardless. Do not "fix" this by redeclaring the name under
bindings.secrets when the plugin is on: the compiler does not deduplicate,
and the generated project fails tsc with Duplicate identifier
'STRIPE_SECRET_KEY'.
The client's configuration
new Stripe(secretKey, { maxNetworkRetries: 3, timeout: 30_000 })Two things are notable by their absence:
- No
httpClient. On Workers theworkerdexport condition resolves to stripe-node's fetch/SubtleCryptobuild, soStripe.createFetchHttpClient()is not needed. Passing it is not an improvement. - No
apiVersion. stripe-node typesapiVersionas an exact string literal matching the SDK's own pinned version, so hardcoding one breakstscthe next time the pin moves. Omitting it uses the SDK's pinned version — the same choice Stripe's own Workers template makes. Pin the SDK, not the API version string.
Reaching Stripe outside an action
services is an action-executor parameter. Queue-dispatched webhook handlers
get a raw env instead, and the generated Env is CloudflareBindings —
so createServices takes it directly:
import { createServices } from "../../../lib/services";
onWebhookEvent("stripe:invoice.paid", async (ctx) => {
const { stripe } = createServices(ctx.env);
});You rarely need this on the inbound path: the surface has already verified the signature and handed you the parsed envelope. Reach for it when a handler must call back into Stripe — fetching line items an event omits, say.
If you ever verify a Stripe signature yourself, note that on Workers it is
async-only: await stripe.webhooks.constructEventAsync(...). The synchronous
constructEvent has no Web Crypto path and throws. The inbound surface
already does this for you.
What is not native
| Not built | Why, and what to do instead |
|---|---|
| Stripe Connect | Nothing to build. The inbound envelope already preserves the top-level account (acct_…) — read it via ctx.event.account and route on it yourself. See Stripe Connect. |
| Invoicing | Stripe's own invoicing API is a better invoicing product than a wrapper over it would be. Call it through services.stripe. |
| A generic payments pillar | A provider-neutral payments abstraction over one provider is a guess about the second one. There is deliberately no providers.payments category; it gets revisited if a second payments provider actually lands. |
| A one-off payment primitive | Deliberately a recipe, not a define surface, until the recipe proves to have a hole in real use. |
The escape hatch
services.stripe is a plain, fully-typed Stripe instance. Anything the Stripe
Node SDK can do, an action can do — payment intents, refunds, transfers, Connect
account links, tax calculations — with no Quickback surface in between and no
feature request required.
The rule that constrains it is not about Stripe. It is about where money moves: anything that must post to a ledger synchronously belongs in an action, where the caller sees the failure, and never in an async queue handler. The recipe spells this out.
Next
- One-off checkout — charge for a record, then fulfil it.
- The Stripe dev loop — test keys,
stripe listen, test clocks. - Subscriptions Plugin — recurring billing, already built.