Skip to main content
The payment service module (app/services/payment.ts) provides three server-side functions that cover the entire subscription flow: loading available plans, initiating a subscription, and querying a customer’s existing subscriptions. All functions use the Stripe Node SDK and must run server-side inside React Router loaders or actions.
The Stripe client is initialized with a hardcoded test key. Replace sk_test_... with your actual secret key via an environment variable before deploying. See Environment configuration.

Functions

getConfig

Fetches all active prices from Stripe with their associated product data expanded inline.
export const getConfig = async (): Promise<{ prices: Stripe.ApiList<Stripe.Price> }>
Calls stripe.prices.list() with expand: ["data.product"]. The expand option tells Stripe to inline the full Stripe.Product object on each price instead of returning a bare product ID, so you can display the plan name and metadata without a second API call. Parameters This function takes no parameters. Return value
prices
Stripe.ApiList<Stripe.Price>
required
A paginated Stripe list object containing all prices.
Example usage Load prices in the plans route loader and pass them to the component:
prices loader
import { getConfig } from "~/services/payment";

export async function loader() {
  const { prices } = await getConfig();
  return { prices: prices.data };
}

subscribe

Creates a new Stripe subscription in an incomplete state and returns the client secret needed to confirm payment on the client side.
export const subscribe = async (
  customerId: string,
  priceId: string
): Promise<{ subscription: Stripe.Subscription; clientSecret: string }>
Calls stripe.subscriptions.create() with payment_behavior: "default_incomplete". This means Stripe creates the subscription and generates a PaymentIntent, but does not charge the customer immediately. Your frontend must call stripe.confirmCardPayment(clientSecret) to complete the payment and activate the subscription. The expand: ["latest_invoice.payment_intent"] option tells Stripe to inline the PaymentIntent on the response so the client secret is available without a second API call. Parameters
customerId
string
required
The Stripe customer ID (e.g. cus_Abc123). Read this from the session cookie — it is stored there by the registration action after calling createCustomer.
priceId
string
required
The Stripe price ID to subscribe the customer to (e.g. price_1Abc123). Obtained from the getConfig response or from the submitted form.
Return value
subscription
Stripe.Subscription
required
The newly created subscription object. Its status will be incomplete until payment is confirmed.
clientSecret
string
required
The client_secret from the subscription’s latest invoice’s PaymentIntent. Pass this to stripe.confirmCardPayment() in your React component to collect and confirm the card payment.
Example usage The subscribe action reads customerId from the session and priceId from the submitted form:
subscribe action
import { subscribe } from "~/services/payment";
import { getSession } from "~/sessions";

export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  const customerId = session.get("customerId");

  const formData = await request.formData();
  const priceId = String(formData.get("priceId"));

  const { clientSecret } = await subscribe(customerId, priceId);

  // Return the client secret to the component for payment confirmation
  return { clientSecret };
}
On the client side, use the client secret with Stripe Elements:
payment confirmation
import { useStripe, useElements, CardElement } from "@stripe/react-stripe-js";

const stripe = useStripe();
const elements = useElements();

const { error } = await stripe.confirmCardPayment(clientSecret, {
  payment_method: { card: elements.getElement(CardElement) },
});
The clientSecret grants the ability to confirm the underlying PaymentIntent. Never log it or expose it beyond the immediate payment flow.

listSubscriptions

Returns all Stripe subscriptions for a given customer.
export const listSubscriptions = async (
  customerId: string
): Promise<Stripe.ApiList<Stripe.Subscription>>
Calls stripe.subscriptions.list({ customer: customerId }). Returns subscriptions in all statuses (active, incomplete, canceled, etc.) unless you add a status filter. Parameters
customerId
string
required
The Stripe customer ID whose subscriptions you want to retrieve. Read from the session cookie.
Return value Returns Promise<Stripe.ApiList<Stripe.Subscription>>.
data
Stripe.Subscription[]
required
Array of subscription objects for the customer.
Example usage Load subscriptions in the account route loader:
account loader
import { listSubscriptions } from "~/services/payment";
import { getSession } from "~/sessions";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  const customerId = session.get("customerId");

  if (!customerId) {
    return redirect("/");
  }

  const subscriptions = await listSubscriptions(customerId);
  return { subscriptions: subscriptions.data };
}
Filter by status: "active" to show only currently active subscriptions: stripe.subscriptions.list({ customer: customerId, status: "active" }). You can apply this filter directly inside listSubscriptions or at the call site.

Build docs developers (and LLMs) love