Cosmo Docs
Server

Auth Module

JWT, Clerk sync, org management, invitations, and webhooks

The Auth module handles all identity, token, and organisation management. Source: packages/server/src/modules/auth/.

REST Endpoints

AuthController (auth.controller.ts)

All endpoints are prefixed with /api/v1/auth.

MethodPathGuardsDescription
GET/cli-authNoneCLI auth step 1 — redirect to Clerk sign-in
GET/cli-callbackNoneCLI auth step 2 — exchange session for Cosmo JWT
POST/exchange-tokenNoneDesktop/web — exchange Clerk JWT for Cosmo JWT
POST/onboarding-completeClerkAuthGuardMark user's onboarding complete
POST/welcome-seenClerkAuthGuardMark the first-run welcome modal dismissed (idempotent)
GET/onboarding-stateClerkAuthGuardRead { onboardingCompleted, welcomeSeen } for this membership
POST/seed-onboardingClerkAuthGuardSeed org with sample teams + tasks (idempotent)
POST/refreshNoneRefresh expired Cosmo JWT (7-day grace)
GET/meClerkAuthGuard + OptionalOrgGet authenticated user profile
POST/webhookNoneClerk webhook receiver (Svix-verified)

OrgController (org.controller.ts)

All endpoints are prefixed with /api/v1/org.

MethodPathGuardsDescription
GET/membersClerkAuthGuard + RolesGuardList org members
PATCH/members/:userId/role@Roles('OWNER')Change a member's role
DELETE/members/:userId@Roles('OWNER', 'ADMIN')Remove a member
GET/invites@Roles('OWNER', 'ADMIN')List pending invitations
POST/invite@Roles('OWNER', 'ADMIN')Invite a user by email
POST/invite/:id/resend@Roles('OWNER', 'ADMIN')Resend an invitation
DELETE/invite/:id@Roles('OWNER', 'ADMIN')Revoke an invitation

Auth Service (services/auth.service.ts)

Token Operations

MethodDescription
mintCosmoToken(userId, orgId, orgRole)Issue a 30-day signed JWT
verifyCosmoToken(token)Verify and decode; throws on invalid/expired
refreshToken(token)Re-issue JWT; allows up to 7 days after expiry
verifyClerkToken(token)Verify Clerk JWT, return { clerkUserId, clerkOrgId, clerkOrgRole }

User & Org Provisioning

MethodDescription
syncUserFromClerk(clerkUserId, email, name, avatarUrl)Create or update User record
syncOrgFromClerk(clerkId, name, slug)Create or update Organization record
syncMembership(clerkOrgId, clerkUserId, role)Upsert OrgMember with mapped role
getMembership(orgId, userId)Fetch membership or null
markOnboardingCompleted(orgId, userId)Set onboardingCompleted = true
markWelcomeSeen(orgId, userId)Stamp welcomeSeenAt on first dismissal (idempotent)
getOnboardingState(orgId, userId)Return { onboardingCompleted, welcomeSeen } for the membership
fetchClerkUser(clerkUserId)Fetch user from Clerk Management API
fetchFirstClerkOrg(clerkUserId)Get user's first org from Clerk
fetchClerkMembershipRole(orgId, userId)Get role from Clerk (for refresh flows)

Member Management

MethodDescription
listMembers(orgId)All members with user data
updateMemberRole(orgId, userId, role)Change role
removeMember(orgId, userId)Delete membership record

Invitations

MethodDescription
createInvite(orgId, email, role)Generate invite with secure token (24h expiry)
listInvites(orgId)All pending (non-expired) invites
resendInvite(inviteId, orgId, actingUserId)Revoke the outgoing Clerk invitation (as actingUserId, resolved to a Clerk id and sent as requesting_user_id), mint a replacement, then update the existing row in place with the new token/expiry — never inserts a second row for the same email. Recreates the row if the organizationInvitation.revoked webhook races the revoke and deletes it first. Throws if Clerk reports the invitee already joined the org (and cleans up the now-dead local row in that case)
revokeInvite(inviteId, orgId, actingUserId)Revoke the invitation in Clerk (same requesting_user_id handling as above) before deleting the local row, so the emailed link stops working. Degrades gracefully — warns and still deletes locally — when Clerk isn't configured or the org has no Clerk linkage

An invite that cannot land is rejected with a 409 Conflict whose message is user-facing copy the client shows verbatim — an existing member of this org, an invite already pending, or an account that already belongs to any org (see the one-org rule below). Clerk's own refusal codes (already_a_member_in_organization, duplicate_record) map to the same 409 rather than being swallowed into a 201 success.

One organization per user

assertCanProvisionOrg(userId, clerkOrgId) is the enforcement point, called from POST /auth/exchange-token. A user who belongs to any org — in any role, not only as its owner — is refused (403) when the org being provisioned is a different one. Re-authenticating into the org they already belong to is always allowed, so nobody is locked out of their own workspace. createInvite applies the same rule up front so an admin is told before sending, rather than waiting on an acceptance that the exchange would reject.

Onboarding Service (services/onboarding.service.ts)

seedOrgOnboarding(orgId, userId, data?) is the entry point called by POST /auth/seed-onboarding. It:

  1. Creates a shared team ("Shared") and a personal team for the user.
  2. Creates a shared workspace ("Shared Workspace") in the shared team.
  3. Seeds 6 sample tasks across the personal and shared workspaces.

The seeding is idempotent — it checks for an existing shared team before creating anything. A database transaction prevents race conditions from concurrent requests.

Webhook Handler (webhooks/webhook.handler.ts)

Handles Clerk webhooks verified with Svix. Registered at POST /api/v1/auth/webhook.

EventAction
user.createdsyncUserFromClerk(...)
user.updatedsyncUserFromClerk(...)
user.deletedDelete User record
organization.createdsyncOrgFromClerk(...)
organization.updatedsyncOrgFromClerk(...)
organizationMembership.createdsyncMembership(...)
organizationMembership.updatedsyncMembership(...)
organizationMembership.deletedremoveMember(...)
organizationInvitation.createdcreateInvite(...)
organizationInvitation.acceptedMark invite accepted
organizationInvitation.revokedrevokeInvite(...)

Auth Repository (repositories/auth.repository.ts)

All Prisma queries for auth data are delegated here:

  • User CRUD (findByClerkId, upsert, delete)
  • Org CRUD (findByClerkId, upsert)
  • Membership queries (findMembership, upsertMembership, deleteMembership, listByOrg)
  • Invite queries (create, findById, findByOrg, findByToken, delete, updateInviteToken — replaces an invite's token/expiry in place for resend)

CLI Auth Flow

  1. CLI calls GET /api/v1/auth/cli-auth?port={localPort}&state={random}.
  2. Server redirects to Clerk hosted sign-in with a callback to GET /api/v1/auth/cli-callback.
  3. After Clerk auth, the callback exchanges the Clerk session for a Cosmo JWT.
  4. Server redirects to http://localhost:{port}/callback?token=...&state=... — the CLI's local callback server (path is /callback, not /; includes the state parameter for CSRF protection).

Desktop Auth Flow

  1. Desktop opens the web app's auth page in the system browser.
  2. Web app authenticates via Clerk, then calls POST /api/v1/auth/exchange-token with the Clerk JWT.
  3. Server verifies the Clerk token, syncs user/org, issues a Cosmo JWT.
  4. Web app deep-links back to the desktop with cosmo://auth?token=....