Skip to content

Auth & Accounts

apps/api/src/router/authRoutes.ts · the only router that is public by default.

Endpoints

MethodPathAuthPurpose
POST/authenticateLog in, returns { user, token }
POST/create-accountRegister, returns { user, token }
POST/logoutLog out
POST/check-usernameIs a username taken?
POST/check-username-offensiveDoes a username contain profanity?
POST/forgot-passwordStub
POST/reset-passwordStub
POST/verify-identitySubmit identity verification
PUT/auth/change-passwordChange password (in userRoutes)
POST/auth/logout-allRevoke every session (in userRoutes)

POST /authenticate

json
{ "email": "user@example.com", "password": "…" }
json
{
  "user": { "id": "clx…", "email": "…", "username": "…", "onboarded": true, "userType": "user" },
  "token": "eyJhbGciOiJIUzI1NiIs…"
}

Also creates an AuthSession audit record capturing device name, IP, and user agent. Invalid credentials return an error from NotFoundError — the same message for "no such user" and "wrong password", which is the right choice for enumeration resistance.

Store the token and send it as Authorization: Bearer <token> on every subsequent request. It is valid for 7 days; there is no refresh token, so the client re-authenticates when it expires.

POST /create-account

json
{ "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com", "password": "…" }

Registration does three things beyond creating the user:

  1. Hashes the password with bcrypt.
  2. Creates a LiveStream record with Mux credentials — every user is stream-ready from day one.
  3. Creates an AuthSession.

The response matches /authenticate. Note that username is not set at registration — it is chosen during onboarding, so User.username is nullable.

Username checks

json
POST /check-username
{ "username": "gamer123" }
→ { "exists": false }

POST /check-username-offensive runs the trie-based filter in apps/api/src/lib/offensive-words-validator/. Call both before letting a user commit to a name.

PUT /auth/change-password

Requires authentication. Changing a password should also increment tokenVersion so other devices are logged out — verify that behaviour before relying on it.

POST /auth/logout-all

The real revocation mechanism:

ts
prisma.user.update({ where: { id: userId }, data: { tokenVersion: { increment: 1 } } });
prisma.authSession.updateMany({ where: { userId }, data: { isActive: false } });

Incrementing tokenVersion invalidates every JWT ever issued to that user, because the auth middleware compares the token's version against the user's. See Authentication.

POST /logout

Client-side logout. Since JWTs are stateless, this cannot invalidate the token — the client discards it. Use /auth/logout-all when you actually need revocation.

POST /verify-identity

Part of creator onboarding, alongside POST /verification-asset for uploading identity documents. The mobile flow is id-captureselfiereview-pending (apps/mobile/app/(onboarding)/).

Password reset is not implemented

ts
export async function forgotPassword(email: string) {}
export async function resetPassword(token: string, password: string) {}

Both routes exist and both service functions are empty. RESET_SECRET is configured in anticipation. Implementing this needs an email provider — packages/transactional holds templates but is not wired to one. Planned

Internal documentation — PlayPalz platform