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
- Copy
teamspace.tsfrom the block detail page and paste it intolib/teamspace.tsin your project. - Install peer dependencies:
npm install @supabase/supabase-js zod4. Environment
| Variable | Required | Default | Purpose |
|---|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | yes | — | Your Supabase project URL |
| SUPABASE_SERVICE_ROLE_KEY | yes | — | Service 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
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
- Create a workspace:
createWorkspace({ name: 'test', ownerId: userId }). Confirm a row appears inworkspaces. - Invite a user:
createInvite({ workspaceId, actorId, email, role: 'member' }). Confirm aninvitesrow and an invite email. - Accept the invite with the token. Confirm the user appears in
members. - Check a permission:
can(memberId, workspaceId, 'invite_member')— must returntruefor admin/owner andfalsefor 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_memberspermission (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.