Auth and Tokens
One bearer header, two credential kinds. The backend branches on the pat_ prefix.
Credential kinds#
- Session JWT: minted by Google OAuth in the browser (sign in at shanejli.com). Full power: bypasses all scope checks and all per-minute rate limits.
- Personal access token (PAT):
pat_+ 32 random bytes base64url. Scoped and rate limited. This is what agents should use.
Send either as Authorization: Bearer <token>.
Minting a PAT#
Easiest: sign in and mint at https://shanejli.com/settings/tokens.
Programmatic (requires a session JWT; a PAT can never mint another PAT, by design):
curl -X POST https://shanebackend-production.up.railway.app/api/auth/tokens \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"name":"my-agent","scopes":["entries:write","comments:write"]}'
# 201 {"id":"<uuid>","token":"pat_..."} raw token shown exactly once
- List:
GET /api/auth/tokens. Revoke:DELETE /api/auth/tokens/:id(204). Both need a session JWT, with one exception: a PAT may revoke itself (its own id). Any other PAT call gets 403. - PATs never expire; revocation is the only kill switch.
- Who am I:
GET /api/auth/mereturns your user object for either credential kind.
Scopes#
entries:write, suggestions:write, comments:write, reactions:write, knowledge:write, trips:write, practice:write
A PAT without the required scope gets 403 Token missing required scope: <scope>. A route shared by several features accepts any one of their scopes and says so: journal image uploads take entries:write, suggestions:write, comments:write or knowledge:write and answer 403 Token missing required scope: one of entries:write, suggestions:write, comments:write, knowledge:write otherwise. Some routes need only authentication, no scope (for example RNG Capitalist).
Rate limits (PATs only, rolling 60s, 429 + Retry-After)#
| Bucket | Limit/min | Covers |
|---|---|---|
| journal-entries-write | 30 | entry create, append, revert, trash |
| journal-suggestions-write | 30 | suggest, approve, reject, withdraw |
| journal-comments-write | 30 | journal comments |
| journal-reactions-write | 60 | journal reactions |
| courses-write | 60 | all courses writes |
| scoreboard-write | 60 | all scoreboard writes |
| tea-entries-write | 30 | tea writes |
| skincare-write | 60 | skincare writes |
| knowledge-notes-single / -batch | 30 / 5 | note ingest |
| knowledge-entries-write | 30 | knowledge entry create, edit, delete, bulk-delete; connection create, delete |
| knowledge-enrich | 10 | knowledge entry enrich (one LLM call each) |
| knowledge-comments-write | 30 | knowledge comments |
| vocabulary-writes / vocabulary-enrich | 30 / 10 | vocabulary writes / enrich |
| practice-* | 30 (sync and vocab reviews 120) | practice writes |
| rng-capitalist-evaluate / -plaid | 10 / 10 | rng evaluate / budget |
RNG Capitalist bank linking (POST /api/rng/plaid/link-token and /plaid/exchange) needs a session JWT: any PAT gets 403 Bank linking requires a browser session, not a PAT. The rest of /api/rng accepts any PAT.
The loans ledger (/api/loans, Who Owes Me) is browser-session only, reads included: any PAT gets 403 Loans requires a browser session, not a PAT.
Tea entries (/api/tea-entries) take entries:write for reads as well as writes. Without it the list answers 403 Token missing required scope: entries:write, and GET /api/tea-entries/:id treats the PAT like anyone else: 401 PIN required until it sends the right X-Tea-Pin, and the entry's PIN is never in the response.
Skincare (/api/skincare) is the same: GET /api/skincare and GET /api/skincare/search need entries:write, like every skincare write, and answer 403 Token missing required scope: entries:write without it.
Journal image uploads have a separate 100 per rolling 24h per-user quota (applies to JWTs too) with an honest Retry-After.
Blitz sync sessions (browser only)#
POST /api/blitz/sync-session with a browser JWT returns { baseUrl, accessToken, encryptKey, email, expiresAt }: the SuperSync server URL, a 365-day SuperSync token, and the caller's encryption password (created on first call, identical on every device). PATs get 403. The Blitz app calls it through the popup at /blitz/connect; agents have no reason to call it.
Blitz is retired: it is no longer an element on this site. The endpoint stays up only so existing Blitz installs keep syncing until the sync server is shut down, and it will be removed then.
Admin-gated surfaces#
A few routes are JWT-plus-admin-email only (practice settings PATCH, activity ingest). PATs always get 403 there; agents should skip them.