Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

@worldcoin/walletkit-web

@worldcoin/walletkit-web brings WalletKit to web apps: a browser app can register a World ID account, keep credentials in encrypted browser storage, and generate World ID proofs. Each call to initializeWalletKit starts a dedicated Web Worker that runs WalletKit’s Rust code, compiled to WebAssembly (WASM), off the main thread, and returns one WalletKit instance that talks to it.

Requirements

To run WalletKit, your app needs the following:

  • A secure context: HTTPS, or localhost during development
  • A browser with module workers and the Origin Private File System (OPFS), including FileSystemSyncAccessHandle
  • A bundler that emits assets referenced with new URL(…, import.meta.url), such as Vite or the Next.js bundler, or custom asset URLs

Cross-origin isolation headers aren’t required.

Get started

  1. Install the package:

    npm install @worldcoin/walletkit-web
    
  2. In browser code, initialize WalletKit, and close the instance in a finally block. An open instance keeps the origin’s storage, so closing it even when a later step fails lets a new instance start. Run the remaining steps inside the try block:

    import { initializeWalletKit } from "@worldcoin/walletkit-web";
    
    const walletkit = await initializeWalletKit();
    try {
      // Steps 3 to 5 go here.
    } finally {
      await walletkit.close();
    }
    
  3. Open the account’s encrypted storage:

    declare const seed: Uint8Array; // The account's authenticator seed.
    declare const databaseKey: Uint8Array; // See "Protect the database key".
    
    const keys = await walletkit.StorageKeys.fromBytes(databaseKey);
    const paths = await walletkit.StoragePaths.fromRoot("/walletkit/my-account");
    const store = await walletkit.CredentialStore.new(paths, keys);
    
  4. If the account isn’t registered, register it and wait until the registration is final:

    const registration =
      await walletkit.InitializingAuthenticator.registerWithDefaults(
        seed,
        undefined, // rpcUrl: use the default.
        "staging",
        "eu",
        undefined, // recoveryAddress: none.
      );
    try {
      for (;;) {
        const status = await registration.pollStatus();
        if (status.state === "finalized") break;
        if (status.state === "failed") throw new Error(status.error);
        await new Promise((resolve) => setTimeout(resolve, 2000));
      }
    } finally {
      registration.free();
    }
    
  5. To answer a relying party’s proof request, open the registered account and generate a proof:

    declare const requestJson: string; // The relying party's proof request.
    
    const authenticator = await walletkit.Authenticator.initWithDefaults(
      seed,
      undefined, // rpcUrl: use the default.
      "staging",
      "eu",
      await walletkit.EmbeddedZkArtifacts.new(),
      store,
    );
    await authenticator.initStorage(BigInt(Math.floor(Date.now() / 1000)));
    const request = await walletkit.ProofRequest.fromJson(requestJson);
    const proofJson = await (
      await authenticator.generateProof(request)
    ).toJson();
    

Importing the package is safe during server-side rendering; call initializeWalletKit only in the browser. For a complete registration, issuance, and proof flow, see the Next.js example.

Protect the database key

In the browser, your app supplies the 32-byte key that encrypts the credential store. On iOS and Android, WalletKit creates this key and seals it with the device keystore; browsers have none, so your app derives the key, for example from a passkey’s PRF output, and passes it to StorageKeys.fromBytes.

  • Never store the key in localStorage or anywhere else that scripts can read. The key is the stored credentials’ only protection.
  • Derive the key the same way every time, and keep a stable root path per account. Reopening a store needs both.
  • Clear your own copies. WalletKit clears the copy that it receives and never persists the key, but it doesn’t modify your array.

Store credentials

WalletKit keeps its encrypted SQLite databases in OPFS, subject to the browser’s storage quota and eviction policy. It never falls back to in-memory storage.

Only one instance per origin can own the storage. If another tab or instance owns it, initializeWalletKit rejects, even for a different root path. Because a closed instance can hold the storage briefly, initialization retries for up to about three seconds first.

To add credentials from an authenticated vault backup, call store.mergeVaultFromBackup(backupBytes).

Use the API

The classes, methods, and arguments mirror the Swift and Kotlin bindings, in camelCase. The TypeScript declarations in dist/index.d.ts are the complete API reference. Constructors and static functions are properties of the instance, for example walletkit.FieldElement.fromU64(1n).

The browser API differs from the Swift and Kotlin bindings in these ways:

  • Every WalletKit function and method returns a Promise, except free(), terminate(), and isStopped(), which return immediately.
  • A u64 is a bigint, a byte array is a Uint8Array, and a 256-bit value is a 0x-prefixed hex string.
  • Environment, Region, and other enums are lowercase strings, such as "staging" and "eu". Records are plain objects.
  • Only generateProof and proveCredentialSub default their now argument, to the browser clock. Pass Unix seconds everywhere else.
  • Objects stay in the worker until you call free() or they are garbage collected.

Extract a Selfie Check embedding

const result = await walletkit.extractSelfieEmbedding(
  JSON.stringify(trustedEnrollmentRelease),
  imageBytes,
);
if (result.status === "success") {
  // Sensitive biometric data: keep it out of logs and persistent storage.
  const embedding = result.embedding;
}

Use the trusted client-stage.json generated by the enrollment release in worldcoin/tee-apps. It pins the endpoint and Nitro PCR0/1/2, which the client verifies before sealing the image. A failed attestation never causes an image upload, and no bypass option is exposed. imageBytes must be between 1 byte and 8 MiB; any other size throws a TypeError before a connection is opened.

The client requests and verifies an enclave assignment, then sends one sealed image and receives one sealed result over the same WebSocket. Access control belongs to the upstream proxy; this API has no ticket issuer or admission callback.

The response is SelfieEmbeddingResult: a success containing FaceEmbedding, or a typed image/quality failure. FaceEmbedding follows the existing Face PCP contract: embedding (the original encoded vector), embeddingType, embeddingVersion, and embeddingInferenceBackend. This operation extracts an embedding and does not issue a credential or prove camera liveness. The experimental staging service connects directly to the TEE host. Proxy protection is planned separately.

Handle errors

WalletKit reports failures in these ways:

  • A WalletKit error rejects with an Error whose name is the error type, such as StorageError, and whose code is the variant, such as NullifierReplay. Hex secrets in the message are redacted.
  • An invalid argument rejects with a TypeError.
  • If storage setup fails, initializeWalletKit rejects with a StorageError whose code is PersistentStorage.
  • A crash in WalletKit’s Rust code stops the instance, and every later call rejects. To recover, initialize a new instance.

WalletKit logs its own warnings and errors to the worker’s console. To redact hex secrets from your own messages before you log them, call walletkit.sanitizeHexSecrets(text).

Prove credential ownership

authenticator.proveCredentialSub(nonce, context, blindingFactor, sub) generates a WIP-103 ownership proof in the worker and resolves with its CBOR encoding, the format issuers accept. Call it only from a direct user action, never from an issuer, backend, or unauthenticated request flow.

Proving runs in the worker and blocks the calls queued after it. To cancel, call terminate() and initialize a new instance.

Manage the instance

The worker runs one call at a time, in order, and calls have no timeout. A network request that never settles blocks the calls queued after it.

  • close() waits for queued calls, frees every object, and stops the worker. After five seconds without a response, it stops the worker anyway and rejects.
  • terminate() stops the worker immediately and rejects pending calls.
  • isStopped() returns true once the instance can no longer serve calls.

Serve the assets from custom URLs

The package ships a worker script and a WASM module of about 40 MB. If your app serves them itself, pass their URLs:

const walletkit = await initializeWalletKit({
  workerUrl: "/assets/walletkit.worker.js",
  wasmUrl: "/assets/walletkit.wasm",
});

Relative URLs resolve against the page URL. Serve the worker from your page’s origin, and serve the WASM module as application/wasm.

Browser limitations

These parts of the Swift and Kotlin bindings aren’t available in the browser:

APIReason
Logger and initLoggingNot implemented for the browser.
Vault and activity change listenersNot implemented for the browser.
Vault backup export and replacement importSupported only on iOS and Android. Use mergeVaultFromBackup to import.
FlamingoBrowser WebSockets can’t send its signed handshake headers, and it relies on device attestation.
DeviceKeystore, AtomicBlobStore, and StorageProviderThey need synchronous host callbacks. Use StorageKeys.fromBytes.
CachingZkArtifactsThe WASM module embeds the proving keys. Use EmbeddedZkArtifacts.
UserAgent and UserAgentBuilderThe browser controls the User-Agent header.
Issuer clients (TfhNfcIssuer, RecoveryBindingManager)Not included.
World ID v3 (walletkit_core::v3)Not included.

Get help

To report a bug or ask a question, open an issue. To contribute, see the browser package docs.