Quickback Docs

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:

PieceWhat it isWhere
Subscriptions pluginTiers, Checkout, the billing portal, and subscription state kept in sync by StripeSubscriptions Plugin
services.stripeA configured Stripe client on the services layer, reachable from any actionThis page
stripe:* inbound webhooksSignature-verified, deduplicated Stripe events dispatched to your handlersInbound 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:

  1. the subscriptions plugin is enabled, or
  2. the webhooks subsystem is on — that is, providers.database declares a webhooksBinding.

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 the workerd export condition resolves to stripe-node's fetch/SubtleCrypto build, so Stripe.createFetchHttpClient() is not needed. Passing it is not an improvement.
  • No apiVersion. stripe-node types apiVersion as an exact string literal matching the SDK's own pinned version, so hardcoding one breaks tsc the 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 builtWhy, and what to do instead
Stripe ConnectNothing 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.
InvoicingStripe's own invoicing API is a better invoicing product than a wrapper over it would be. Call it through services.stripe.
A generic payments pillarA 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 primitiveDeliberately 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

On this page