Fragno
Stripe

Admin Routes

Build admin dashboards for managing Stripe data

Overview

The Stripe fragment provides reactive hooks that wrap some of the Stripe API routes. These make it easy to build UI's in an admin dashboard in order to link Stripe data (products/prices/etc) to your internal application data.

Available Admin Routes

The fragment exposes these admin endpoints:

RouteDescriptionHook
GET /admin/customersList all Stripe customersuseCustomers()
GET /admin/productsList all Stripe productsuseProducts()
GET /admin/products/:productId/pricesList prices for a specific productusePrices()
GET /admin/subscriptionsList all local subscriptionsuseSubscriptions()

Using Admin Hooks

Set enableAdminRoutes: true in your fragment configuration to enable these admin-only routes. When disabled, these routes will return a 401 Unauthorized error.

Securing Admin Routes

The admin routes should be behind authentication and authorization, use a middleware to make sure that they are not publically accessible!

import { createStripeFragment } from "@fragno-dev/stripe";

export const stripeFragment = createStripeFragment(
  {
    enableAdminRoutes: true,
    ...config,
  },
  dbConfig,
).withMiddleware(async ({ path, headers }, { error }) => {
  const { role } = await getSession(headers);

  if (path.startsWith("/admin") && role !== "admin") {
    return error({ message: "Unauthorized", code: "UNAUTHORIZED" }, 401);
  }
  return undefined;
});

Example: Listing Products

Display all Stripe products with pagination:

app/admin/products.tsx
import { stripeClient } from "@/lib/stripe-client";
import { useState } from "react";

export function ProductsPage() {
  const [cursor, setCursor] = useState<string | undefined>(undefined);

  const {
    data: products,
    loading,
    error,
  } = stripeClient.useProducts({
    query: {
      limit: 25,
      startingAfter: cursor,
    },
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Products ({products?.products.length})</h1>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Description</th>
            <th>Active</th>
          </tr>
        </thead>
        <tbody>
          {products?.products.map((product) => (
            <tr key={product.id}>
              <td>{product.id}</td>
              <td>{product.name}</td>
              <td>{product.description}</td>
              <td>{product.active ? "✓" : "✗"}</td>
            </tr>
          ))}
        </tbody>
      </table>
      {products?.hasMore && (
        <button
          onClick={() => {
            const lastProduct = response.products[response.products.length - 1];
            setCursor(lastProduct.id);
          }}
        >
          Load More
        </button>
      )}
    </div>
  );
}