This document explains how to use the Clerk Billing integration in the app.
Plans configured in Clerk:
free_user- Free tierstandard- Paid tier
Metadata configured in Clerk: To give a user a free subscription:
Store a plan name (currently only "standard") in billingOverride in user's public metadata:
{billingOverride: 'standard'}
The billing code uses this to override the user's actual plan (may be none or free) with the given plan.
Every API request automatically logs the user's billing status:
[Billing] User user_xxx: { activePlan: 'standard', hasFreeUserPlan: false, hasStandardPlan: true }
This happens in src/middleware.ts:25-29.
The billing status is available in locals.billingStatus for all API routes.
// GET /api/user/billing-status
export const GET: APIRoute = async ({ locals }) => {
const status = locals.billingStatus;
return new Response(
JSON.stringify({
activePlan: status.activePlan,
plans: {
free_user: status.hasFreeUserPlan,
standard: status.hasStandardPlan,
},
}),
{ status: 200 }
);
};import { requireStandardPlan } from '../../../lib/api-helpers';
export const GET: APIRoute = async ({ locals }) => {
// Check if user has standard plan
const billingCheck = requireStandardPlan(locals);
if (billingCheck) return billingCheck; // Returns 403 if not standard
// User has standard plan - continue with premium feature
// ...
};import { requirePlan } from '../../../lib/api-helpers';
export const GET: APIRoute = async ({ locals }) => {
// Require free_user plan
const billingCheck = requirePlan(locals, 'free_user');
if (billingCheck) return billingCheck;
// Or require standard plan
const standardCheck = requirePlan(locals, 'standard');
if (standardCheck) return standardCheck;
// User has required plan
// ...
};checkBillingStatus(auth)- Get billing status from auth objectlogBillingStatus(userId, status)- Log billing info to consolehasActivePlan(status, plan)- Check if user has specific planhasStandardPlan(status)- Check if user has standard planisFreePlan(status)- Check if user is on free plan
getBillingStatus(locals)- Get billing status from localsrequirePlan(locals, plan)- Require specific plan (returns 403 error if not met)requireStandardPlan(locals)- Require standard plan (returns 403 error if not met)
type BillingPlan = 'free_user' | 'standard';
interface BillingStatus {
hasFreeUserPlan: boolean;
hasStandardPlan: boolean;
activePlan: BillingPlan | 'none';
}-
View billing status:
- Make any API request
- Check Cloudflare logs for
[Billing]messages
-
Get billing status endpoint:
curl https://packzen.org/api/user/billing-status
-
Test plan requirements:
- Add
requireStandardPlan(locals)to an API route - Try accessing as free user → should get 403
- Try accessing as standard user → should work
- Add
The app uses environment variables to configure fallback redirect URLs after authentication. These are configured in wrangler.jsonc:
How it works:
-
Protected pages with redirect_url: When an unauthenticated user accesses a protected page (e.g.,
/dashboard), the API returns 401. The client-side error handler insrc/lib/api.ts:90redirects to/sign-in?redirect_url=/dashboard. After authentication, Clerk uses thisredirect_urlto return the user to the original page. -
Direct sign-in visits: When a user directly visits
/sign-in(noredirect_urlparameter), the fallback environment variable is used, sending them to/dashboardafter authentication.
Checkout Redirect: The pricing page uses the newSubscriptionRedirectUrl prop in the mountPricingTable() call to redirect users to /dashboard after successful subscription.
A pricing page has been created at /pricing that uses Clerk's <PricingTable /> component.
Enable Free Trials:
- Go to Clerk Dashboard → Billing → Subscription Plans
- Select a plan
- Enable "Free Trial"
- Set trial duration (e.g., 14 days)
- The PricingTable component automatically updates
Users can access pricing/subscription through:
- User Menu → "Subscription & Pricing"
- UserProfile component (shows current subscription)
- Direct link:
/pricing
Users can manage their subscriptions through the <UserProfile /> component:
- View current plan
- Cancel subscription
- Update payment method
When you add features in Clerk Dashboard:
- Go to Clerk Dashboard → Billing → Features
- Add feature (e.g., "advanced_analytics")
- Assign to plans
- Use in code:
const auth = await getAuth(locals); if (!auth.has({ feature: 'advanced_analytics' })) { return new Response('Feature not available', { status: 403 }); }
Or add to the billing helpers in src/lib/billing.ts.
{ "vars": { "CLERK_SIGN_IN_FALLBACK_REDIRECT_URL": "/dashboard", "CLERK_SIGN_UP_FALLBACK_REDIRECT_URL": "/dashboard", }, }