Phase 4 backend: teams data layer + membership management
CI / backend (push) Failing after 1m6s
CI / policy (push) Successful in 4s
CI / profile (push) Successful in 12s
CI / mobile (push) Successful in 17s

- Migration 0003: teams + team_memberships (owner/admin/member, seat_limit)
- POST /v1/teams (Team-tier gated), GET /v1/teams/{id},
  GET|POST /v1/teams/{id}/members, DELETE …/members/{userId}
- Add-by-email (existing accounts), seat-cap enforcement (402), role-based
  authorization (admin to mutate; non-members get 404; owner unremovable)
- Invite notification email; Tier::can_own_team()

100 backend tests; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 15:13:03 -05:00
co-authored by Claude Opus 4.8
parent 4ad3220917
commit 2b8e23dd3f
13 changed files with 647 additions and 2 deletions
@@ -0,0 +1,23 @@
-- 0003_teams.sql — Team tier data layer (PRD §21 Phase 4).
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- NULL = unlimited seats; otherwise the billed seat cap.
seat_limit INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_teams_owner ON teams(owner_id);
CREATE TABLE team_memberships (
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member'
CHECK (role IN ('owner', 'admin', 'member')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (team_id, user_id)
);
CREATE INDEX idx_team_memberships_user ON team_memberships(user_id);