Auth and workspace endpoints

Authentication

Signup, sign-in and sign-out go through the API rather than the Supabase browser client, so session cookies are set by the server.

POST /api/auth/register

Creates the Supabase user, then a Profile, a Subscription and a SubscriptionUser with role owner.

Body

{ "email": "…​", "password": "…​", "name": "…​" }

Response

{ "success": true } or { "user_id": "…​" }

The client must then call POST /api/auth/signin with the same credentials to establish the session. Password strength is checked by lib/auth/password-strength.ts, and the email domain by isAllowedPayabilityEmail().

POST /api/auth/signin

Body

{ "email": "…​", "password": "…​" }

Sets the session cookies on the response through the route-handler Supabase client. Call with credentials: "same-origin".

POST /api/auth/signout

Clears the session and the active_subscription_id cookie.

Response

{ "success": true }

The client should redirect to /login and call router.refresh() afterwards.

Current user

GET /api/me

The dashboard will not render until this resolves — WorkspaceGate blocks on it.

Response

{ "user", "profile", "subscriptions", "active_subscription_id" }

profile is the caller’s profile within the active workspace; subscriptions is every workspace they belong to, with roles.

Workspace selection

GET /api/subscriptions

Response
{
  "subscriptions": [
    { "id": "uuid", "name": "Acme", "role": "owner", "created_at": "2026-01-01T00:00:00.000Z" }
  ],
  "active_subscription_id": "uuid"
}

POST /api/subscriptions/active

Body

{ "subscriptionId": "uuid" }

Verifies membership, then sets active_subscription_id on the response. This is the only place the cookie is written — never set it client-side.

Members

All member endpoints operate on the current workspace, resolved from the cookie or header.

GET /api/subscriptions/members

Response

{ "data": [{ "id", "profile_id", "name", "role", "joined_at" }], "total": n }

Member data comes from Profile only; there is no Supabase Admin API call, so the endpoint stays fast and needs no service-role key.

POST /api/subscriptions/members

Owner or admin only.

Body

{ "email": "user@example.com", "role": "member" | "admin" | "owner" }

Looks up the Profile by email and creates a SubscriptionUser. Only owners may add an owner.

Response

201 { "success": true }

The invitee must already have a Profile — that is, they must have signed up. There is no invitation-by-email flow.

PATCH /api/subscriptions/members/{membershipId}

Owner or admin only. Body

{ "role": "owner" | "admin" | "member" }. Only owners may assign the owner role.

DELETE /api/subscriptions/members/{membershipId}

Owners may remove anyone; admins may remove non-owners only.

Response

{ "success": true }

Legacy agent endpoints

GET /api/agents and PATCH /api/agents operate on Profile as a calling agent — the JustCall-era human availability model.

GET /api/agents accepts isAvailable and selectRandom, and derives an availability value:

Value Condition

available

isAvailable and activeCalls < maxConcurrentCalls

busy

isAvailable and at capacity

offline

isAvailable is false

PATCH /api/agents updates the caller’s own record (name, max_concurrent_calls, availability).

Current rep identity lives on SalesRep — see Sales rep and task endpoints.

Workspace config

Key/value rows in the configs table, scoped to the active workspace. Values are always strings; callers coerce them (for example feature flags via ConfigRepository.isValueEnabled).

key is globally unique rather than unique per workspace — creating a key that already exists returns 409.

GET /api/configs

Response

{ "data": [{ "id", "key", "value", "created_at", "updated_at" }], "total": n }

POST /api/configs

Body

{ "key": "purchase_new_phone_numbers", "value": "true" }

Response

201 with the created row.

PATCH /api/configs/{id}

Body

{ "value": "false" }. The key cannot be renamed. Returns 404 if the row is not in the active workspace.