The current team accounts implementation uses a dual-authentication architecture built by a previous developer. This document outlines the technical disadvantages of continuing with this approach versus migrating to a unified system.
The legacy approach maintains two separate auth systems:
team_userstable: Custom auth outside Supabase Auth with manually hashed passwords and session management viateam_user_sessions.team_sub_accountstable: Real Supabase Auth users tied as "children" of a parent user.
This means every security concern (token refresh, password reset, session expiry, brute-force protection) must be handled in two places. Supabase Auth provides these out of the box, but the custom team_users system reimplements them manually.
Risk: Any security vulnerability in the custom auth layer (e.g., weak hashing, session fixation, missing rate limiting) is a liability that Supabase Auth already solves.
Two login pages exist:
/get-startedβ Regular users (Supabase Auth)/team-login(TeamLogin.tsx) β Team users (custom auth)
Users must know which login to use. This doubles the login surface area for bugs, UX confusion, and support requests. A single login page that works for everyone is simpler for users and developers.
Four separate permission tables exist for per-user-per-resource granularity:
| Table | Scope |
|---|---|
team_user_platform_access |
Per platform: can_create_campaigns, can_manage_creatives, can_view_insights |
team_user_mapping_access |
Per account_mapping: read / write / admin |
team_sub_account_platform_access |
Same as above, but for sub-accounts |
team_sub_account_mapping_access |
Same as above, but for sub-accounts |
The product brief only requires 4 predefined roles (Admin, Manager, Contributor, Read-Only). The legacy system solves a much more complex problem than what is actually needed, adding maintenance burden without product value.
Impact: Every permission check requires querying multiple tables with joins. A single team_members.role field checked against a permission matrix achieves the same result with one query.
Team members can exist in three different tables:
| Table | User Type |
|---|---|
team_members |
Supabase Auth users |
team_users |
Custom auth users (password-hashed, session-based) |
team_sub_accounts |
Child Supabase Auth users (parent-child relationship) |
Querying "who is on this team" requires checking all three tables. Member counts, display lists, permission checks, and team management operations all need UNION queries across multiple tables. This creates inconsistency and makes bugs harder to trace.
The teams table stores shared account mapping IDs as a JSONB array (account_mapping_ids):
-- Current approach
teams.account_mapping_ids = '["a49b50ad-...", "b12c34de-..."]'::jsonbProblems with this approach:
| Issue | Impact |
|---|---|
| No referential integrity | Array can reference deleted account_mappings with no error |
| No index support | Full table scan with unnest() on every query |
| Manual sync required | Adding/removing mappings requires updating the array manually |
| Cannot write RLS policies | PostgreSQL RLS cannot efficiently filter based on JSONB array contents |
| No cascade behavior | Deleting an account_mapping does not remove it from the array |
The alternative (a team_id FK column on account_mappings) provides proper foreign keys, B-tree index support, standard JOIN performance, and clean RLS policies.
The team_users and team_invitations tables have permissive "allow all" RLS policies left from development:
-- Current state: any authenticated user can read/modify ANY team's data
CREATE POLICY "allow_all" ON team_users FOR ALL USING (true);
CREATE POLICY "allow_all" ON team_invitations FOR ALL USING (true);This means any authenticated user in the system can:
- Read all team invitations across all teams
- Modify team membership records they do not belong to
- View sensitive team configuration data
Shipping with these policies is a data isolation failure. Proper RLS requires team membership and role checks.
The TeamLogin.tsx page shows signs of incomplete implementation:
- Fallback chains: Try edge function, fall back to RPC, fall back to direct query β indicates unstable API contracts
- Debug logging left in: Console statements with emoji markers (
π,β,β) in production code - Type bypassing:
(supabase as any).rpc(...)casting to avoid TypeScript errors rather than fixing the type definitions - No error recovery: If the invitation acceptance fails after login succeeds, the user is in a broken state (logged in but not on the team)
These patterns suggest the implementation was still in prototype/exploration phase, not production-ready.
Every new feature must work for three user types:
| User Type | Auth Method | Session Management | Token Storage |
|---|---|---|---|
| Regular users | Supabase Auth | Supabase managed | Supabase JWT |
| Team users | Custom password hash | team_user_sessions table |
Custom session token |
| Sub-account users | Supabase Auth (child) | Supabase managed | Supabase JWT |
This triples testing, edge cases, and maintenance for every feature shipped. Each new page, edge function, or query must account for all three user types and their different authentication patterns.
The legacy architecture does not have a migration path to the product brief's requirements:
- Custom auth users (
team_users) cannot use Supabase Auth features: password reset, email verification, OAuth providers, session management β all unavailable. - Sub-accounts create real Supabase Auth users but with no team awareness: The parent-child relationship is tracked in a separate table, not in the auth system itself.
- Role mapping is impossible: The granular per-resource permissions (4 tables) do not map cleanly to the 4 predefined roles (Admin, Manager, Contributor, Read-Only) from the product brief.
| Aspect | Legacy Approach | New Approach |
|---|---|---|
| Auth systems | 2 (Supabase + custom) | 1 (Supabase only) |
| Login pages | 2 (/get-started + /team-login) |
1 (/get-started) |
| Permission tables | 4 (per-user-per-resource) | 1 (team_members.role) |
| Membership tables | 3 (team_members + team_users + team_sub_accounts) |
1 (team_members) |
| Account mapping linking | JSONB array (no FK, no index, no RLS) | team_id FK (proper constraints, indexed, RLS-compatible) |
| RLS policies | Allow all (insecure) | Role-based policies per table |
| New feature cost | 3x (three user types) | 1x (one user type) |
| Total team-related tables | 10+ | 3 (teams, team_members, team_invitations) |
| Permission query complexity | Multi-table JOINs across 4 permission tables | Single lookup: team_members.role |
| Security posture | Custom auth + open RLS | Supabase Auth + proper RLS |
| Table | Reason for Deprecation |
|---|---|
team_users |
Custom auth replaced by Supabase Auth via team_members |
team_user_sessions |
Session management goes with team_users |
team_sub_accounts |
Parent-child model replaced by flat team_members |
team_sub_account_mapping_access |
Per-resource permissions replaced by role-based RLS |
team_sub_account_platform_access |
Per-platform permissions replaced by role-permission matrix |
team_user_platform_access |
Goes with team_users |
team_user_mapping_access |
Goes with team_users |
organizations |
Table exists but was never used in the codebase |
org_members |
Table exists but was never used in the codebase |
| File / Route | Reason |
|---|---|
src/pages/TeamLogin.tsx |
Separate team login no longer needed β single login at /get-started |
/team-login route |
Remove from App.tsx |