Teams & Organizations

indiegoon includes a complete multi-tenant team system. Users can create teams, invite members, assign roles, and manage permissions.

Features

  • Create teams with auto-generated slugs
  • Three roles: Owner, Admin, Member
  • Email invitations with 7-day expiry tokens
  • Role-based access control — owners and admins can manage, members can view
  • Team switching — users can belong to multiple teams
  • Team deletion — cascade deletes all members and invitations

Database Schema

Three tables power the team system:

// teams — the workspace itself
{ id, name, slug, logo, createdAt, updatedAt }

// teamMembers — who belongs to what team
{ id, teamId, userId, role, createdAt, updatedAt }

// teamInvitations — pending invites
{ id, teamId, email, role, token, expiresAt, acceptedAt, createdAt }

Using Teams in Your Code

Create a Team

import { createTeam } from "@/lib/teams"

const team = await createTeam("Acme Corp", userId)
// → { id: "...", name: "Acme Corp", slug: "acme-corp", ... }
// Creator is automatically added as "owner"

Get User's Teams

import { getUserTeams } from "@/lib/teams"

const teams = await getUserTeams(userId)
// → [{ team: {...}, role: "owner" }, { team: {...}, role: "member" }]

Check Permissions

import { canManageTeam, getTeamRole } from "@/lib/teams"

// Can this user edit team settings?
const canEdit = await canManageTeam(teamId, userId)
// true for owners and admins, false for members

// Get specific role
const role = await getTeamRole(teamId, userId)
// "owner" | "admin" | "member" | null

Invite a Member

import { inviteToTeam } from "@/lib/teams"

const invitation = await inviteToTeam(teamId, "new@member.com", "member")
// Creates a token, valid for 7 days
// You'd send an email with a link: /teams/invite?token=...

Accept an Invitation

import { acceptInvitation } from "@/lib/teams"

await acceptInvitation(token, userId)
// Validates token, checks expiry, adds user to team

Manage Members

import { updateMemberRole, removeTeamMember } from "@/lib/teams"

// Promote to admin
await updateMemberRole(teamId, memberId, "admin")

// Remove from team
await removeTeamMember(teamId, memberId)

API Routes

MethodEndpointPermissionAction
GET/api/teamsAuthenticatedList user's teams
POST/api/teamsAuthenticatedCreate team
GET/api/teams/[teamId]MemberGet team details
PATCH/api/teams/[teamId]Admin/OwnerUpdate team
DELETE/api/teams/[teamId]OwnerDelete team
POST/api/teams/[teamId]/inviteAdmin/OwnerSend invitation

Pre-Built Pages

The template includes these team pages:

  • /teams — List all your teams, create new ones
  • /teams/new — Create team form
  • /teams/[teamSlug] — Team overview
  • /teams/[teamSlug]/members — View and manage members
  • /teams/[teamSlug]/settings — Team settings (name, logo, danger zone)

Adding Team-Scoped Data

Want projects, documents, or other resources scoped to a team? Add a teamId foreign key:

// src/lib/db/schema/projects.ts
export const projects = pgTable("projects", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  teamId: text("team_id").references(() => teams.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").defaultNow(),
})

Then query by team:

const teamProjects = await db.query.projects.findMany({
  where: eq(projects.teamId, teamId),
})

Role-Based UI

Show/hide UI elements based on role:

"use client"

export function TeamSettings({ teamId, userRole }) {
  if (userRole === "member") {
    return <p>You don't have permission to manage settings.</p>
  }

  return (
    <form>
      {/* Admin + Owner can see this */}
      <input name="name" />
      
      {/* Only Owner can delete */}
      {userRole === "owner" && (
        <button className="text-red-500">Delete Team</button>
      )}
    </form>
  )
}

Next Steps