Authentication
platform-api routes are protected by ClerkAuthGuard, applied per-controller. The guard accepts two kinds of credential in the Authorization header:
| Credential | Header | Best for |
|---|---|---|
| Personal Access Token | Authorization: Bearer mr_pat_… | Scripts, CI, server-to-server, MCP, any non-browser integration |
| Clerk session JWT | Authorization: Bearer <clerk-jwt> | Browser sessions where the user is already signed in via Clerk |
The guard tries the PAT path first (any Bearer mr_pat_… token), and falls back to Clerk session verification otherwise.
agent-service endpoints (/api/chat, /api/agent, /api/graph, /api/pdf, /api/translate, /api/interview) are not Clerk-guarded at the controller level — they are designed to sit behind the gateway and be called by trusted callers. Do not expose them directly to untrusted clients without a proxy that enforces auth.
Personal Access Tokens (PAT)
A PAT is a long-lived bearer credential bound to your user. The plaintext is mr_pat_ + a base64url random string; the server stores only its SHA-256 hash, so the token is shown exactly once at creation.
Mint a token
You must already be authenticated (via a Clerk session) to create the first PAT. Tokens are created through the platform-api:
Create
curl -X POST http://localhost:3111/api/users/me/personal-access-tokens \
-H "Authorization: Bearer <clerk-jwt>" \
-H "Content-Type: application/json" \
-d '{ "name": "Codex on MacBook", "scopes": ["resume:read", "resume:write"] }'Copy the token from the response
{
"code": 200,
"data": {
"id": "clx...",
"name": "Codex on MacBook",
"tokenPrefix": "mr_pat_AbCd1234",
"tokenPreview": "mr_pat_AbCd1234...",
"scopes": ["resume:read", "resume:write"],
"token": "mr_pat_AbCd1234xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"createdAt": "2026-06-19T08:00:00.000Z"
},
"message": "success",
"timestamp": "2026-06-19T08:00:00.000Z"
}data.token is returned only on creation. Store it in a secret manager immediately. On every later listing you get tokenPreview (mr_pat_AbCd1234...) but never the full value again.
Use it
curl http://localhost:3111/api/resumes/mine \
-H "Authorization: Bearer mr_pat_AbCd1234xxxxxxxxxxxxxxxxxxxxxxxxxxxx"Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✅ | 1–80 chars, human-readable label |
expiresAt | ISO 8601 string | — | Optional expiry; omit for a non-expiring token |
scopes | string[] | — | Defaults to ["resume:read", "resume:write"] |
Manage tokens
| Action | Method & path |
|---|---|
| List your tokens | GET /api/users/me/personal-access-tokens |
| Create a token | POST /api/users/me/personal-access-tokens |
| Revoke a token | PATCH /api/users/me/personal-access-tokens/:id/revoke |
A revoked or expired token stops authenticating immediately. The server stamps lastUsedAt on each successful use, so you can audit activity in the listing.
Roles & RBAC
Routes can require roles via @Roles('user', 'admin') (the default, when unspecified, is admin). Your PAT inherits your user’s role — so a PAT for a normal user can hit user-level routes but not admin-only ones (for example, listing all feedback). If your role is insufficient, the API responds 401 with Insufficient permissions. Required: ….
Clerk session JWT
In a browser where the user is signed in through Clerk, send the session token Clerk issues:
Frontend (fetch)
const token = await window.Clerk.session.getToken()
const res = await fetch('https://api.magic-resume.cn/api/resumes/mine', {
headers: { Authorization: `Bearer ${token}` },
})For non-browser integrations, prefer a PAT — it doesn’t expire with a session and is trivial to rotate and revoke.