Account API | Next.js Supabase SaaS Kit
Complete reference for the Account API in MakerKit. Manage personal accounts, subscriptions, billing customer IDs, and workspace data with type-safe methods.
The Account API is MakerKit's server-side service for managing personal user accounts. It provides methods to fetch subscription data, billing customer IDs, and account switcher information. Use it when building billing portals, feature gates, or account selection UIs. All methods are type-safe and respect Supabase RLS policies.
Use the Account API for: checking subscription status for feature gating, loading data for account switchers, accessing billing customer IDs for direct provider API calls. Use the Team Account API instead for team-based operations.
Account API Reference
Learn how to use the Account API in MakerKit
Setup and initialization
Import createAccountsApi from @kit/accounts/api and pass a Supabase server client. The client handles authentication automatically through RLS.
import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function ServerComponent() { const client = getSupabaseServerClient(); const api = createAccountsApi(client); // Use API methods}In Server Actions:
'use server';import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';export async function myServerAction() { const client = getSupabaseServerClient(); const api = createAccountsApi(client); // Use API methods}Always create the Supabase client and API instance inside your request handler, not at module scope. The client is tied to the current user's session.
API Methods
getAccountWorkspace
Returns the personal workspace data for the authenticated user. This includes account details, subscription status, and profile information.
const workspace = await api.getAccountWorkspace();Returns:
{ id: string | null; name: string | null; picture_url: string | null; public_data: Json | null; subscription_status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'incomplete' | 'incomplete_expired' | 'paused' | null;}Usage notes:
- Called automatically in the
/home/(user)layout - Cached per-request, so multiple calls are deduplicated
- Returns
nullvalues if the user has no personal account
loadUserAccounts
Loads all accounts the user belongs to, formatted for account switcher components.
const accounts = await api.loadUserAccounts();Returns:
Array<{ label: string; // Account display name value: string; // Account ID or slug image: string | null; // Account picture URL}>Example: Build an account switcher
import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function AccountSwitcher() { const client = getSupabaseServerClient(); const api = createAccountsApi(client); const accounts = await api.loadUserAccounts(); return ( <select> {accounts.map((account) => ( <option key={account.value} value={account.value}> {account.label} </option> ))} </select> );}getSubscription
Returns the subscription data for a given account, including all subscription items (line items).
const subscription = await api.getSubscription(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The account UUID |
Returns:
{ id: string; account_id: string; billing_provider: 'stripe' | 'lemon-squeezy' | 'paddle'; status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'incomplete' | 'incomplete_expired' | 'paused'; currency: string; cancel_at_period_end: boolean; period_starts_at: string; period_ends_at: string; trial_starts_at: string | null; trial_ends_at: string | null; items: Array<{ id: string; subscription_id: string; product_id: string; variant_id: string; type: 'flat' | 'per_seat' | 'metered'; quantity: number; price_amount: number; interval: 'month' | 'year'; interval_count: number; }>;} | nullExample: Check subscription access
import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function checkPlanAccess(accountId: string, requiredPlan: string) { const client = getSupabaseServerClient(); const api = createAccountsApi(client); const subscription = await api.getSubscription(accountId); if (!subscription) { return { hasAccess: false, reason: 'no_subscription' }; } if (subscription.status !== 'active' && subscription.status !== 'trialing') { return { hasAccess: false, reason: 'inactive_subscription' }; } const hasRequiredPlan = subscription.items.some( (item) => item.product_id === requiredPlan ); if (!hasRequiredPlan) { return { hasAccess: false, reason: 'wrong_plan' }; } return { hasAccess: true };}getCustomerId
Returns the billing provider customer ID for an account. Use this when integrating with Stripe, Paddle, or Lemon Squeezy APIs directly.
const customerId = await api.getCustomerId(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The account UUID |
Returns: string | null
Example: Redirect to billing portal
import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';import Stripe from 'stripe';const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);async function createBillingPortalSession(accountId: string) { const client = getSupabaseServerClient(); const api = createAccountsApi(client); const customerId = await api.getCustomerId(accountId); if (!customerId) { throw new Error('No billing customer found'); } const session = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`, }); return session.url;}getOrder
Returns one-time purchase order data for accounts using lifetime deals or credit-based billing.
const order = await api.getOrder(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The account UUID |
Returns:
{ id: string; account_id: string; billing_provider: 'stripe' | 'lemon-squeezy' | 'paddle'; status: 'pending' | 'completed' | 'refunded'; currency: string; total_amount: number; items: Array<{ product_id: string; variant_id: string; quantity: number; price_amount: number; }>;} | nullReal-world examples
Feature gating based on subscription
import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';type FeatureAccess = { allowed: boolean; reason?: string; upgradeUrl?: string;};export async function canAccessFeature( accountId: string, feature: 'ai_assistant' | 'export' | 'api_access'): Promise<FeatureAccess> { const client = getSupabaseServerClient(); const api = createAccountsApi(client); const subscription = await api.getSubscription(accountId); // No subscription means free tier if (!subscription) { const freeFeatures = ['export']; if (freeFeatures.includes(feature)) { return { allowed: true }; } return { allowed: false, reason: 'This feature requires a paid plan', upgradeUrl: '/pricing', }; } // Check if subscription is active const activeStatuses = ['active', 'trialing']; if (!activeStatuses.includes(subscription.status)) { return { allowed: false, reason: 'Your subscription is not active', upgradeUrl: '/settings/billing', }; } // Map features to required product IDs const featureRequirements: Record<string, string[]> = { ai_assistant: ['pro', 'enterprise'], export: ['starter', 'pro', 'enterprise'], api_access: ['enterprise'], }; const requiredProducts = featureRequirements[feature] || []; const userProducts = subscription.items.map((item) => item.product_id); const hasAccess = requiredProducts.some((p) => userProducts.includes(p)); if (!hasAccess) { return { allowed: false, reason: 'This feature requires a higher plan', upgradeUrl: '/pricing', }; } return { allowed: true };}Server Action with subscription check
'use server';import { z } from 'zod';import { enhanceAction } from '@kit/next/actions';import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';const GenerateReportSchema = z.object({ accountId: z.string().uuid(), reportType: z.enum(['summary', 'detailed', 'export']),});export const generateReport = enhanceAction( async (data, user) => { const client = getSupabaseServerClient(); const api = createAccountsApi(client); // Check subscription before expensive operation const subscription = await api.getSubscription(data.accountId); const isProUser = subscription?.items.some( (item) => item.product_id === 'pro' || item.product_id === 'enterprise' ); if (data.reportType === 'detailed' && !isProUser) { return { success: false, error: 'Detailed reports require a Pro subscription', }; } // Generate report... return { success: true, reportUrl: '/reports/123' }; }, { schema: GenerateReportSchema });Common pitfalls
Creating client at module scope
// WRONG: Client created at module scopeconst client = getSupabaseServerClient();const api = createAccountsApi(client);export async function handler() { const subscription = await api.getSubscription(accountId); // Won't work}// RIGHT: Client created in request contextexport async function handler() { const client = getSupabaseServerClient(); const api = createAccountsApi(client); const subscription = await api.getSubscription(accountId);}Forgetting to handle null subscriptions
// WRONG: Assumes subscription existsconst subscription = await api.getSubscription(accountId);const plan = subscription.items[0].product_id; // Crashes if null// RIGHT: Handle null caseconst subscription = await api.getSubscription(accountId);if (!subscription) { return { plan: 'free' };}const plan = subscription.items[0]?.product_id ?? 'free';Confusing account ID with user ID
The Account API expects account UUIDs, not user UUIDs. For personal accounts, the account ID is the same as the user ID, but for team accounts they differ.
Related documentation
- Team Account API - Team account management
- User Workspace API - Workspace context for layouts
- Billing Configuration - Stripe and payment setup