General
Billing
Check Purchases & Subscriptions
Learn how to check for purchases and subscriptions to provide access to premium features.
One of the most common use cases for billing is to provide access to premium features based on a user's or organization's subscription status.
Plan IDs
Plan IDs are defined by the keys in your billing.config.ts file. For example, if you have:
config/billing.config.ts
export const billingConfig = {
plans: {
free: { isFree: true },
pro: {
/* ... */
},
lifetime: {
/* ... */
}
}
};The plan IDs would be "free", "pro" and "lifetime".
Client-Side Checks
Use tRPC queries to check for purchases and subscriptions on the client:
components/premium-feature.tsx
'use client';
import { trpc } from '@/trpc/client';
export function PremiumFeature() {
const { data: billingStatus } =
trpc.organization.subscription.getStatus.useQuery();
if (!billingStatus?.activePlan) {
return <div>Please subscribe to access this feature</div>;
}
// Check if user has a specific plan
const hasProPlan = billingStatus.activePlan.planId === 'pro';
const hasLifetimeAccess = billingStatus.activePlan.planId === 'lifetime';
const hasActiveSubscription =
billingStatus.subscription?.status === 'active' ||
billingStatus.subscription?.status === 'trialing';
if (!hasActiveSubscription && !hasLifetimeAccess) {
return <div>Please subscribe to access this feature</div>;
}
return <div>Premium feature content</div>;
}Billing Status Properties
The getStatus query returns:
activePlan: The currently active plan (if any), containingplanId,planName,status, etc.subscription: The active subscription object (if any), containingstatus,currentPeriodEnd, etc.enabled: Whether billing is enabled
Server-Side Checks
In tRPC Procedures
trpc/routers/premium-feature.ts
import { createTRPCRouter, protectedOrganizationProcedure } from '@/trpc/init';
import { TRPCError } from '@trpc/server';
import {
getActivePlanForOrganization,
hasActivePaidPlan,
hasSpecificPlan
} from '@/lib/billing';
export const premiumFeatureRouter = createTRPCRouter({
access: protectedOrganizationProcedure.query(async ({ ctx }) => {
const activePlan = await getActivePlanForOrganization(ctx.organization.id);
if (
!activePlan ||
(activePlan.planId === 'free' && !activePlan.isLifetime)
) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'This feature requires an active subscription'
});
}
return { data: 'premium content' };
})
});In Server Components
app/(saas)/dashboard/premium/page.tsx
import { getSession } from "@/lib/auth";
import { getActivePlanForOrganization } from "@/lib/billing";
import { redirect } from "next/navigation";
export default async function PremiumPage() {
const session = await getSession();
if (!session?.session.activeOrganizationId) {
redirect("/auth/sign-in");
}
const activePlan = await getActivePlanForOrganization(session.session.activeOrganizationId);
if (!activePlan || (activePlan.planId === 'free' && !activePlan.isLifetime)) {
redirect("/dashboard/choose-plan");
}
return <div>Premium page content</div>;
}Organization-Based Checks
Billing is organization-based by default. All checks use the organization ID from the context:
Client-Side
components/organization-feature.tsx
'use client';
import { trpc } from '@/trpc/client';
export function OrganizationFeature() {
const { data: billingStatus } =
trpc.organization.subscription.getStatus.useQuery();
if (
!billingStatus?.subscription ||
(billingStatus.subscription.status !== 'active' &&
billingStatus.subscription.status !== 'trialing')
) {
return <div>This organization needs an active subscription</div>;
}
return <div>Organization premium feature</div>;
}Server-Side
trpc/routers/organization-feature.ts
import { createTRPCRouter, protectedOrganizationProcedure } from '@/trpc/init';
import { TRPCError } from '@trpc/server';
import { hasActivePaidPlan } from '@/lib/billing';
export const organizationFeatureRouter = createTRPCRouter({
access: protectedOrganizationProcedure.query(async ({ ctx }) => {
const hasActivePlan = await hasActivePaidPlan(ctx.organization.id);
if (!hasActivePlan) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'This organization requires an active subscription'
});
}
return { data: 'organization premium content' };
})
});Plan Limits
You can also check plan limits:
lib/billing/check-limits.ts
import { getOrganizationPlanLimits } from '@/lib/billing/guards';
export async function checkMemberLimit(organizationId: string) {
const limits = await getOrganizationPlanLimits(organizationId);
// -1 means unlimited
if (limits.maxMembers === -1) {
return true; // Unlimited members
}
// Check current member count against limit
const currentMembers = await getMemberCount(organizationId);
return currentMembers < limits.maxMembers;
}Helper Functions
Use the built-in guard functions for common checks:
lib/billing/feature-guards.ts
import { TRPCError } from '@trpc/server';
import {
hasActivePaidPlan,
hasSpecificPlan,
requirePaidPlan,
requireSpecificPlan
} from '@/lib/billing';
// Require any paid plan
export async function requireActivePlan(organizationId: string) {
await requirePaidPlan(organizationId);
}
// Require a specific plan
export async function requirePlan(organizationId: string, planIds: string[]) {
await requireSpecificPlan(organizationId, planIds);
}
// Check if has active plan (doesn't throw)
export async function checkHasActivePlan(
organizationId: string
): Promise<boolean> {
return hasActivePaidPlan(organizationId);
}
// Check if has specific plan (doesn't throw)
export async function checkHasPlan(
organizationId: string,
planId: string
): Promise<boolean> {
return hasSpecificPlan(organizationId, planId);
}