Luxxon is wallet-only. There are no usernames, passwords, or OAuth
providers. Two ways to authenticate:
| Path | Used by | Carries |
|---|
API key — Authorization: Bearer lxxn_* | Server-side integrations, agents, jobs | A workspace identity + scope set |
Wallet session — Cookie: lx_session=… | The dashboard, after a SIWE sign-in | A wallet address + (optionally) a picked workspace and role |
Both paths resolve, at the guard, to a workspace-scoped principal. From
there, every authorization check is “is this principal entitled to act
on this resource?” — workspaceId is the answer in both cases.
API keys
Authorization: Bearer lxxn_live_abc123_<secret>
The key is sha256-hashed at rest; the plaintext is shown exactly once,
at creation time. Keys are scoped to a single workspace and carry a
list of capability scopes (sessions:read, wallet:read, etc.) — see
API keys.
What gets attached to the request:
{
kind: "api_key",
workspaceId, keyId,
scopes: ["sessions:read", ...],
environment: "LIVE" | "TEST"
}
Workspace binding is strict: any endpoint that takes a workspaceId
parameter must match the key’s bound workspace.
Wallet sessions (dashboard)
The dashboard signs in by proving control of a wallet, then picking
which workspace to act as.
POST /api/v1/auth/wallet/challenge
{ "walletAddress": "0xAbC…" }
→ { "nonce": "…", "message": "Luxxon — sign in to prove …", "expiresAt": "…" }
POST /api/v1/auth/wallet/login
{ "walletAddress": "0xAbC…", "nonce": "…", "signature": "0x…" }
→ Set-Cookie: lx_session=<HMAC-signed payload>
→ { walletAddress, workspaces: [{ id, slug, name, role }, …] }
POST /api/v1/auth/workspace/select (requires lx_session cookie)
{ "workspaceId": "uuid" }
→ Set-Cookie: lx_session=<re-issued with workspaceId + role>
→ { workspaceId, role }
POST /api/v1/auth/logout → clears the cookie
The cookie is an HMAC-signed payload (no server-side state), 12-hour
TTL, HttpOnly, SameSite=Lax. Signature verification on every
request happens in constant time.
Signature support: EOAs via ECDSA recovery, and ERC-1271 smart contract
wallets via isValidSignature (Coinbase Smart Wallet, Safe, etc.) —
viem handles both transparently against a Base RPC. The verifying
chain is configurable (Base mainnet in prod, Base Sepolia in early
access).
What gets attached to the request:
{
kind: "wallet_session",
walletAddress: "0xAbC…",
workspaceId?: string, // present after /workspace/select
role?: "OWNER" | "ADMIN" | "VIEWER"
}
Routes that need a picked workspace (everything past /me and
/workspaces) reject sessions still in the wallet-only state with
INVALID_INPUT: workspaceNotSelected.
Authorization
Two layers:
Scope: "is this credential allowed to call this verb?" (API keys only)
Membership: "is this wallet/workspace entitled to this resource?"
API keys pass the scope check first; if the resource is scoped to
the key’s workspace (sessions on that workspace, the workspace’s
wallet, etc.) the FK match is the rest of the answer.
Wallet sessions carry the role chosen at workspace-select time.
Per-resource membership is checked via SpiceDB at that pick moment;
the cookie’s (workspaceId, role) is the cached result.
SpiceDB’s responsibility is narrow: it models the wallet → workspace
membership graph. Session-level authorization (consumer/operator) is
a direct FK match on consumer_workspace_id / operator_workspace_id
against the principal’s workspaceId.
Workspace creation has no auth
POST /api/v1/workspaces/challenge { walletAddress } → nonce + message
POST /api/v1/workspaces { walletAddress, signature, nonce, slug, name, roles }
The wallet signature in the body is the auth — no API key, no
cookie. The signing wallet becomes the workspace OWNER and the first
member.
/me — who am I
curl https://api.luxxon.dev/api/v1/me -H "Authorization: Bearer lxxn_live_…"
curl https://api.luxxon.dev/api/v1/me --cookie "lx_session=…"
Returns the principal kind plus identity context — useful for quickly
confirming a credential works.
Rotation + revocation
API keys: see API keys. Mint new, revoke
old, the revoked key keeps working for 60s while your fleet rolls.
Wallet sessions: expire after 12h. To rotate sooner, sign in again.
There’s no server-side revocation list — leaked cookies remain valid
until the TTL elapses. Production-grade revocation can come later if
the threat model demands it.
Logging
keyId + workspaceId on every API-key call. walletAddress + workspaceId
on every wallet-session call. Plaintext API keys and signatures are
never logged.
The verify endpoint /me is the cheapest way to confirm a credential
works and inspect what it carries.