Subscriptions
Create, manage, and cancel subscriptions
Data Model
The fragment maintains a local database table (subscription_stripe) that tracks subscription
state, with a subscription linked back to an entity in your application through the
referenceId.
In Stripe's model:
- A Subscription connects a Customer (identified by your
referenceId) to a Price - A Product can have multiple Prices (e.g., monthly vs annual)
Stripe also has a concept of "plans" but this API is superseded by the more flexible Prices API. This fragment uses the latter.
Mutations
Stripe is the source of truth for subscription state. Never modify subscription data in the database
tables. All subscription changes (create, update, cancel) should be performed through either this
fragments /subscription/upgrade or /subscription/cancel hooks or directly through the Stripe
API. The database table is updated automatically via webhooks or manually through the
syncStripeSubscription service.
Security
To prevent exposing sensitive information make sure to only expose Stripe Price IDs to end-users, Stripe Customer IDs, Product IDs, and Subscription IDs should remain private.
Payment Collection
Collecting payments from users happens through a redirect to a Stripe Checkout page. Updating or cancelling a subscription happens through a redirect to a Stripe Customer Portal.
Database Schema
The Stripe fragment maintains a subscription_stripe table in your database to track subscription
state. This allows you to query subscription data without making API calls to Stripe and ensures
your application remains responsive even if Stripe is temporarily unavailable.
The subscription_stripe table has a reference_id column that is used to link a subscription to
an application-specific identifier like a user ID or organization ID.
For setting up Fragno DB schemas, see the Fragno DB Quickstart.
Creating Subscriptions
To create a new subscription, use the upgradeSubscription mutator. This mutator handles both
creating new subscriptions and upgrading existing ones.
import { stripeClient } from "@/lib/stripe-client";
export function PricingCard({ priceId, planName }: { priceId: string; planName: string }) {
const { mutate, loading, error } = stripeClient.upgradeSubscription();
const handleSubscribe = async () => {
const { url, redirect } = await mutate({
body: {
priceId: priceId, // Stripe price ID (e.g., "price_1234567890")
successUrl: `${window.location.origin}/success`,
cancelUrl: window.location.href,
},
});
if (redirect) {
window.location.href = url; // Redirect to Stripe Checkout
}
};
return (
<div className="pricing-card">
<h3>{planName}</h3>
<button onClick={handleSubscribe} disabled={loading}>
{loading ? "Loading..." : "Subscribe"}
</button>
</div>
);
}Showing a Success Page
The Stripe Checkout portal will redirect the user back to successUrl after a successful payment,
at this point the webhook event that registers this payment may not (yet) have arrived. Therefore
you can create a dedicated success page which fetches the latest subscription status from using the
syncStripeSubscription service to ensure the end-user sees the right result. See
Manual Synchronization for details.
Configuration Requirements
When creating the Stripe fragment, you must provide two callbacks:
onStripeCustomerCreated
Called when a new Stripe Customer is created for an entity in your system. Use this to store the Stripe Customer ID in your database so you can link it back to your user/organization.
resolveEntityFromRequest
Called on subscription management routes (/subscription/upgrade and /subscription/cancel) to
identify which entity (user or organization) is making the request. This callback should:
- Extract authentication information from the request context (session cookies, JWT tokens in headers, etc.)
- Return the entity's Stripe-related data needed for subscription operations
Prop
Type
See this example:
import { createStripeFragment } from "@fragno-dev/stripe";
import { updateUser } from "@/db/user-repo";
import { getSession } from "@/lib/auth";
export const stripeFragment = createStripeFragment({
// ... other config
onStripeCustomerCreated: async (stripeCustomerId, referenceId) => {
// Store the Stripe Customer ID in your database
await updateUser(referenceId, { stripeCustomerId });
},
resolveEntityFromRequest: async (context) => {
// Extract authentication from your session/auth system
const session = getSession(context.headers);
return {
referenceId: session.user.id,
customerEmail: session.user.email,
stripeCustomerId: session.user.stripeCustomerId || undefined,
subscriptionId: session.user.subscriptionId || undefined,
stripeMetadata: {
// Additional metadata you want to attach
},
};
},
});Retrieving Subscriptions
The fragment exposes a couple of service functions that can be used server-side to query data from the subscription table:
import { stripeFragment } from "@/lib/stripe";
const subscription = await stripeFragment.services.getSubscriptionByReferenceId(user.id);
// or
const subscription = await stripeFragment.services.getSubscriptionById(subscriptionId);
// or
const subscription = await stripeFragment.services.getSubscriptionByStripeCustomer(customerId);Canceling Subscriptions
Users can cancel their subscriptions using the cancelSubscription mutator. Cancellations are
handled through Stripe's Billing Portal, where users confirm their cancellation.
import { stripeClient } from "@/lib/stripe-client";
export function CancelSubscriptionButton({ subscriptionId }: { subscriptionId: string }) {
const { mutate, loading, error } = stripeClient.cancelSubscription();
const handleCancel = async () => {
const { url, redirect } = await mutate({
body: {
returnUrl: `${window.location.origin}/account`,
},
});
if (redirect) {
window.location.href = url; // Redirect to Stripe Billing Portal
}
};
return (
<button onClick={handleCancel} disabled={loading}>
{loading ? "Processing..." : "Cancel Subscription"}
</button>
);
}Changing Subscription Plans
To change the user from one plan to another, use the same upgradeSubscription mutator with a
different price ID. If the user already has an active subscription, the fragment will create a
Stripe Billing Portal confirming the change.
const { mutate } = stripeClient.upgradeSubscription();
await mutate({
body: {
priceId: "price_new_plan", // The new price ID
successUrl: `${window.location.origin}/success`,
cancelUrl: window.location.href,
},
});Manual Synchronization
The subscription table is automatically kept in sync with Stripe through webhook events. However, there are cases where you cannot depend on webhook events because you need a guarantee that you have the most recent data.
For example a "checkout success" page, directly after a user completes payment through Stripe Checkout. They should see their subscription status reflected in your UI immediately. Since webhooks could arrive a few seconds later, it would be better to get the latest info from Stripe directly on the checkout success page.
Using syncStripeSubscription
The fragment provides the syncStripeSubscription method as a fragment service. This method will
fetch the latest subscription from Stripe and write it to your database. If no subscriptions are
found for a customer, then the subscription will be dropped from the database.
import { stripeFragment } from "@/lib/stripe";
import { getSession } from "@/lib/auth";
/* ... */
const session = await getSession();
// Fetch latest subscription data from Stripe and sync to database
await stripeFragment.services.syncStripeSubscription(
session.user.id, // referenceId
session.user.stripeCustomerId, // stripeCustomerId
);
// Now you can assume the db state is recent
const subscription = await stripeFragment.services.getSubscriptionById(session.subscriptionId);
// do things
console.log(subscription.status);