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)
We assume that 1 reference id is mapped to 1 Stripe Customer, which can have N subscriptions. Note
that this is required even if you only want a single subscription per customer because cancelling a
subscription and renewing causes multiple subscription to be created by Stripe. In our API's we
often make the assumption of a single 'active' subscription, but multiple active subscriptions are
supported by providing the specific subscription id for a given action like upgrade or cancel.
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
syncStripeSubscriptions 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
syncStripeSubscriptions 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,
stripeMetadata: {
// Additional metadata you want to attach
},
};
},
});Retrieving Subscriptions
The fragment exposes service functions that can be used server-side to query data from the subscription table:
import { stripeFragment } from "@/lib/stripe";
// Get all subscriptions for a reference ID (e.g., user ID)
const subscriptions = await stripeFragment.services.getSubscriptionsByReferenceId(user.id);
// Get a specific subscription by ID
const subscription = await stripeFragment.services.getSubscriptionById(subscriptionId);
// Get all subscriptions for a Stripe customer
const subscriptions = await stripeFragment.services.getSubscriptionsByStripeCustomerId(customerId);Handling Multiple Subscriptions
When users have multiple subscriptions, you need to specify which subscription to upgrade or cancel using the subscription ID, this is the identifier of the fragments subscription table.
const { mutate } = stripeClient.upgradeSubscription();
await mutate({
body: {
priceId: "price_new_plan",
subscriptionId: subscriptionId,
successUrl: `${window.location.origin}/success`,
cancelUrl: window.location.href,
},
});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() {
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,
},
});Managing Billing Details
The Stripe fragment provides a useBillingPortal mutator that generates a redirect URL for users to
Stripe's hosted billing portal. This portal can be used to:
- Update payment methods
- View invoices and payment history
- Update billing information
const { mutate, error, loading } = stripeClient.useBillingPortal();
const response = await mutate({
body: {
returnUrl: `${window.location.origin}/account`,
},
});
if (response?.redirect) {
window.location.href = response.url; // Redirect to Stripe Billing Portal
}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 syncStripeSubscriptions
The fragment provides the syncStripeSubscriptions method as a fragment service. This method will
fetch the latest subscriptions 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.syncStripeSubscriptions(
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);