# Client Magic Link Auth Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace `profiles.is_client` and the Supabase-auth-based client portal with a backend-owned client identity system using email magic links, stateless JWT session cookies, and API-backed authorization.

**Architecture:** Introduce `public.clients`, `public.client_access`, and `public.client_magic_links` as the new client auth model. The API will own magic-link issuance, one-time exchange, cookie verification, and client-scoped resource authorization; `apps/clients` will stop using Supabase Auth and direct browser Supabase reads, and will instead rely on backend APIs with `withCredentials` cookies.

**Tech Stack:** Supabase Postgres, Hono API, JWT + HttpOnly cookies, React 19, React Router, TanStack Query, Axios, Vitest, pnpm workspaces.

**Spec:** `docs/superpowers/specs/2026-04-30-client-magic-link-auth-design.md`

---

## File Structure

### New files

**Database**
- `supabase/migrations/20260501100000_create_client_auth_tables.sql` — creates `clients`, `client_access`, and `client_magic_links`
- `supabase/migrations/20260501150000_drop_profiles_is_client.sql` — removes `profiles.is_client` after code migration is complete

**API helpers and tests**
- `apps/api/src/helpers/clientAccounts.ts` — normalize email, upsert client identities, manage access grants
- `apps/api/src/helpers/clientSessions.ts` — sign/verify client session JWTs, sign/verify magic-link JWT payloads, hash or look up one-time tokens
- `apps/api/src/__tests__/helpers/clientSessions.test.ts` — unit tests for JWT and cookie helpers
- `apps/api/src/routers/clientAuth.ts` — request-link, exchange, logout, and current-client endpoints
- `apps/api/src/routers/clientPortal.ts` — cookie-authenticated endpoints used by `apps/clients`
- `apps/api/src/__tests__/routes/clientAuth.test.ts` — request-link, exchange, logout, neutral-response, and invalid-token coverage
- `apps/api/src/__tests__/routes/clientPortal.test.ts` — client-cookie authorization and tablo/resource access coverage

**Client frontend**
- `apps/clients/src/lib/api.ts` — Axios instance with `withCredentials: true`
- `apps/clients/src/hooks/useClientSession.ts` — loads current client session from the API
- `apps/clients/src/hooks/useClientPortal.ts` — React Query hooks for tablos, tasks, etapes, files, events, and members
- `apps/clients/src/test/clientSessionTestUtils.tsx` — shared test wrapper for cookie-backed session mocks

### Files to delete after migration

- `apps/clients/src/lib/supabase.ts`
- `apps/clients/src/pages/AuthCallback.tsx`
- `apps/clients/src/pages/ResetPasswordPage.tsx`
- `apps/clients/src/pages/SetPasswordPage.tsx`
- `apps/clients/src/pages/ResetPasswordPage.test.tsx`
- `apps/clients/src/pages/SetPasswordPage.test.tsx`

### Modified files

**API**
- `apps/api/src/config.ts` — add client-auth env vars: JWT signing secret, session TTL, magic-link TTL, cookie name, cookie domain, and clients app URL
- `apps/api/src/types/app.types.ts` — add client-authenticated environment typing
- `apps/api/src/middlewares/middleware.ts` — add client-cookie auth middleware and remove `is_client`-based collaborator gating
- `apps/api/src/index.ts` — keep credentialed CORS working for client cookie requests
- `apps/api/src/routers/index.ts` — mount `clientAuth` and `clientPortal` routers
- `apps/api/src/routers/clientInvites.ts` — keep admin invite creation/cancel/list behavior, but switch its internals to the new `clients` tables and magic-link issuance
- `apps/api/src/routers/tablo.ts` or `apps/api/src/routers/tablo_data.ts` — extract reusable tablo-loading helpers if needed by `clientPortal.ts`
- `apps/api/src/helpers/helpers.ts` — remove old `findOrCreateClientAccount` / `ensureClientTabloAccess` logic once new helper files take over
- `apps/api/src/helpers/billing.ts` — remove `is_client` billing exclusions
- `apps/api/src/routers/user.ts` — stop exposing or expecting `is_client` on collaborator profile responses
- `apps/api/src/__tests__/routes/clientInvites.test.ts`
- `apps/api/src/__tests__/middlewares/middlewares.test.ts`
- `apps/api/src/__tests__/helpers/billing.test.ts`

**Shared types**
- `packages/shared-types/src/database.types.ts` — add `clients`, `client_access`, `client_magic_links`; remove `profiles.is_client`

**Main app**
- `apps/main/src/hooks/client_invites.ts` — adapt to the simplified invite response shape
- `apps/main/src/pages/tablo-details.tsx` — update invite copy and pending-invite UI
- `apps/main/src/pages/tablo-details.layout.test.tsx`
- `apps/main/src/components/ProtectedRoute.tsx` — remove `user.is_client` redirect logic
- `apps/main/src/components/AuthenticationGateway.tsx` — remove `user.is_client` redirect logic
- `apps/main/src/lib/clientPortal.ts` — delete if no longer needed
- `apps/main/src/components/ProtectedRoute.test.tsx`
- `apps/main/src/components/AuthenticationGateway.test.tsx`
- `apps/main/src/components/AuthenticationGateway.unit.tsx`
- `apps/main/src/providers/UserStoreProvider.tsx` — ensure the collaborator profile type no longer depends on `is_client`
- `apps/main/src/utils/testHelpers.tsx`
- `apps/main/src/contexts/UpgradeBlockContext.test.tsx`

**Client app**
- `apps/clients/src/main.tsx` — remove `SessionProvider`
- `apps/clients/src/routes.tsx` — remove callback/setup/reset routes and keep email-link login + protected routes
- `apps/clients/src/components/ClientAuthGate.tsx` — use backend session endpoint instead of Supabase session state
- `apps/clients/src/components/ClientLayout.tsx` — render current client email/name from backend session and call logout endpoint
- `apps/clients/src/components/ClientLayout.test.tsx`
- `apps/clients/src/pages/LoginPage.tsx` — convert to email-only request-link form
- `apps/clients/src/pages/LoginPage.test.tsx`
- `apps/clients/src/pages/ClientTabloListPage.tsx` — fetch from backend instead of `supabase.from("user_tablos")`
- `apps/clients/src/pages/ClientTabloPage.tsx` — migrate all data and mutations to backend hooks
- `apps/clients/src/pages/ClientTabloPage.test.tsx`

---

## Chunk 1: Backend Auth Foundation

### Task 1: Create the new client-auth schema and shared types

**Files:**
- Create: `supabase/migrations/20260501100000_create_client_auth_tables.sql`
- Modify: `packages/shared-types/src/database.types.ts`
- Test: `apps/api/src/__tests__/routes/clientInvites.test.ts`

- [ ] **Step 1: Write the failing invite tests against the new tables**

Add or rewrite route tests in `apps/api/src/__tests__/routes/clientInvites.test.ts` for:

```ts
it("upserts one global client identity per normalized email", async () => {});
it("creates a client_access grant for the invited tablo", async () => {});
it("creates a one-time invite magic link row", async () => {});
it("reuses the same client row when the same email is invited again", async () => {});
```

Make the assertions target `clients`, `client_access`, and `client_magic_links`, not `profiles.is_client` or `client_invites`.

- [ ] **Step 2: Run the route test file to verify failure**

Run:

```bash
pnpm --filter @xtablo/api test -- clientInvites.test.ts
```

Expected:
- FAIL because the current implementation still writes to `profiles.is_client` and `client_invites`

- [ ] **Step 3: Write the schema migration**

In `supabase/migrations/20260501100000_create_client_auth_tables.sql`, create the new tables with the minimum approved shape:

```sql
create table public.clients (...);
create unique index clients_normalized_email_idx on public.clients (normalized_email);

create table public.client_access (...);
create unique index client_access_active_unique_idx
  on public.client_access (client_id, tablo_id)
  where revoked_at is null;

create table public.client_magic_links (...);
create index client_magic_links_active_idx
  on public.client_magic_links (client_id, purpose, expires_at)
  where consumed_at is null;
```

Use one-time-use fields (`consumed_at`) and explicit expiry (`expires_at`). Do not recreate `is_pending` semantics under a different name.

- [ ] **Step 4: Update shared database types**

Update `packages/shared-types/src/database.types.ts` so it exposes:
- `clients`
- `client_access`
- `client_magic_links`

Keep the new tables typed before any router work starts.

- [ ] **Step 5: Run a type-focused verification**

Run:

```bash
pnpm --filter @xtablo/api typecheck
pnpm --filter @xtablo/clients typecheck
pnpm --filter @xtablo/main typecheck
```

Expected:
- collaborator app and client app still compile before code migration starts
- any failures should only be from the new type references you have not wired yet

- [ ] **Step 6: Commit**

```bash
git add supabase/migrations/20260501100000_create_client_auth_tables.sql packages/shared-types/src/database.types.ts apps/api/src/__tests__/routes/clientInvites.test.ts
git commit -m "feat: add client auth tables"
```

### Task 2: Add client account helpers, session helpers, and cookie middleware

**Files:**
- Create: `apps/api/src/helpers/clientAccounts.ts`
- Create: `apps/api/src/helpers/clientSessions.ts`
- Create: `apps/api/src/__tests__/helpers/clientSessions.test.ts`
- Modify: `apps/api/src/config.ts`
- Modify: `apps/api/src/types/app.types.ts`
- Modify: `apps/api/src/middlewares/middleware.ts`
- Modify: `apps/api/src/helpers/helpers.ts`

- [ ] **Step 1: Write the failing helper and middleware tests**

Add tests in `apps/api/src/__tests__/helpers/clientSessions.test.ts` for:

```ts
it("signs and verifies a client session JWT", async () => {});
it("rejects expired client session JWTs", async () => {});
it("extracts the configured client cookie from the request", async () => {});
it("signs magic-link JWTs with jti and expiry claims", async () => {});
```

Extend `apps/api/src/__tests__/middlewares/middlewares.test.ts` with:

```ts
it("authenticates a client request from the client session cookie", async () => {});
it("returns 401 when the client cookie is missing or invalid", async () => {});
```

- [ ] **Step 2: Run the tests to verify failure**

Run:

```bash
pnpm --filter @xtablo/api test -- clientSessions.test.ts middlewares.test.ts
```

Expected:
- FAIL because no client cookie auth primitives exist yet

- [ ] **Step 3: Add config and environment support**

In `apps/api/src/config.ts`, add:

```ts
CLIENT_AUTH_JWT_SECRET: string;
CLIENT_AUTH_COOKIE_NAME: string;
CLIENT_AUTH_COOKIE_DOMAIN: string;
CLIENT_MAGIC_LINK_TTL_MINUTES: number;
CLIENT_SESSION_TTL_DAYS: number;
CLIENTS_URL: string;
```

Use explicit config fields instead of reading `process.env` directly inside routers.

- [ ] **Step 4: Implement focused account and token helpers**

In `apps/api/src/helpers/clientAccounts.ts`, add small single-purpose helpers:

```ts
export function normalizeClientEmail(email: string): string {}
export async function upsertClientByEmail(...) {}
export async function ensureActiveClientAccess(...) {}
export async function clientHasAnyActiveAccess(...) {}
export async function revokeClientAccess(...) {}
```

In `apps/api/src/helpers/clientSessions.ts`, add:

```ts
export function signClientSession(...) {}
export function verifyClientSession(...) {}
export function signClientMagicLink(...) {}
export function verifyClientMagicLink(...) {}
export function buildClientSessionCookie(...) {}
export function clearClientSessionCookie(...) {}
export function readClientSessionCookie(...) {}
```

Keep cookie formatting and JWT signing out of the routers.

- [ ] **Step 5: Add client-auth middleware and typing**

Update `apps/api/src/types/app.types.ts` with a client-authenticated environment shape, for example:

```ts
type ClientEnv = BaseEnv & {
  Variables: BaseEnv["Variables"] & {
    client: Tables<"clients">;
  };
};
```

Then update `apps/api/src/middlewares/middleware.ts` to expose:

```ts
clientAuthMiddleware
maybeClientAuthMiddleware
```

These should verify the cookie, load the `clients` row, and set `c.set("client", client)`.

- [ ] **Step 6: Remove helper duplication**

Move client-specific logic out of `apps/api/src/helpers/helpers.ts`. Leave only thin wrappers there if other files still import the old names during the migration.

- [ ] **Step 7: Run backend verification**

Run:

```bash
pnpm --filter @xtablo/api test -- clientSessions.test.ts middlewares.test.ts
pnpm --filter @xtablo/api typecheck
```

Expected:
- PASS for helper and middleware coverage

- [ ] **Step 8: Commit**

```bash
git add apps/api/src/helpers/clientAccounts.ts apps/api/src/helpers/clientSessions.ts apps/api/src/__tests__/helpers/clientSessions.test.ts apps/api/src/config.ts apps/api/src/types/app.types.ts apps/api/src/middlewares/middleware.ts apps/api/src/helpers/helpers.ts apps/api/src/__tests__/middlewares/middlewares.test.ts
git commit -m "feat: add client auth helpers and middleware"
```

---

## Chunk 2: Backend Routes And Authorized Client API

### Task 3: Replace the invite flow and add public client-auth routes

**Files:**
- Create: `apps/api/src/routers/clientAuth.ts`
- Create: `apps/api/src/__tests__/routes/clientAuth.test.ts`
- Modify: `apps/api/src/routers/clientInvites.ts`
- Modify: `apps/api/src/routers/index.ts`
- Modify: `apps/api/src/index.ts`
- Test: `apps/api/src/__tests__/routes/clientInvites.test.ts`

- [ ] **Step 1: Write the failing route tests**

Add `apps/api/src/__tests__/routes/clientAuth.test.ts` covering:

```ts
it("returns a neutral success response for request-link even when the email is unknown", async () => {});
it("creates and emails a login magic link when the client has active access", async () => {});
it("rejects an expired or consumed exchange token", async () => {});
it("sets the client session cookie when a valid token is exchanged", async () => {});
it("clears the cookie on logout", async () => {});
it("returns the current client from /me when the cookie is valid", async () => {});
```

Extend `clientInvites.test.ts` with:

```ts
it("sends an invite magic link email for a new client", async () => {});
it("reissues access via the same global client row for an existing client", async () => {});
it("cancelling an invite revokes the targeted access row", async () => {});
```

- [ ] **Step 2: Run the route tests to verify failure**

Run:

```bash
pnpm --filter @xtablo/api test -- clientAuth.test.ts clientInvites.test.ts
```

Expected:
- FAIL because `clientAuth` router does not exist and `clientInvites` still uses the old Supabase-auth client model

- [ ] **Step 3: Implement `clientAuth` public routes**

In `apps/api/src/routers/clientAuth.ts`, add:

```ts
POST /client-auth/request-link
GET  /client-auth/exchange
POST /client-auth/logout
GET  /client-auth/me
```

Behavior:
- `request-link`
  - always return a neutral success payload
  - only create and email a magic link if the normalized email maps to a client with active access
- `exchange`
  - verify JWT + backing `client_magic_links` row
  - reject missing / expired / consumed links
  - mark `consumed_at`
  - set the client session cookie
  - redirect to the `redirect_to` route or a safe default inside `clients.xtablo.com`
- `logout`
  - clear the cookie
- `me`
  - require cookie auth and return the current client identity

- [ ] **Step 4: Rework admin invite creation**

In `apps/api/src/routers/clientInvites.ts`, keep the existing admin-facing surface:

```ts
POST   /client-invites/:tabloId
GET    /client-invites/:tabloId/pending
DELETE /client-invites/:tabloId/:inviteId
```

But change the implementation to:
- upsert `clients`
- ensure `client_access`
- create a `purpose = 'invite'` magic-link row
- send an invite email that targets the new backend exchange route
- stop creating Supabase Auth users
- stop updating `profiles.is_client`
- stop using password-setup semantics

Pending invites should now read from `client_magic_links` filtered by `purpose = 'invite'` and `consumed_at is null`.

- [ ] **Step 5: Mount the router and keep CORS cookie-safe**

In `apps/api/src/routers/index.ts`, mount `clientAuth.ts`.

In `apps/api/src/index.ts`, verify the CORS middleware continues to send:
- `credentials: true`
- exact caller origin

Do not rely on Authorization headers for the client portal anymore.

- [ ] **Step 6: Run backend auth route verification**

Run:

```bash
pnpm --filter @xtablo/api test -- clientAuth.test.ts clientInvites.test.ts
pnpm --filter @xtablo/api typecheck
```

Expected:
- PASS for invite issuance, neutral login-link requests, exchange, logout, and `/me`

- [ ] **Step 7: Commit**

```bash
git add apps/api/src/routers/clientAuth.ts apps/api/src/__tests__/routes/clientAuth.test.ts apps/api/src/routers/clientInvites.ts apps/api/src/routers/index.ts apps/api/src/index.ts apps/api/src/__tests__/routes/clientInvites.test.ts
git commit -m "feat: add client magic link auth routes"
```

### Task 4: Add the cookie-authenticated client portal API

**Files:**
- Create: `apps/api/src/routers/clientPortal.ts`
- Create: `apps/api/src/__tests__/routes/clientPortal.test.ts`
- Modify: `apps/api/src/routers/index.ts`
- Modify: `apps/api/src/routers/tablo.ts`
- Modify: `apps/api/src/routers/tablo_data.ts`
- Modify: `apps/api/src/helpers/billing.ts`

- [ ] **Step 1: Write the failing client portal route tests**

Add tests for:

```ts
it("lists only the tablos accessible to the authenticated client", async () => {});
it("loads a single tablo only when client_access is active", async () => {});
it("rejects access to a tablo outside the client's grants", async () => {});
it("allows file, folder, task, etape, event, and member reads through the client portal API", async () => {});
```

Keep response shapes aligned with the data `apps/clients` already expects today so the frontend rewrite is mostly transport-level.

- [ ] **Step 2: Run the portal route tests to verify failure**

Run:

```bash
pnpm --filter @xtablo/api test -- clientPortal.test.ts
```

Expected:
- FAIL because there is no cookie-authenticated client portal router yet

- [ ] **Step 3: Implement `clientPortal.ts`**

Add a dedicated router such as:

```ts
GET /client-portal/tablos
GET /client-portal/tablos/:tabloId
GET /client-portal/tablos/:tabloId/tasks
GET /client-portal/tablos/:tabloId/etapes
GET /client-portal/tablos/:tabloId/events
GET /client-portal/tablos/:tabloId/members
GET /client-portal/tablos/:tabloId/files
GET /client-portal/tablos/:tabloId/folders
POST /client-portal/tablos/:tabloId/files
GET /client-portal/tablos/:tabloId/files/:fileName
POST /client-portal/tablos/:tabloId/folders
PUT /client-portal/tablos/:tabloId/folders/:folderId
DELETE /client-portal/tablos/:tabloId/folders/:folderId
```

Each route must:
- require `clientAuthMiddleware`
- verify `client_access` for the requested `tabloId`
- reuse existing query logic from `tablo.ts` / `tablo_data.ts` where practical

- [ ] **Step 4: Extract shared query helpers instead of duplicating router code**

If `tablo.ts` or `tablo_data.ts` contains query logic you need twice, extract helper functions with one responsibility, for example:

```ts
async function loadTabloByIdForClient(...) {}
async function loadTabloTasks(...) {}
async function loadTabloFolders(...) {}
```

Do not copy-paste large query blocks into `clientPortal.ts`.

- [ ] **Step 5: Remove billing and collaborator assumptions**

Update `apps/api/src/helpers/billing.ts` to stop filtering `profiles.is_client`. Client identities no longer live in `profiles`, so collaborator billing should count only collaborator profile rows.

- [ ] **Step 6: Run backend portal verification**

Run:

```bash
pnpm --filter @xtablo/api test -- clientPortal.test.ts billing.test.ts
pnpm --filter @xtablo/api typecheck
```

Expected:
- PASS for client portal data access and billing logic

- [ ] **Step 7: Commit**

```bash
git add apps/api/src/routers/clientPortal.ts apps/api/src/__tests__/routes/clientPortal.test.ts apps/api/src/routers/index.ts apps/api/src/routers/tablo.ts apps/api/src/routers/tablo_data.ts apps/api/src/helpers/billing.ts apps/api/src/__tests__/helpers/billing.test.ts
git commit -m "feat: add cookie-authenticated client portal api"
```

---

## Chunk 3: Frontend Migration And Legacy Cleanup

### Task 5: Migrate `apps/clients` to backend session cookies

**Files:**
- Create: `apps/clients/src/lib/api.ts`
- Create: `apps/clients/src/hooks/useClientSession.ts`
- Create: `apps/clients/src/hooks/useClientPortal.ts`
- Create: `apps/clients/src/test/clientSessionTestUtils.tsx`
- Modify: `apps/clients/src/main.tsx`
- Modify: `apps/clients/src/routes.tsx`
- Modify: `apps/clients/src/components/ClientAuthGate.tsx`
- Modify: `apps/clients/src/components/ClientLayout.tsx`
- Modify: `apps/clients/src/pages/LoginPage.tsx`
- Modify: `apps/clients/src/pages/LoginPage.test.tsx`
- Modify: `apps/clients/src/pages/ClientTabloListPage.tsx`
- Modify: `apps/clients/src/pages/ClientTabloPage.tsx`
- Modify: `apps/clients/src/pages/ClientTabloPage.test.tsx`
- Modify: `apps/clients/src/components/ClientLayout.test.tsx`
- Delete: `apps/clients/src/lib/supabase.ts`
- Delete: `apps/clients/src/pages/AuthCallback.tsx`
- Delete: `apps/clients/src/pages/ResetPasswordPage.tsx`
- Delete: `apps/clients/src/pages/SetPasswordPage.tsx`
- Delete: `apps/clients/src/pages/ResetPasswordPage.test.tsx`
- Delete: `apps/clients/src/pages/SetPasswordPage.test.tsx`

- [ ] **Step 1: Write the failing client app tests**

Update tests to cover:

```ts
it("submits an email-only login form and shows a neutral success state", async () => {});
it("redirects unauthenticated users to /login after the session endpoint returns 401", async () => {});
it("loads the current client from the backend session endpoint", async () => {});
it("loads client tablos and tablo details from backend API hooks instead of Supabase", async () => {});
it("logs out via the backend and returns to /login", async () => {});
```

- [ ] **Step 2: Run the client app tests to verify failure**

Run:

```bash
pnpm --filter @xtablo/clients test -- LoginPage.test.tsx ClientLayout.test.tsx ClientTabloPage.test.tsx
```

Expected:
- FAIL because the app still depends on `SessionProvider`, `supabase.auth`, and browser-side Supabase queries

- [ ] **Step 3: Introduce a cookie-aware client API layer**

In `apps/clients/src/lib/api.ts`, create an Axios instance:

```ts
export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true,
  headers: { "Content-Type": "application/json" },
});
```

Then add `useClientSession.ts` and `useClientPortal.ts` so pages do not build ad hoc API clients inline.

- [ ] **Step 4: Remove Supabase session bootstrap from the app shell**

Update `apps/clients/src/main.tsx` to remove `SessionProvider` and the Supabase client import entirely.

Update `ClientAuthGate.tsx` to:
- call `/api/v1/client-auth/me`
- store the intended destination in `localStorage`
- redirect to `/login` when the API says unauthenticated

Update `ClientLayout.tsx` to:
- read the current client from `useClientSession()`
- call `POST /api/v1/client-auth/logout`

- [ ] **Step 5: Convert login and protected pages**

Update `LoginPage.tsx` so it:
- renders a single email field
- submits to `POST /api/v1/client-auth/request-link`
- shows a generic “check your email” confirmation state
- removes password and reset-password flows

Update `ClientTabloListPage.tsx` and `ClientTabloPage.tsx` so every query and mutation uses `useClientPortal.ts` hooks backed by the new API endpoints.

Preserve current response shapes where possible to minimize UI churn in `@xtablo/tablo-views`.

- [ ] **Step 6: Delete the obsolete client auth pages**

Remove:
- `AuthCallback.tsx`
- `ResetPasswordPage.tsx`
- `SetPasswordPage.tsx`

Also remove their routes from `apps/clients/src/routes.tsx`.

- [ ] **Step 7: Run client app verification**

Run:

```bash
pnpm --filter @xtablo/clients test -- LoginPage.test.tsx ClientLayout.test.tsx ClientTabloPage.test.tsx
pnpm --filter @xtablo/clients typecheck
```

Expected:
- PASS for email-link login, auth gate, logout, and cookie-backed data loading

- [ ] **Step 8: Commit**

```bash
git add apps/clients/src/lib/api.ts apps/clients/src/hooks/useClientSession.ts apps/clients/src/hooks/useClientPortal.ts apps/clients/src/test/clientSessionTestUtils.tsx apps/clients/src/main.tsx apps/clients/src/routes.tsx apps/clients/src/components/ClientAuthGate.tsx apps/clients/src/components/ClientLayout.tsx apps/clients/src/pages/LoginPage.tsx apps/clients/src/pages/LoginPage.test.tsx apps/clients/src/pages/ClientTabloListPage.tsx apps/clients/src/pages/ClientTabloPage.tsx apps/clients/src/pages/ClientTabloPage.test.tsx apps/clients/src/components/ClientLayout.test.tsx
git add -u apps/clients/src/lib/supabase.ts apps/clients/src/pages/AuthCallback.tsx apps/clients/src/pages/ResetPasswordPage.tsx apps/clients/src/pages/SetPasswordPage.tsx apps/clients/src/pages/ResetPasswordPage.test.tsx apps/clients/src/pages/SetPasswordPage.test.tsx
git commit -m "feat: migrate clients app to cookie auth"
```

### Task 6: Remove legacy `is_client` logic from collaborator flows and finish cleanup

**Files:**
- Create: `supabase/migrations/20260501150000_drop_profiles_is_client.sql`
- Modify: `apps/api/src/middlewares/middleware.ts`
- Modify: `apps/api/src/routers/user.ts`
- Modify: `apps/main/src/components/ProtectedRoute.tsx`
- Modify: `apps/main/src/components/AuthenticationGateway.tsx`
- Modify: `apps/main/src/components/ProtectedRoute.test.tsx`
- Modify: `apps/main/src/components/AuthenticationGateway.test.tsx`
- Modify: `apps/main/src/components/AuthenticationGateway.unit.tsx`
- Modify: `apps/main/src/hooks/client_invites.ts`
- Modify: `apps/main/src/pages/tablo-details.tsx`
- Modify: `apps/main/src/pages/tablo-details.layout.test.tsx`
- Modify: `apps/main/src/providers/UserStoreProvider.tsx`
- Modify: `apps/main/src/utils/testHelpers.tsx`
- Modify: `apps/main/src/contexts/UpgradeBlockContext.test.tsx`
- Delete: `apps/main/src/lib/clientPortal.ts` if no remaining imports

- [ ] **Step 1: Write the failing cleanup tests**

Update or add tests asserting:

```ts
it("does not special-case collaborator routing based on user.is_client", async () => {});
it("shows generic client invite success copy after invite creation", async () => {});
it("stops expecting is_client in collaborator test fixtures", async () => {});
```

- [ ] **Step 2: Run the targeted main-app and API tests to verify failure**

Run:

```bash
pnpm --filter @xtablo/main test -- ProtectedRoute.test.tsx AuthenticationGateway.test.tsx tablo-details.layout.test.tsx
pnpm --filter @xtablo/api test -- middlewares.test.ts
```

Expected:
- FAIL because collaborator routes and middleware still reference `is_client`

- [ ] **Step 3: Remove `is_client` from runtime code**

Update:
- `apps/api/src/middlewares/middleware.ts`
- `apps/api/src/routers/user.ts`
- `apps/main/src/components/ProtectedRoute.tsx`
- `apps/main/src/components/AuthenticationGateway.tsx`
- `apps/main/src/providers/UserStoreProvider.tsx`

The collaborator app should no longer know about a client-user subtype inside the collaborator session model.

- [ ] **Step 4: Update invite UX and fixtures**

Update `apps/main/src/hooks/client_invites.ts` and `apps/main/src/pages/tablo-details.tsx` so the success states match the new backend behavior:
- client invited by email
- magic link sent
- pending invites listed from `client_magic_links`

Then remove `is_client` from test helpers and fixtures.

- [ ] **Step 5: Drop the old schema field**

In `supabase/migrations/20260501150000_drop_profiles_is_client.sql`, remove the legacy column:

```sql
alter table public.profiles drop column if exists is_client;
```

Only add this migration after the codebase no longer reads or writes the column.

- [ ] **Step 6: Run final verification**

Run:

```bash
pnpm --filter @xtablo/api test
pnpm --filter @xtablo/api typecheck
pnpm --filter @xtablo/clients test
pnpm --filter @xtablo/clients typecheck
pnpm --filter @xtablo/main test -- ProtectedRoute.test.tsx AuthenticationGateway.test.tsx tablo-details.layout.test.tsx
pnpm --filter @xtablo/main typecheck
```

Expected:
- PASS for full client-auth flow and collaborator cleanup

- [ ] **Step 7: Commit**

```bash
git add supabase/migrations/20260501150000_drop_profiles_is_client.sql apps/api/src/middlewares/middleware.ts apps/api/src/routers/user.ts apps/main/src/components/ProtectedRoute.tsx apps/main/src/components/AuthenticationGateway.tsx apps/main/src/components/ProtectedRoute.test.tsx apps/main/src/components/AuthenticationGateway.test.tsx apps/main/src/components/AuthenticationGateway.unit.tsx apps/main/src/hooks/client_invites.ts apps/main/src/pages/tablo-details.tsx apps/main/src/pages/tablo-details.layout.test.tsx apps/main/src/providers/UserStoreProvider.tsx apps/main/src/utils/testHelpers.tsx apps/main/src/contexts/UpgradeBlockContext.test.tsx
git add -u apps/main/src/lib/clientPortal.ts
git commit -m "refactor: remove legacy is_client flow"
```

---

## Notes For Execution

- Keep response payloads for `ClientTabloPage.tsx` as close as possible to the current shapes from `user_tablos`, `tasks_with_assignee`, `events_and_tablos`, and related queries. The feature is already large; do not rewrite UI state models unless the API boundary forces it.
- Do not mix collaborator bearer-token auth and client cookie auth in the same frontend hooks. Keep them separate.
- Prefer backend exchange-and-redirect for invite/login links so the raw magic-link token is not handled by React unless absolutely necessary.
- Do not add a `client_sessions` table in v1. That is explicitly out of scope for this plan.
- Delay the `drop_profiles_is_client` migration until every compile-time and runtime reference is gone.

Plan complete and saved to `docs/superpowers/plans/2026-04-30-client-magic-link-auth.md`. Ready to execute?
