# Auth Plan

Auth is part of the product.

Do not treat it as plumbing.

## Default Choice

Use WorkOS AuthKit first.

Reason:

- Co-Vibe is team/workspace shaped.
- We need real developer identity.
- We need tenant and workspace identity from the start.

## Historical Fallback

Clerk was the emergency fallback if WorkOS blocked the first build loop. It is
not the active implementation path.

Reason:

- Clerk is quick to wire into Next.js.
- It has strong prebuilt UI.
- It is good enough for dogfood if WorkOS takes too long.

## Required Auth Behavior

The product must support:

- UI login
- workspace admin/member roles
- logged-in developer record
- MCP token generation
- MCP token revocation
- MCP calls mapped to developer id
- tasks/events/warnings tied to developer id

## MCP Tokens

MCP clients cannot rely only on browser cookies.

The UI should let a logged-in developer create a local agent token.

The server stores only a token hash.

The raw token is shown once.

Agent config uses that token for MCP calls.

The UI supports creating and revoking local agent tokens.

MCP tokens are always owned by the developer who creates them. Coding agents
using that token inherit the owner's developer identity, active workspace, and
workspace tenant.

Tokens may carry a typed `agent_type` (`claude-code`, `codex`, `cursor`).
Besides the UI, `covibe-local setup` can mint typed tokens through an
RFC-8628-style device flow: the CLI requests a grant, the developer approves
it once at `/setup/authorize`, and the next poll delivers one typed token per
agent exactly once. Device codes are stored hashed and grants expire after 10
minutes. See `docs/engineering/device-setup.md`.

## Workspace Scope

Co-Vibe stores `tenants`, `tenant_memberships`, `workspaces`, and
`workspace_memberships`.

In WorkOS mode, a WorkOS Organization is the tenant/company boundary. Co-Vibe
workspaces are team or project spaces inside that tenant; creating a workspace
must not create another WorkOS Organization. The selected AuthKit
`organizationId` decides which tenant is active, and a workspace cookie is
accepted only when that workspace belongs to the signed-in tenant.

Tenant admins can rename the tenant, create workspaces, and create `admin` or
`member` users. User onboarding defaults to a WorkOS invitation email. Tenant
admins may set an initial password instead; Co-Vibe sends that password only to
WorkOS and does not store or log it. Workspace admins can rename their
workspace. Users can edit name/handle and request WorkOS password reset. Email
remains immutable in Co-Vibe.

## Current Modes

Co-Vibe runs in two modes:

- `local-dev`: no WorkOS env vars, explicit local tenant/workspace onboarding
- `workos`: all WorkOS env vars present, AuthKit routes enabled

`COVIBE_AUTH_MODE=workos` requires complete WorkOS env.

Production without complete WorkOS env is blocked instead of silently using local dev identity.

`npm run doctor:production` checks WorkOS env completeness, auth mode, cookie password strength, and callback URL shape before deploy.

After `npm run build`, `npm run smoke:production:blocked` starts the built server with WorkOS env removed and verifies blocked auth responses.

WorkOS sign-in starts at `/auth/sign-in`.

WorkOS callback is `/callback`.

In WorkOS mode, `/auth/sign-in` and `/auth/sign-up` render Co-Vibe-designed
chooser cards with exactly three options: GitHub, Google, and email. The
GitHub/Google buttons deep-link straight to the provider through a WorkOS
authorization URL built in `src/server/auth/provider-authorization.ts` — it
mirrors authkit-nextjs's sealed PKCE state contract (the package is pinned at
4.1.1 for this reason), so the standard `/callback` handler verifies it
unchanged. The email option is **headless**: the form POSTs email+password to
`/auth/email`, which authenticates against WorkOS via the User Management API
(`authenticateWithPassword`, or `createUser` then authenticate on sign-up),
calls `saveSession`, and redirects — it never leaves to the hosted AuthKit page.
Errors come back as `?error=` and prefill the email; mapping lives in
`src/server/auth/email-auth.ts`. (MFA/SSO challenges can't complete headless and
surface a "use a social provider" message.) GitHub/Google still redirect to the
provider, which is unavoidable for OAuth. `/auth/sign-up` is the tenant-creation path: it
seals `returnTo=/onboarding` so new teams land in tenant creation, then the
welcome wizard. The legacy `?screen=sign-up` query redirects to `/auth/sign-up`,
and `/auth` redirects to `/auth/sign-in`. In local-dev both pages render a
minimal name/email identity form that stores a pending-identity cookie
(`covibe_pending_identity`) through `POST /api/local-identity`.

After authentication, the auth gate routes by state: unauthenticated visitors
go to `/auth/sign-in`; authenticated users without a resolvable tenant and
developer go to `/onboarding`; freshly provisioned developers (an
`onboarding_pending` flag set on every new developer row) go to the `/welcome`
wizard; everyone else lands on the dashboard.

`/welcome` is the first-session wizard. Tenant creators see: tenant ready →
connect agents (device-flow setup commands plus a live `/api/onboarding/status`
poll for "agent connected" and "first snapshot") → what gets logged (the
automatic repo logging disclosure with `covibe-local exclude` and
`exclude --new-repos` opt-outs) → invite team (WorkOS email invites) → done.
Invited members get a "you've joined &lt;tenant&gt;" variant without the invite
step. Finishing or skipping posts `/api/onboarding/complete`, which clears the
pending flag.

`/onboarding` is the post-auth tenant-creation page shared by both modes. It
submits to `POST /api/onboarding`, which creates the first tenant, workspace,
tenant membership, workspace membership, and owner developer in PostgreSQL —
but only when the tenant has no members yet (the bootstrap guard), so a user
joining an existing tenant cannot self-elevate. Retries and duplicate submits
from an already-provisioned caller return ok instead of 403. A WorkOS user
invited to an existing organization tenant that already has members sees an
access-pending panel instead of a doomed create form; a tenant admin must add
them from Settings. A 401 from the onboarding API sends the form back to
`/auth/sign-in`.

Sessions are always persistent. Co-Vibe session cookies and the WorkOS session
cookie are written with the `Max-Age` configured by `COVIBE_REMEMBER_ME_DAYS`
(session length in days, default 30, max 365; the env name is kept for config
compatibility). Cookie deletions on sign-out are never extended.

Local-dev profile updates refresh the local developer cookie when a user changes
their handle. Stale or missing local cookies do not resolve to a default
developer; users must sign in through an existing tenant/workspace session or
create a new tenant through `/onboarding`.

`/auth/sign-out` is the matching logout path. It clears Co-Vibe's local
developer, tenant, workspace, and pending-identity cookies, plus the retired
`covibe_remember_me` opt-in cookie for one release cycle. Local-dev also sets a
signed-out marker so it stays signed out until explicit onboarding; WorkOS mode
then calls AuthKit `signOut` and returns users to `/auth/sign-in`.

Direct sign-in and callback route calls also fail closed when auth config is blocked.

`/api/dev-login`, `/api/demo-scenario`, and `/api/local-identity` are local-dev
only and return 404 in WorkOS mode.

`src/proxy.ts` runs AuthKit proxy handling in WorkOS mode so `withAuth()` has trusted session headers.

The dashboard page requires a signed-in WorkOS user, and `/api/state` returns 401 if no developer is authenticated.

Live WorkOS local testing requires the WorkOS dashboard Redirects page to allow
the exact local port:

- `http://localhost:3206/callback` when running `npm run dev -- --port 3206`

If that URI is missing, WorkOS redirects to `redirect-uri-invalid` after `/auth/sign-in`.

Deployment callbacks must use the deployed HTTPS origin, for example
`https://co-vibe.example.com/callback`; `doctor:production` rejects non-local
HTTP callbacks.

## WorkOS Dashboard Branding

The hosted AuthKit page is configured in the WorkOS dashboard — this is
configuration, not code:

- dark theme to match the product
- logo: `public/logo-mark.svg`
- accent color `#5e6ad2`
- sentence-case copy overrides where the dashboard allows text changes
- optional custom auth domain so the hosted page lives under the product domain

## Organization Backing For Self-Serve Tenants

Self-serve onboarding (no `organizationId` in the session) provisions a WorkOS
organization for the new tenant: `/api/onboarding` calls
`createWorkosOrganizationForTenant` (`src/server/workos/admin.ts`), names the
org after the tenant, and attaches the creator as an `admin` org member. The
tenant keeps its `workos_user:<id>` external id — that is what the creator's
current session resolves — while `workos_organization_id` carries the new org,
which is what invites (`createWorkosTenantMember`) and invited members'
org-scoped sessions resolve. Every self-serve tenant is therefore invitable
from day one.

Failure handling is graceful: if organization creation fails or the API is
unavailable, the tenant is still created user-backed (single-seat, the old
behavior) — "Ask a team admin to add you" copy stays honest because Settings
invites simply stay unavailable there. If the org is created but the admin
membership attach fails, the org binding is kept: invitations only need the
organization, and the creator's session resolves the tenant by external id.
Known residual: a truly concurrent double-submit of first-time onboarding can
strand one unused WorkOS organization; tenants that predate this feature stay
user-backed until a migration assigns them an organization.

## Minimum Security

- never store raw MCP tokens
- never log raw tokens
- protect token creation routes
- protect dashboard state routes
- disable local-only demo and developer-switch routes in WorkOS mode
- protect dashboard routes
- fail closed when auth is missing
- revoke tokens immediately when requested

## Morning Report Requirement

The morning report must state:

- chosen auth provider
- what auth works
- what remains incomplete or externally unverified
- how to create an MCP token
- any auth risk left open
