Skip to Content
LaunchfastDocs

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():

src/app/dashboard/reports/page.tsx
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:

src/app/dashboard/reports/actions.ts
'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 get plan: null. See Paid but shown as Free.

On this page