Gating features
Show or hide parts of your app depending on which plan the user has paid for.
Check a user's plan in a server component with getBillingStatus():
import { Suspense } from 'react';
import Link from 'next/link';
import { getBillingStatus } from '@/lib/billing';
import { requireUser } from '@/lib/session';
export default function ReportsPage() {
return (
<Suspense fallback={<p>Loading…</p>}>
<Reports />
</Suspense>
);
}
async function Reports() {
const user = await requireUser();
const { plan } = await getBillingStatus(user.id);
if (plan?.slug !== 'pro') {
return (
<p>
Reports come with Pro. <Link href="/#pricing">Upgrade</Link>
</p>
);
}
return <ProReports />;
}What getBillingStatus() returns
{
plan: Plan | null; // the plan from src/config.ts, or null for free users
source: 'purchase' | 'subscription' | null;
subscription: {
status: string; // 'active', 'on_hold', 'cancelled'…
currentPeriodEnd: Date | null;
cancelAtPeriodEnd: boolean;
} | null;
}Any paid plan: if (plan). A specific plan: if (plan?.slug === 'pro'). Several: ['pro', 'lifetime'].includes(plan?.slug ?? '').
Gate server actions too
Hiding a button isn't enough: anyone can call a server action directly. Check the plan inside the action as well:
'use server';
import { getBillingStatus } from '@/lib/billing';
import { requireUser } from '@/lib/session';
export async function exportReport() {
const user = await requireUser();
if ((await getBillingStatus(user.id)).plan?.slug !== 'pro') {
return { error: 'Exports come with Pro.' };
}
// …
}Many users at once
For lists (such as an admin table), use getBillingStatuses(userIds). It runs two queries in total instead of two per user, and returns a Map from user ID to status.
Rules
- Never call Dodo's API to check access on a page request. It's slow and rate limited. The billing tables are kept in sync by webhooks.
- Plans are matched by their Dodo product ID from the environment. If a plan's
DODO_PRODUCT_…variable is missing in production, users who bought it getplan: null. See Paid but shown as Free.