Skip to main content

Authentication

Sendall never asks for a password, and connecting a wallet never costs a fee. Signing in is a signed message, not a transaction.

Signing in: SEP-53 in three calls

  1. POST /api/auth/challenge with your public key. The server builds a plain-text message ("<domain> wants you to sign in with your Stellar account," the public key, a random nonce, an issued-at and expiry timestamp) and returns it alongside an opaque JWT that encodes the same claims. The message itself is never stored server-side; it's rebuilt from the token's claims when verifying, so a client can't replay an old token against a different message.
  2. Your wallet signs that message text with its SEP-53 "sign message" capability. This is a detached signature over arbitrary text, not a transaction, so it never touches the network and never costs anything.
  3. POST /api/auth/verify with the public key, the signature, and the token from step 1. The server re-derives the exact message from the token's claims, verifies the signature against it, and, if it checks out, issues a session.

A muxed account (M...) signs with its underlying base G... keypair; verification resolves to that base account before checking the signature.

Sessions

A session is a short-lived JWT (sendall_session, one day) carrying just the authenticated public key, plus a longer-lived refresh token (sendall_refresh, 30 days) that can silently mint a new session without asking for another signature. The refresh token is single-use: redeeming it deletes the row and issues a new one (rotateRefreshToken in src/lib/auth/refreshToken.ts), so a stolen, already-used token is worthless. Only its SHA-256 hash is ever stored.

Logging out revokes the current refresh token and clears both cookies.

Anonymous sessions

A visitor with no wallet connected still gets an anonymous session, an opaque id in its own cookie, that scopes any batch they draft. resolveBatchAccess (src/lib/auth/batchAccess.ts) checks the authenticated session first; if there isn't one, it falls back to the anonymous id. A visitor with neither cookie has access to nothing, rather than being handed a fresh anonymous identity just by looking.

This is what lets batch drafting work without a wallet: every batch route checks access through the same batchAccessWhere helper, so an anonymous draft and a claimed batch are authorized the same way, just against a different identity.