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 onboardingworkos: 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 <tenant>" 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/callbackwhen runningnpm 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