Authentication
LocalUniverse signs every authenticated request with a bearer token. Tokens are minted by a two-step email-OTP flow backed by Turnkey — there are no passwords anywhere in the system.
The same token authenticates the REST API, the WebSocket Gateway, and any future surface. There is one token format and it does not vary by client.
Login
Two round-trips. The first sends a code, the second exchanges the code for a session token.
Request a code
Send the user's email. The server provisions a Turnkey sub-org and multi-chain wallet on first contact, creates the user's HOME channel, then mails a one-time code.
The response carries an otp_id you replay on step 2 and a new_user flag the client can use to route into onboarding.
Request
curl -X POST https://api.localuniverse.io/v1/auth/login \
-H "content-type: application/json" \
-d '{ "email": "tino@example.com" }'
Response
{
"otp_id": "ot_19df8a4c12...",
"new_user": true
}
Verify the code
The client generates a throwaway P-256 keypair (use generateP256KeyPair() from @turnkey/crypto) and submits its public key as tpk along with the code. Turnkey returns a credential_bundle encrypted to that public key.
The API also mints its own session token and stamps verified_at on the user row.
- Name
email- Type
- string
- Description
Same email you used on
/login.
- Name
code- Type
- string
- Description
4–12 character code from the email.
- Name
otp_id- Type
- string
- Description
Echo of the id from step 1.
- Name
tpk- Type
- string
- Description
Uncompressed public key from the client's freshly-generated P-256 keypair.
Request
curl -X POST https://api.localuniverse.io/v1/auth/verify-otp \
-H "content-type: application/json" \
-d '{
"email": "tino@example.com",
"code": "123456",
"otp_id": "ot_19df8a4c12...",
"tpk": "04ab..."
}'
Response
{
"user": { "id": "31223...", "email": "tino@example.com", "display_name": "tino", "role": 0 },
"token": "MTk4NjIyNDgzNDcxOTI1MjQ4.aFp2qA.k7Hx9mPq...",
"credential_bundle": "...",
"new_user": true
}
The credential_bundle is opaque to the API. Decrypt it with the private half of your tpk keypair and persist it locally — Turnkey uses it for any chain-signing the client does. The API session token alone is what gates REST + Gateway access.
Authorization header
The token is sent bare for user sessions. Two reserved prefixes carry different token classes.
| Token class | Header value | Used by |
|---|---|---|
| User session | Authorization: <token> | Logged-in humans, the only flow today |
| Bot | Authorization: Bot <token> | Server-side bots (reserved) |
| OAuth bearer | Authorization: Bearer <token> | Third-party apps acting on behalf of a user (reserved) |
The server reads the prefix, looks up the session, and rejects a user token sent with Bot or Bearer. The token itself is three base64url segments — opaque to the client, never parse it.
Authenticated request
curl https://api.localuniverse.io/v1/users/@me \
-H "authorization: MTk4NjIyNDgzNDcxOTI1MjQ4.aFp2qA.k7Hx9mPq..."
Logout
Revokes the bearer token presented in the header. Idempotent — calling on an already-revoked session still returns 204.
To revoke a different session by id, use DELETE /v1/users/@me/sessions/:sessionId. To force-revoke every session except the current one, DELETE /v1/users/@me/sessions.
Request
curl -X POST https://api.localuniverse.io/v1/auth/logout \
-H "authorization: <token>"
Sessions
List and revoke active sessions for the current user.
| Method | Path | Notes |
|---|---|---|
GET | /v1/users/@me/sessions | All active sessions — device, browser, country, last seen |
DELETE | /v1/users/@me/sessions | Revoke every session except the current one |
DELETE | /v1/users/@me/sessions/:sessionId | Revoke a single session by id |
Server-derived fields (user_agent, ip_address, country_code, device_type, browser_*, os_*) are authoritative. Client-supplied display fields (X-Client-Name, X-Client-Version, X-Device-Name) are recorded for display only and never gate authorization.
Storage
| Client | Where the token lives |
|---|---|
| Web | localStorage at key auth_token |
| iOS native | Keychain |
| Android native | EncryptedSharedPreferences / Keystore |
| Electron | safeStorage (OS keychain) |
Web localStorage is intentional — JavaScript needs to read the token to attach it to fetch headers and the WebSocket URL. Defense lives in strict CSP, content sanitization, and short server-revocable tokens. If a token is compromised, revoke it via DELETE /v1/users/@me/sessions/:sessionId.
Tokens are bearer credentials — treat them like passwords. Never log them, never put them in URLs that get sent in Referer headers, never serialize them in error responses.