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

POST
/v1/auth/login
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

POST
/v1/auth/verify-otp
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
}

Authorization header

The token is sent bare for user sessions. Two reserved prefixes carry different token classes.

Token classHeader valueUsed by
User sessionAuthorization: <token>Logged-in humans, the only flow today
BotAuthorization: Bot <token>Server-side bots (reserved)
OAuth bearerAuthorization: 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

POST
/v1/auth/logout
curl -X POST https://api.localuniverse.io/v1/auth/logout \
  -H "authorization: <token>"

Sessions

List and revoke active sessions for the current user.

MethodPathNotes
GET/v1/users/@me/sessionsAll active sessions — device, browser, country, last seen
DELETE/v1/users/@me/sessionsRevoke every session except the current one
DELETE/v1/users/@me/sessions/:sessionIdRevoke 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

ClientWhere the token lives
WeblocalStorage at key auth_token
iOS nativeKeychain
Android nativeEncryptedSharedPreferences / Keystore
ElectronsafeStorage (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.

Was this page helpful?