Team Account API | Next.js Supabase SaaS Kit
Complete reference for the Team Account API in MakerKit. Manage teams, members, permissions, invitations, subscriptions, and workspace data with type-safe methods.
The Team Account API manages team accounts, members, permissions, and invitations. Use it to check user permissions, manage team subscriptions, and handle team invitations in your multi-tenant SaaS application.
Team Account API Reference
Learn how to use the Team Account API in MakerKit
Setup and initialization
Import createTeamAccountsApi from @kit/team-accounts/api and pass a Supabase server client.
import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function ServerComponent() { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); // Use API methods}In Server Actions:
'use server';import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';export async function myServerAction() { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(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 and RLS policies.
API Methods
getTeamAccountById
Retrieves a team account by its UUID. Also verifies the current user has access to the team.
const account = await api.getTeamAccountById(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The team account UUID |
Returns:
{ id: string; name: string; slug: string; picture_url: string | null; public_data: Json | null; primary_owner_user_id: string; created_at: string; updated_at: string;} | nullUsage notes:
- Returns
nullif the account doesn't exist or user lacks access - RLS policies ensure users only see teams they belong to
- Use this to verify team membership before operations
getAccountWorkspace
Returns the team workspace data for a given team slug. This is the primary method for loading team context in layouts.
const workspace = await api.getAccountWorkspace(slug);Parameters:
| Parameter | Type | Description |
|---|---|---|
slug | string | The team URL slug |
Returns:
{ account: { id: string; name: string; slug: string; picture_url: string | null; role: string; role_hierarchy_level: number; primary_owner_user_id: string; subscription_status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'incomplete' | 'incomplete_expired' | 'paused' | null; permissions: string[]; }; accounts: Array<{ id: string; name: string; slug: string; picture_url: string | null; role: string; }>;}Usage notes:
- Called automatically in the
/home/[account]layout - The
permissionsarray contains all permissions for the current user in this team - Use
role_hierarchy_levelfor role-based comparisons (lower = more permissions)
getSubscription
Returns the subscription data for a team account, including all line items.
const subscription = await api.getSubscription(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The team 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 per-seat limits
import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function canAddTeamMember(accountId: string) { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); const [subscription, membersCount] = await Promise.all([ api.getSubscription(accountId), api.getMembersCount(accountId), ]); if (!subscription) { // Free tier: allow up to 3 members return membersCount < 3; } const perSeatItem = subscription.items.find((item) => item.type === 'per_seat'); if (perSeatItem) { return membersCount < perSeatItem.quantity; } // Flat-rate plan: no seat limit return true;}getOrder
Returns one-time purchase order data for team accounts using lifetime deals.
const order = await api.getOrder(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The team 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; }>;} | nullhasPermission
Checks if a user has a specific permission within a team account. Use this for fine-grained authorization checks.
const canManage = await api.hasPermission({ accountId: 'team-uuid', userId: 'user-uuid', permission: 'billing.manage',});Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The team account UUID |
userId | string | The user UUID to check |
permission | string | The permission identifier |
Returns: boolean
Built-in permissions:
| Permission | Description |
|---|---|
billing.manage | Manage subscription and payment methods |
members.invite | Invite new team members |
members.remove | Remove team members |
members.manage | Update member roles |
settings.manage | Update team settings |
Example: Permission-gated Server Action
'use server';import { z } from 'zod';import { enhanceAction } from '@kit/next/actions';import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';const UpdateTeamSchema = z.object({ accountId: z.string().uuid(), name: z.string().min(2).max(50),});export const updateTeamSettings = enhanceAction( async (data, user) => { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); const canManage = await api.hasPermission({ accountId: data.accountId, userId: user.id, permission: 'settings.manage', }); if (!canManage) { return { success: false, error: 'You do not have permission to update team settings', }; } // Update team... return { success: true }; }, { schema: UpdateTeamSchema });getMembersCount
Returns the total number of members in a team account.
const count = await api.getMembersCount(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The team account UUID |
Returns: number | null
Example: Display team size
import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function TeamStats({ accountId }: { accountId: string }) { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); const membersCount = await api.getMembersCount(accountId); return ( <div> <span className="font-medium">{membersCount}</span> <span className="text-muted-foreground"> team members</span> </div> );}getCustomerId
Returns the billing provider customer ID for a team account.
const customerId = await api.getCustomerId(accountId);Parameters:
| Parameter | Type | Description |
|---|---|---|
accountId | string | The team account UUID |
Returns: string | null
getInvitation
Retrieves invitation data from an invite token. Requires an admin client to bypass RLS for pending invitations.
const invitation = await api.getInvitation(adminClient, token);Parameters:
| Parameter | Type | Description |
|---|---|---|
adminClient | SupabaseClient | Admin client (bypasses RLS) |
token | string | The invitation token |
Returns:
{ id: number; email: string; account: { id: string; name: string; slug: string; }; role: string; expires_at: string;} | nullExample: Accept invitation flow
import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';async function getInvitationDetails(token: string) { const client = getSupabaseServerClient(); const adminClient = getSupabaseServerAdminClient(); const api = createTeamAccountsApi(client); const invitation = await api.getInvitation(adminClient, token); if (!invitation) { return { error: 'Invalid or expired invitation' }; } const now = new Date(); const expiresAt = new Date(invitation.expires_at); if (now > expiresAt) { return { error: 'This invitation has expired' }; } return { teamName: invitation.account.name, role: invitation.role, email: invitation.email, };}The admin client bypasses Row Level Security. Only use it for operations that require elevated privileges, and always validate authorization separately.
Real-world examples
Complete team management Server Actions
// lib/server/team-actions.ts'use server';import { z } from 'zod';import { revalidatePath } from 'next/cache';import { enhanceAction } from '@kit/next/actions';import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';const InviteMemberSchema = z.object({ accountId: z.string().uuid(), email: z.string().email(), role: z.enum(['admin', 'member']),});const RemoveMemberSchema = z.object({ accountId: z.string().uuid(), userId: z.string().uuid(),});export const inviteMember = enhanceAction( async (data, user) => { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); // Check permission const canInvite = await api.hasPermission({ accountId: data.accountId, userId: user.id, permission: 'members.invite', }); if (!canInvite) { return { success: false, error: 'Permission denied' }; } // Check seat limits const [subscription, membersCount] = await Promise.all([ api.getSubscription(data.accountId), api.getMembersCount(data.accountId), ]); if (subscription) { const perSeatItem = subscription.items.find((i) => i.type === 'per_seat'); if (perSeatItem && membersCount >= perSeatItem.quantity) { return { success: false, error: 'Team has reached maximum seats. Please upgrade your plan.', }; } } // Create invitation... const { error } = await client.from('invitations').insert({ account_id: data.accountId, email: data.email, role: data.role, invited_by: user.id, }); if (error) { return { success: false, error: 'Failed to send invitation' }; } revalidatePath(`/home/[account]/settings/members`, 'page'); return { success: true }; }, { schema: InviteMemberSchema });export const removeMember = enhanceAction( async (data, user) => { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); // Cannot remove yourself if (data.userId === user.id) { return { success: false, error: 'You cannot remove yourself' }; } // Check permission const canRemove = await api.hasPermission({ accountId: data.accountId, userId: user.id, permission: 'members.remove', }); if (!canRemove) { return { success: false, error: 'Permission denied' }; } // Check if target is owner const account = await api.getTeamAccountById(data.accountId); if (account?.primary_owner_user_id === data.userId) { return { success: false, error: 'Cannot remove the team owner' }; } // Remove member... const { error } = await client .from('accounts_memberships') .delete() .eq('account_id', data.accountId) .eq('user_id', data.userId); if (error) { return { success: false, error: 'Failed to remove member' }; } revalidatePath(`/home/[account]/settings/members`, 'page'); return { success: true }; }, { schema: RemoveMemberSchema });Permission-based UI rendering
import { createTeamAccountsApi } from '@kit/team-accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { requireUser } from '@kit/supabase/require-user';import { redirect } from 'next/navigation';async function TeamSettingsPage({ params }: { params: { account: string } }) { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { redirect(auth.redirectTo); } const api = createTeamAccountsApi(client); const workspace = await api.getAccountWorkspace(params.account); const permissions = { canManageSettings: workspace.account.permissions.includes('settings.manage'), canManageBilling: workspace.account.permissions.includes('billing.manage'), canInviteMembers: workspace.account.permissions.includes('members.invite'), canRemoveMembers: workspace.account.permissions.includes('members.remove'), }; return ( <div> <h1>Team Settings</h1> {permissions.canManageSettings && ( <section> <h2>General Settings</h2> {/* Settings form */} </section> )} {permissions.canManageBilling && ( <section> <h2>Billing</h2> {/* Billing management */} </section> )} {permissions.canInviteMembers && ( <section> <h2>Invite Members</h2> {/* Invitation form */} </section> )} {!permissions.canManageSettings && !permissions.canManageBilling && !permissions.canInviteMembers && ( <p>You don't have permission to manage this team.</p> )} </div> );}Related documentation
- Account API - Personal account management
- Team Workspace API - Workspace context for layouts
- Policies API - Business rule validation
- Per-seat Billing - Team-based pricing