World authentication

world-auth

One wallet sign-in flow, shared across several applications.

world-auth-public

I maintain the World wallet-authentication library used by Pebbler World, Bridge, and five World mini games. It shares the sign-in and session mechanics between browser clients and backend services while each application keeps its own account model and permissions.

The public edition exposes the integration contracts around MiniKit wallet commands, Sign-In with Ethereum verification, signed challenges, and encrypted access and refresh credentials. The central question is how a wallet response becomes a session the backend can trust.

@bridge-applications/world-authworld-authWorld → authenticated session

Sharing mechanics across different products

A micro-task application, a file-sharing tool, and a mini game use authenticated sessions for different reasons. They still need the same underlying sequence: request a challenge, ask the wallet to sign it, verify the proof, and maintain credentials for subsequent API requests.

The package gives that sequence a shared implementation without owning the application’s user database or HTTP routes. MiniKit is supplied by the host application. Separate browser, server, and schema entry points keep server verification dependencies out of the frontend bundle, while runtime schemas make the client/server contract explicit.

Binding a wallet proof to the application session

The backend generates a signed challenge and binds its nonce to a trusted browser session or an HttpOnly challenge cookie. The browser passes that nonce to MiniKit’s wallet-authentication command, then returns the wallet response for server verification.

Checking the wallet signature is only one part of accepting the login. The verifier also checks the expected nonce, domain, URI, chain, wallet address, and message timestamps. Expected values come from server configuration and session state, independently of the submitted proof. Successful verification establishes control of a wallet; World ID verification and application permissions are separate decisions.

Verify against trusted session and application valuesTypeScript
const accepted = await auth.verifySiwe(walletProof, submittedNonce, {
  expectedNonce: challengeSession.nonce,
  domain: appConfig.domain,
  uri: appConfig.uri,
  chainId: appConfig.chainId,
});

if (!accepted) {
  throw new Error('Wallet sign-in rejected');
}

Illustrative integration excerpt. The configured verifier checks the submitted proof against an independently retrieved session nonce and trusted application configuration.

Wallet login reference

Making challenge consumption atomic

A signed challenge proves issuance and expiry, but it does not by itself prevent two backend instances from accepting the same login. Checking whether a nonce has been used and then recording it as a separate operation leaves a race between those requests.

Nonce consumption therefore happens atomically in a shared store. Redis uses SET NX with a lifetime tied to the challenge’s expiry; application-supplied stores must provide the same guarantee across processes. Only a valid proof consumes the challenge. Replays, missing stores, and store failures reject the login rather than allowing verification to proceed without that guarantee.

Challenge and storage contracts

Separating access, refresh, and invalidation

After verifying the wallet proof, the application resolves its user record and requests access and refresh credentials. Their encrypted envelopes include a version, an application namespace, and a distinct token purpose. Claims are validated when issued and when verified, so decrypted data is still checked against the application’s schema.

An account-wide refresh-group counter provides invalidation across backend instances. Incrementing it invalidates existing refresh credentials for the user, while existing access credentials remain valid until expiry. This is a deliberate scope: group invalidation does not replace each refresh token on use or immediately revoke an access token. Durable counters and short access lifetimes are part of the deployment contract.

Handling overlapping browser operations

The browser client coordinates wallet sign-in, cached access credentials, and refresh requests. Access credentials live in memory unless storage is explicitly configured; the refresh credential remains in an HttpOnly cookie. Multiple callers share an in-flight refresh, and a failed request releases it so a later attempt can retry.

Logout clears local state immediately and advances the session generation. A late response from the previous generation cannot restore authentication. The distinction between local state and server state still matters: if the logout HTTP request fails, the server cookie may remain valid and the application needs to handle that failure. Deferred credential commits are also available when an application must coordinate authentication with other state updates.

Browser client reference

Reusing the flow in production

Pebbler World uses the package for wallet sign-in and authenticated sessions. Bridge uses it to protect file-transfer operations. Brain Bus, UnScrewed!, Picture Jam, Match & Win, and Color Jam share the browser integration through the internal htcc-kit package and use WorldAuthServer in their shared backend.

The public repository checks real encrypted tokens, signed challenges, a signed SIWE proof, asynchronous session races, packed consumers, browser bundle isolation, and a Workers runtime smoke test. Native World App interactions, deployed cookie behavior, Redis durability, and contract-wallet RPC availability need checks in the consuming application’s environment. Keeping those boundaries explicit is part of making the package reusable.

Explore the public repository HTTP integration responsibilities Pebbler on World case study Bridge case study Telegram authentication case study