Integration guide

Team Workspace

Multi-tenant workspaces: role hierarchy (owner/admin/member/viewer), invite flows with expiring tokens, a composable permission matrix, and an ORM-agnostic adapter pattern that works with Prisma, Drizzle, or raw Supabase.

Read the Getting Access guideif you haven't yet.

1. Get the file

Sign in at marrowstack.dev, open the Team Workspace block, and click Copy all files. Paste lib/teamspace.ts (~440 lines) into your project. The SQL migration is embedded in the file. Mount the exported functions in your own API routes.

2. Prerequisites

  • Next.js 14 or 15, App Router
  • Supabase project (uses the same profiles table as the Auth block, or any users table)
  • Auth session already implemented (to identify the acting user)

3. Install

  1. Copy teamspace.ts from the block detail page and paste it into lib/teamspace.ts in your project.
  2. Install peer dependencies:
bash
npm install @supabase/supabase-js zod

4. Environment

VariableRequiredDefaultPurpose
NEXT_PUBLIC_SUPABASE_URLyesYour Supabase project URL
SUPABASE_SERVICE_ROLE_KEYyesService role key — server-side only

5. Database

Run the MIGRATION constant from teamspace.ts in Supabase SQL Editor. Creates: workspaces, members,invites tables with RLS. Invite tokens expire after 48 hours (configurable in the file).

6. Wire it in

app/api/workspace/invite/route.ts
import { createInvite, acceptInvite } from '@/lib/teamspace'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { NextRequest, NextResponse } from 'next/server'

// POST /api/workspace/invite  — actor must be owner or admin
export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions)
  if (!session?.user?.id) return new NextResponse('Unauthorized', { status: 401 })
  const { workspaceId, email, role } = await req.json()
  const invite = await createInvite({ workspaceId, actorId: session.user.id, email, role })
  return NextResponse.json(invite)
}

// PATCH /api/workspace/invite  — called with the invite token from the email link
export async function PATCH(req: NextRequest) {
  const session = await getServerSession(authOptions)
  if (!session?.user?.id) return new NextResponse('Unauthorized', { status: 401 })
  const { token } = await req.json()
  await acceptInvite({ token, userId: session.user.id })
  return NextResponse.json({ ok: true })
}

7. Verify it works

  1. Create a workspace: createWorkspace({ name: 'test', ownerId: userId }). Confirm a row appears in workspaces.
  2. Invite a user: createInvite({ workspaceId, actorId, email, role: 'member' }). Confirm an invites row and an invite email.
  3. Accept the invite with the token. Confirm the user appears in members.
  4. Check a permission: can(memberId, workspaceId, 'invite_member') — must return true for admin/owner and false for viewer.

8. Failure modes & fixes

InviteExpiredError

Cause: The invite token is older than the configured TTL (48 hours by default).

Fix: Re-send the invite. The TTL is INVITE_TTL_HOURS at the top of teamspace.ts — adjust if needed.

PermissionDeniedError: insufficient role

Cause: The acting user's role does not have the required permission in the PERMISSION_MATRIX.

Fix: Check the acting user's role in the members table. Adjust the PERMISSION_MATRIX in teamspace.ts if your app needs different permissions.

WorkspaceNotFoundError

Cause: The workspace ID is wrong, or the workspace was deleted.

Fix: Verify the workspace ID. Confirm the workspaces row exists in Supabase.

relation 'members' does not exist

Cause: The migration has not been run.

Fix: Run the MIGRATION constant in Supabase SQL Editor.

9. Security responsibilities

What this block handles:

  • All permission checks go through the typed can() function — no ad-hoc role comparisons.
  • Invite tokens are single-use, 48-hour TTL, cryptographically random.
  • Role changes require manage_members permission (owner/admin only).
  • RLS blocks client-side access to all workspace tables.

What you must ensure:

  • Call can() in every API route before performing privileged workspace operations.
  • Verify the acting user is authenticated before any workspace API call.
  • Deliver invite tokens over email only, never via URL query params visible in logs.