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.

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:

ParameterTypeDescription
accountIdstringThe 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;
} | null

Usage notes:

  • Returns null if 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:

ParameterTypeDescription
slugstringThe 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 permissions array contains all permissions for the current user in this team
  • Use role_hierarchy_level for 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:

ParameterTypeDescription
accountIdstringThe 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;
}>;
} | null

Example: 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:

ParameterTypeDescription
accountIdstringThe 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;
}>;
} | null

hasPermission

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:

ParameterTypeDescription
accountIdstringThe team account UUID
userIdstringThe user UUID to check
permissionstringThe permission identifier

Returns: boolean

Built-in permissions:

PermissionDescription
billing.manageManage subscription and payment methods
members.inviteInvite new team members
members.removeRemove team members
members.manageUpdate member roles
settings.manageUpdate 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:

ParameterTypeDescription
accountIdstringThe 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:

ParameterTypeDescription
accountIdstringThe 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:

ParameterTypeDescription
adminClientSupabaseClientAdmin client (bypasses RLS)
tokenstringThe invitation token

Returns:

{
id: number;
email: string;
account: {
id: string;
name: string;
slug: string;
};
role: string;
expires_at: string;
} | null

Example: 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>
);
}