Engineering

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 <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/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
View as .md