Billing
Billing page — subscription plans, checkout, customer portal, and payment history.
Overview
The billing page lives at /dashboard/billing (app/dashboard/billing/page.tsx). It shows the team's current plan, available upgrades, and payment history. Billing is tied to the active team, not individual users.
All payments go through Dodo Payments. There are no in-app purchases — checkout happens on Dodo's hosted page, and webhooks sync the data back to Convex.
What each role sees
| Role | Sees plans | Can upgrade | Can manage billing | Sees history |
|---|---|---|---|---|
| Owner | Yes | Yes | Yes | Yes |
| Admin | Yes | Yes | Yes | Yes |
| Member | No | No | No | Yes (if payments exist) |
Members see a notice saying "Plan upgrades are managed by your team owner or admin."
Page sections
Team billing summary
A card showing the team name, current plan badge (Free / Pro / Max), and member count. Owners and admins see a "Manage Billing" button that opens the Dodo customer portal. The button appears when the team has billing history (past payments) or an active subscription.
Plan cards
Two cards side by side — Pro and Max — each showing:
- Plan name and price
- Feature list with checkmarks
- Action button (Upgrade / Current Plan / Switch)
The button logic depends on your current plan:
| Current plan | Pro card button | Max card button |
|---|---|---|
| Free | "Upgrade to Pro" → checkout | "Upgrade to Max" → checkout |
| Pro | "Current Plan" (disabled) | "Upgrade to Max" → portal |
| Max | "Switch to Pro" → portal | "Current Plan" (disabled) |
Checkout flow
For first-time subscribers (upgrading from Free):
const createCheckout = useAction(api.payments.createCheckout);
const result = await createCheckout({
product_cart: [{ product_id: plan.productId, quantity: 1 }],
returnUrl: window.location.origin + "/dashboard/billing",
teamId,
});
// Redirect to Dodo's hosted checkout page
window.location.href = result.checkout_url;After payment, the user is redirected back to /dashboard/billing. A webhook from Dodo updates the team's plan in the database.
Plan changes (upgrade/downgrade)
For users already on a paid plan, changes go through the Dodo customer portal:
const getPortal = useAction(api.payments.getCustomerPortal);
const result = await getPortal({ send_email: false, teamId });
window.open(result.portal_url, "_blank");The portal handles proration, plan switching, and cancellation.
Cancelled subscription handling
When a subscription has been cancelled, the plan card shows a "Resubscribe" button that routes to a fresh checkout session instead of the customer portal. This creates a new subscription rather than reactivating the old one.
Redirect URL validation
The returnUrl passed to createCheckout is validated against an ALLOWED_REDIRECT_HOSTS array that whitelists Dodo-related domains. This prevents open redirect attacks through the checkout flow.
Billing history
A table of all payments for the active team:
- Plan badge
- Amount (formatted from cents)
- Date
- Status badge (Active, Succeeded, Cancelled, Refunded, etc.)
Key files
| File | Purpose |
|---|---|
app/dashboard/billing/page.tsx | Billing page (plans, checkout, history) |
components/providers/team-provider.tsx | useTeam() — active team context |