Stripe Integration
Add checkout, portal sessions, billing status, webhooks, product catalogs, metering, and database-backed billing snapshots.
Install from the CLI
farm add integration stripe --uiConfig-first setup
import { stripe } from "@farm.js/stripe";
export const integrations = {
billing: stripe({
secretKey: process.env.STRIPE_SECRET_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
products: [
{
id: "pro",
name: "Pro",
prices: [{ interval: "month", amount: 2900, currency: "usd" }],
},
],
}),
};Choose SDK ownership
Let @farm.js/stripe construct the SDK
The config-first example is the adapter-owned path. When instance is omitted, the
@farm.js/stripe adapter creates a real Stripe SDK client from secretKey, supplied directly or
through STRIPE_SECRET_KEY.
Provide an application-owned instance
import Stripe from "stripe";
import { stripe } from "@farm.js/stripe";
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
maxNetworkRetries: 2,
});
export const integrations = {
billing: stripe({
instance: stripeClient,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
}),
};Use this path when the application needs to own retries, telemetry, API-version settings, or a compatible test adapter. The instance wins if a secret key is also supplied. Webhook, product, billing, route, and storage settings remain integration options in either mode.
stripeClient above is the vendor's Stripe SDK object—not another Farm wrapper. The
stripe(...) call remains responsible for adapting that SDK to Farm routes, callers, webhooks, and
lifecycle behavior.
Usage
const checkout = await apiClient.billing.checkout.post({
body: {
productId: "pro",
successPath: "/success",
cancelPath: "/pricing",
},
});
if (checkout.data?.redirectTo) {
window.location.href = checkout.data.redirectTo;
}Database-backed billing
The Stripe integration can use Farm's integration ORM layer through ctx.args.db, so relational billing snapshot reads and writes can work across supported database clients. This is database access, not Farm KV storage.
What Stripe adds
| Area | Details |
|---|---|
| Catalog | Public product and price metadata for pricing pages. |
| Checkout | A typed checkout route that can return JSON for callers or redirect for browser navigation. |
| Customer portal | A typed route for opening Stripe's billing portal. |
| Billing status | Subscription, trial, seats, cancellation, and plan state. |
| Entitlements | Feature, limit, usage, meter, and billing checks. |
| Webhooks | Event verification and snapshot updates for checkout and subscription events. |
| Database | Schema-backed billing account snapshots through ctx.args.db. |
Common callers
const products = await api.billing.products.get();const status = await api.billing.status.get();
if (status.data?.status === "active") {
console.log(status.data.planId);
}const portal = await api.billing.portal.post({
body: {
returnTo: "/settings/billing",
},
});
if (portal.data?.redirectTo) {
window.location.href = portal.data.redirectTo;
}Checkout and portal return paths must be same-origin, root-relative application paths. Protocol-relative paths such as //example.com are rejected.
Billing owner
Production billing usually needs an owner resolver. That resolver decides whether the billing account belongs to a user, organization, workspace, or team.
stripe({
secretKey: process.env.STRIPE_SECRET_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
billing: {
async resolveOwner(ctx) {
const userId = ctx.req.get<string>("user.id");
if (!userId) {
return null;
}
return {
id: userId,
kind: "user",
email: ctx.req.get<string>("user.email") ?? null,
};
},
},
});The same ctx includes ctx.args.db, ctx.data, request params, the raw request, and request-scoped context values from middleware or auth integrations.
Production notes
- Set
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET, andAPP_BASE_URL. - Keep product IDs stable because they become the app-facing contract.
- Verify webhook signatures before mutating billing state.
- A checkout grants its plan only once the session is
completeand its payment ispaid(or needed none). Subscribe the webhook endpoint tocheckout.session.completedandcheckout.session.async_payment_succeededso delayed payment methods are fulfilled when the money arrives. - Store billing snapshots through the integration schema when the app needs fast entitlement reads.
- Run
farm stripe migrate --applyafter upgrading so the additivebilling_checkout_sessiontable can persistently deduplicate success-page and webhook delivery. - Checkout completion, billing sync, and payment hooks are claimed by Stripe session ID before they
run. Custom persistence should implement
claimCheckoutSession; without it, Farm falls back to comparing the stored billing snapshot, so its billing-account reader must accompanysaveBillingSnapshot. ImplementreleaseCheckoutSessionalongside it: when saving the snapshot or a checkout hook fails, Farm releases the claim so Stripe's retry can finish the checkout. The built-in storage adapters do this already. - Use server callers for admin-only operations and browser callers for checkout/portal redirects.
- Test checkout success, cancel, webhook replay, portal return, subscription update, and trial edge cases.