# Team Account API

> Complete reference for the Team Account API in MakerKit. Manage teams, members, permissions, invitations, subscriptions, and workspace data with type-safe methods.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/api/team-account-api*

---

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.

{% sequence title="Team Account API Reference" description="Learn how to use the Team Account API in MakerKit" %}

[Setup and initialization](#setup-and-initialization)

[getTeamAccountById](#getteamaccountbyid)

[getAccountWorkspace](#getaccountworkspace)

[getSubscription](#getsubscription)

[getOrder](#getorder)

[hasPermission](#haspermission)

[getMembersCount](#getmemberscount)

[getCustomerId](#getcustomerid)

[getInvitation](#getinvitation)

[Real-world examples](#real-world-examples)

{% /sequence %}

## Setup and initialization

Import `createTeamAccountsApi` from `@kit/team-accounts/api` and pass a Supabase server client.

```tsx
import { createServerFn } from '@tanstack/react-start';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

export const loadTeamDataFunction = createServerFn({ method: 'GET' }).handler(
  async () => {
    const client = getSupabaseServerClient();
    const api = createTeamAccountsApi(client);

    // Use API methods
  },
);
```

In server functions that mutate data:

```tsx
import { createServerFn } from '@tanstack/react-start';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

export const myServerFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .handler(async ({ context: { user } }) => {
    const client = getSupabaseServerClient();
    const api = createTeamAccountsApi(client);

    // Use API methods
  });
```

{% callout title="Request-scoped clients" %}
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.
{% /callout %}

## API Methods

### getTeamAccountById

Retrieves a team account by its UUID. Also verifies the current user has access to the team.

```tsx
const account = await api.getTeamAccountById(accountId);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `accountId` | `string` | The team account UUID |

**Returns:**

```tsx
{
  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.

```tsx
const workspace = await api.getAccountWorkspace(slug);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `slug` | `string` | The team URL slug |

**Returns:**

```tsx
{
  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 `_authenticated` 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.

```tsx
const subscription = await api.getSubscription(accountId);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `accountId` | `string` | The team account UUID |

**Returns:**

```tsx
{
  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**

```tsx
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.

```tsx
const order = await api.getOrder(accountId);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `accountId` | `string` | The team account UUID |

**Returns:**

```tsx
{
  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.

```tsx
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 function**

```tsx
import * as z from 'zod';
import { createServerFn } from '@tanstack/react-start';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

const UpdateTeamSchema = z.object({
  accountId: z.uuid(),
  name: z.string().min(2).max(50),
});

export const updateTeamSettingsFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(UpdateTeamSchema)
  .handler(async ({ data, context: { 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 };
  });
```

---

### getMembersCount

Returns the total number of members in a team account.

```tsx
const count = await api.getMembersCount(accountId);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `accountId` | `string` | The team account UUID |

**Returns:** `number | null`

**Example: Display team size**

```tsx
import { createServerFn } from '@tanstack/react-start';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

const getMembersCountFunction = createServerFn({ method: 'GET' })
  .validator((accountId: string) => accountId)
  .handler(async ({ data: accountId }) => {
    const client = getSupabaseServerClient();
    const api = createTeamAccountsApi(client);

    return api.getMembersCount(accountId);
  });

// Call it from a route loader, then read the result via Route.useLoaderData()
function TeamStats({ membersCount }: { membersCount: number | null }) {
  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.

```tsx
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.

```tsx
const invitation = await api.getInvitation(adminClient, token);
```

**Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `adminClient` | `SupabaseClient` | Admin client (bypasses RLS) |
| `token` | `string` | The invitation token |

**Returns:**

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

**Example: Accept invitation flow**

```tsx
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,
  };
}
```

{% callout type="warning" title="Admin client security" %}
The admin client bypasses Row Level Security. Only use it for operations that require elevated privileges, and always validate authorization separately.
{% /callout %}

---

## Real-world examples

### Complete team management server functions

```tsx
// lib/server/team.functions.ts
import * as z from 'zod';
import { createServerFn } from '@tanstack/react-start';
import { authFunctionMiddleware } from '@kit/function-middleware/functions';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

const InviteMemberSchema = z.object({
  accountId: z.uuid(),
  email: z.email(),
  role: z.enum(['admin', 'member']),
});

const RemoveMemberSchema = z.object({
  accountId: z.uuid(),
  userId: z.uuid(),
});

export const inviteMemberFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(InviteMemberSchema)
  .handler(async ({ data, context: { 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' };
    }

    // The client refetches after the mutation via router.invalidate()
    return { success: true };
  });

export const removeMemberFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .validator(RemoveMemberSchema)
  .handler(async ({ data, context: { 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' };
    }

    // The client refetches after the mutation via router.invalidate()
    return { success: true };
  });
```

Call these from a client component with `useServerFn` and TanStack Query's
`useMutation`, then call `router.invalidate()` in `onSuccess` to refetch the
members list.

### Permission-based UI rendering

```tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { requireUser } from '@kit/supabase/require-user';

const loadTeamSettingsFunction = createServerFn({ method: 'GET' })
  .validator((account: string) => account)
  .handler(async ({ data: account }) => {
    const client = getSupabaseServerClient();
    const auth = await requireUser(client);

    if (auth.error) {
      throw redirect({ href: auth.redirectTo });
    }

    const api = createTeamAccountsApi(client);

    return api.getAccountWorkspace(account);
  });

export const Route = createFileRoute('/_authenticated/settings')({
  loader: ({ params }) => loadTeamSettingsFunction({ data: params.account }),
  component: TeamSettingsPage,
});

function TeamSettingsPage() {
  const workspace = Route.useLoaderData();

  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](/docs/tanstack-supabase/api/account-api) - Personal account management
- [Workspace API](/docs/tanstack-supabase/api/workspace-api) - Workspace context for layouts
- [Policies API](/docs/tanstack-supabase/api/policies-api) - Business rule validation
- [Per-seat Billing](/docs/tanstack-supabase/billing/per-seat-billing) - Team-based pricing
