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>viarequireAuth()middleware- Podcast membership via
requirePodcastRole()middleware
| Endpoint | Minimum Role |
|---|---|
| GET (list) | member |
| POST invite | admin |
| PUT/DELETE members | admin |
| PUT members/:userId/auto-add (#293) | admin |
| POST resend / DELETE invitation | admin |
#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/:podcastIdReturns all current team members and pending invitations for a podcast.
Auth: requirePodcastRole('member')
Response 200:
{
"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/inviteCreates a team invitation and optionally sends an email notification.
Auth: requirePodcastRole('admin')
Request Body (JSON or form data):
{
"email": "[email protected]",
"role": "member",
"message": "Looking forward to working together!"
}| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email address |
role | 'member' | 'admin' | Yes | Role to assign (cannot invite as owner) |
message | string | No | Personal message included in email |
Response 201:
{
"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_profileslookup) - 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:
| Code | Condition |
|---|---|
400 | Invalid email, missing role, user already a member, duplicate invitation |
403 | Caller lacks admin role |
500 | Database error creating invitation |
Update Member Role
PUT /api/team/:podcastId/members/:userIdChanges a team member's role. Also available as POST /api/team/:podcastId/members/:userId/role for form compatibility.
Auth: requirePodcastRole('admin')
Request Body:
{
"role": "admin"
}| Field | Type | Required | Description |
|---|---|---|---|
role | 'member' | 'admin' | 'owner' | Yes | New 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
ownerrole - Admins can only modify
member-level users
Error Responses:
| Code | Condition |
|---|---|
400 | Invalid role value |
403 | Self-modification, insufficient permissions for target role |
404 | Target user not a member of this podcast |
Remove Team Member
DELETE /api/team/:podcastId/members/:userIdRemoves 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:
{
"success": true
}Resend Invitation
POST /api/team/:podcastId/invitations/:invitationId/resendResets the expiration date (7 days from now) and re-sends the invitation email.
Auth: requirePodcastRole('admin')
Response 200:
{
"success": true,
"expires_at": "2025-02-22T10:00:00Z"
}Cancel Invitation
DELETE /api/team/:podcastId/invitations/:invitationIdMarks an invitation as cancelled. Also available as POST /api/team/:podcastId/invitations/:invitationId/cancel for form compatibility.
Auth: requirePodcastRole('admin')
Response 200:
{
"success": true
}Database Tables
| Table | Purpose |
|---|---|
podcast_members | Current team membership and roles |
team_invitations | Pending, accepted, expired, and cancelled invitations |
user_profiles | User display names and avatars |
podcasts | Podcast metadata (title for emails) |
Views used for efficient listing:
team_members_with_profiles- Joinspodcast_memberswithuser_profilesteam_invitations_with_inviter_profiles- Joinsteam_invitationswith inviter'suser_profiles
Environment Bindings
| Binding | Purpose | Required |
|---|---|---|
PUBLIC_APP_URL | Base URL for invite links (default: https://app.podcasterplus.com) | Yes |
RESEND_API_KEY | Email service API key | No (graceful degradation) |
RESEND_FROM_EMAIL | Sender email address | No (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 Endpoint | Form Alias |
|---|---|
PUT /:podcastId/members/:userId | POST /:podcastId/members/:userId/role |
DELETE /:podcastId/members/:userId | POST /:podcastId/members/:userId/remove |
DELETE /:podcastId/invitations/:id | POST /: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
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 }
);Related Documentation
- Team Invitation Flow - End-to-end invitation lifecycle
- Multi-Tenancy Model - Role-based access control
- Resend Service - Email template details
- Hono API Overview - API architecture and patterns