Skip to content

Team API

Team member and invitation management endpoints for podcast collaboration.

Source: src/api/routes/team/index.ts

Overview

The Team API enables podcast owners and admins to invite collaborators, manage roles, and handle team membership. All endpoints are scoped to a specific podcast via the :podcastId path parameter.

Authentication

All endpoints require:

  • Authorization: Bearer <supabase_access_token> via requireAuth() middleware
  • Podcast membership via requirePodcastRole() middleware
EndpointMinimum Role
GET (list)member
POST inviteadmin
PUT/DELETE membersadmin
PUT members/:userId/auto-add (#293)admin
POST resend / DELETE invitationadmin

#293 notes: the member list now carries auto_add_to_new_episodes per member (merged from podcast_members, since the profiles view predates the column). PUT /:podcastId/members/:userId/auto-add ({ enabled: boolean }) toggles it with the role-change owner protection (only owners touch another owner's row) but self-toggle allowed. Invite creation also publishes the team.invitation in-app notification when the invitee email matches an existing account, and the accept action publishes team.invitation_accepted (in-app + email) to the inviter.

Endpoints

List Team Members & Invitations

GET /api/team/:podcastId

Returns all current team members and pending invitations for a podcast.

Auth: requirePodcastRole('member')

Response 200:

json
{
	"members": [
		{
			"user_id": "uuid",
			"email": "[email protected]",
			"full_name": "Jane Host",
			"avatar_url": "https://...",
			"role": "owner",
			"joined_at": "2025-01-15T10:00:00Z"
		}
	],
	"invitations": [
		{
			"id": "uuid",
			"email": "[email protected]",
			"role": "admin",
			"status": "pending",
			"message": "Welcome to the team!",
			"expires_at": "2025-02-22T10:00:00Z",
			"created_at": "2025-02-15T10:00:00Z",
			"invited_by": {
				"name": "Jane Host",
				"email": "[email protected]"
			}
		}
	]
}

Data Sources: Uses database views team_members_with_profiles and team_invitations_with_inviter_profiles for efficient joins.


Invite Team Member

POST /api/team/:podcastId/invite

Creates a team invitation and optionally sends an email notification.

Auth: requirePodcastRole('admin')

Request Body (JSON or form data):

json
{
	"email": "[email protected]",
	"role": "member",
	"message": "Looking forward to working together!"
}
FieldTypeRequiredDescription
emailstringYesValid email address
role'member' | 'admin'YesRole to assign (cannot invite as owner)
messagestringNoPersonal message included in email

Response 201:

json
{
	"success": true,
	"invitation_id": "uuid",
	"invite_url": "https://app.podcasterplus.com/invite/abc123token",
	"podcast_title": "The Tech Show"
}

Validation Rules:

  • Email must be valid format
  • Target user must not already be a podcast member (checked via user_profiles lookup)
  • No duplicate pending invitation for the same email + podcast (enforced by unique constraint)

Email Sending: The invitation email is sent non-blocking via sendTeamInvitationEmail(). If email delivery fails (e.g., missing Resend config), the API still returns success. See Resend Service for template details.

Error Responses:

CodeCondition
400Invalid email, missing role, user already a member, duplicate invitation
403Caller lacks admin role
500Database error creating invitation

Update Member Role

PUT /api/team/:podcastId/members/:userId

Changes a team member's role. Also available as POST /api/team/:podcastId/members/:userId/role for form compatibility.

Auth: requirePodcastRole('admin')

Request Body:

json
{
	"role": "admin"
}
FieldTypeRequiredDescription
role'member' | 'admin' | 'owner'YesNew role to assign

Safety Rules:

  • Cannot modify your own role (prevents accidental self-demotion)
  • At least one owner must remain at all times
  • Only owners can assign or revoke the owner role
  • Admins can only modify member-level users

Error Responses:

CodeCondition
400Invalid role value
403Self-modification, insufficient permissions for target role
404Target user not a member of this podcast

Remove Team Member

DELETE /api/team/:podcastId/members/:userId

Removes a member from the podcast team. Also available as POST /api/team/:podcastId/members/:userId/remove for form compatibility.

Auth: requirePodcastRole('admin')

Safety Rules:

  • Cannot remove yourself (use a "leave" endpoint instead)
  • At least one owner must remain
  • Only owners can remove other admins or owners

Response 200:

json
{
	"success": true
}

Resend Invitation

POST /api/team/:podcastId/invitations/:invitationId/resend

Resets the expiration date (7 days from now) and re-sends the invitation email.

Auth: requirePodcastRole('admin')

Response 200:

json
{
	"success": true,
	"expires_at": "2025-02-22T10:00:00Z"
}

Cancel Invitation

DELETE /api/team/:podcastId/invitations/:invitationId

Marks an invitation as cancelled. Also available as POST /api/team/:podcastId/invitations/:invitationId/cancel for form compatibility.

Auth: requirePodcastRole('admin')

Response 200:

json
{
	"success": true
}

Database Tables

TablePurpose
podcast_membersCurrent team membership and roles
team_invitationsPending, accepted, expired, and cancelled invitations
user_profilesUser display names and avatars
podcastsPodcast metadata (title for emails)

Views used for efficient listing:

  • team_members_with_profiles - Joins podcast_members with user_profiles
  • team_invitations_with_inviter_profiles - Joins team_invitations with inviter's user_profiles

Environment Bindings

BindingPurposeRequired
PUBLIC_APP_URLBase URL for invite links (default: https://app.podcasterplus.com)Yes
RESEND_API_KEYEmail service API keyNo (graceful degradation)
RESEND_FROM_EMAILSender email addressNo (graceful degradation)

When Resend config is missing, the API operates normally but skips email sending.

Form-Friendly Aliases

Several endpoints have POST aliases for compatibility with HTML forms and SvelteKit form actions:

REST EndpointForm Alias
PUT /:podcastId/members/:userIdPOST /:podcastId/members/:userId/role
DELETE /:podcastId/members/:userIdPOST /:podcastId/members/:userId/remove
DELETE /:podcastId/invitations/:idPOST /:podcastId/invitations/:id/cancel

The route handler uses parseBody() to accept both application/json and application/x-www-form-urlencoded content types.

RPC Client Usage

typescript
import { createApiClient } from '$api/client';

const client = createApiClient(fetch);
const token = session.access_token;
const headers = { Authorization: `Bearer ${token}` };

// List team
const res = await client.api.team[':podcastId'].$get({ param: { podcastId } }, { headers });

// Invite member
const res = await client.api.team[':podcastId'].invite.$post(
	{
		param: { podcastId },
		json: { email: '[email protected]', role: 'member' }
	},
	{ headers }
);

Internal documentation - Not for public distribution