Buyer Portal
Let buyers manage their own subscriptions. v1 uses a proxy pattern: your backend forwards requests from the browser to Throttle, holding the secret key on your side and enforcing ownership.
sk_* key off the wire and lets you reuse your existing user authentication. The hook API in @usethrottle/subscriptions is designed around this production path.Architecture
- Buyer's browser calls
/api/throttle/api/v1/subscriptionson your domain. - Your backend authenticates the user, verifies they own the resource being mutated, and forwards the call to Throttle with the secret key.
@usethrottle/subscriptions'sSubscriptionProvideruses a customfetcherthat targets the proxy.
Next.js App Router template
A single catch-all route handler proxies the relevant subscription endpoints. The helper pins externalCustomerId server-side on every read, rewrites checkout-session creates to the authenticated buyer, resolves payment-method reads from the buyer's customer row, and verifies ownership before subscription mutations.
// app/api/throttle/[...path]/route.ts
// Next.js App Router proxy. Forwards authenticated requests to Throttle.
import { createSubscriptionProxyHandler } from '@usethrottle/subscriptions/server';
import { auth } from '@/lib/auth';
const handler = createSubscriptionProxyHandler({
apiKey: process.env.THROTTLE_SECRET_KEY!,
async getExternalCustomerId() {
const user = await auth();
return user?.id ?? null;
},
});
export { handler as GET, handler as POST, handler as PATCH };Express template
Per-route variant. Slightly more code, but easier to reason about route-by-route authorization.
// MERCHANT BACKEND — Express variant.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
app.get('/api/throttle/api/v1/subscriptions', requireAuth, async (req, res) => {
const result = await subscriptions.list({
externalCustomerId: req.user.id,
status: req.query.status,
});
res.json(result);
});
app.post('/api/throttle/api/v1/subscriptions/:id/cancel', requireAuth, async (req, res) => {
if (!(await assertUserOwnsSub(req.user.id, req.params.id))) return res.sendStatus(403);
const sub = await subscriptions.cancel(req.params.id, {
atPeriodEnd: req.body?.atPeriodEnd ?? true,
});
res.json(sub);
});
// Repeat for: /pause, /resume, PATCH plan changes, GET single sub.Authorization
Throttle's API key authorizes your account to act on any subscription you own. It does not know which buyer owns which subscription. That mapping lives in your DB (or you derive it via customer.externalId === user.id).
// Verify the buyer owns the subscription before mutating.
async function assertUserOwnsSub(userId: string, subId: string) {
const sub = await subscriptions.get(subId);
if (!sub) return false;
const customer = await subscriptions.getCustomerByExternalId(userId);
return customer?.id === sub.customerId;
}Wiring the React provider
// Mount the React provider with a fetcher that hits your proxy.
'use client';
import { SubscriptionProvider } from '@usethrottle/subscriptions';
export function ThrottleProvider({ children }: { children: React.ReactNode }) {
return (
<SubscriptionProvider
fetcher={async (path, init) => {
return fetch(`/api/throttle${path}`, init);
}}
>
{children}
</SubscriptionProvider>
);
}Every hook in @usethrottle/subscriptions calls through this fetcher. Your proxy is the only thing that talks to Throttle.
What buyers can do
- List their subscriptions and see status, plan, next billing date.
- Pause an active subscription and resume later.
- Cancel at period end (or immediately, if you allow it).
- Change plan (your UI decides which plans are offered).
- Surface a
past_duesubscription with an update-card action. The<DunningBanner />primitive gives you the UI hook; your backend owns the actual card-refresh flow.
Next
- React Package — full hook reference.