Web Client Setup Guide
This guide covers the configuration required to use the Miden web SDK (@miden-sdk/miden-sdk) in a Next.js application. These settings apply to all web tutorials in this section.
Prerequisites
- Node.js 20+ (Node 22+ requires an extra
localStoragepolyfill — see below) - Next.js 14+ with App Router
- yarn or npm
Install the SDK
yarn add @miden-sdk/miden-sdk
For React hook support:
yarn add @miden-sdk/react
These tutorials use Next.js, so all code examples import from the SDK's /lazy subpath — see Entry points: eager vs lazy below for why that's required.
Next.js Configuration
Create or update next.config.ts with these required settings:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Static export avoids runtime SSR entirely.
// The SDK is browser-only, so static export is recommended.
output: 'export',
trailingSlash: true,
skipTrailingSlashRedirect: true,
experimental: {
// Required for the SDK's ESM bundle to resolve correctly in webpack.
esmExternals: 'loose',
},
webpack: (config) => {
config.experiments = {
...config.experiments,
// Required: the SDK loads a WASM binary for Miden VM operations.
asyncWebAssembly: true,
topLevelAwait: true,
};
// Serve .wasm files as static assets.
config.module.rules.push({
test: /\.wasm$/,
type: 'asset/resource',
});
return config;
},
};
export default nextConfig;
Importing .masm files (for smart contract tutorials)
If your tutorials use Miden assembly (.masm) files, add this webpack rule inside the webpack callback:
// Import .masm files as plain text strings.
config.module.rules.push({
test: /\.masm$/,
type: 'asset/source',
});
Then create lib/masm/masm.d.ts so TypeScript recognizes the imports:
declare module '*.masm' {
const content: string;
export default content;
}
- Vite: use the
?rawsuffix —import code from './masm/counter_contract.masm?raw' - No bundler: use
fetch()at runtime —const code = await fetch('/masm/counter_contract.masm').then(r => r.text())
Entry points: eager vs lazy
Starting with @miden-sdk/[email protected], the SDK ships two entry points:
- Default entry (
@miden-sdk/miden-sdk,@miden-sdk/react) — awaits WASM initialization at module top level. Ergonomic for Vite and plain-browser projects: import the SDK and construct wasm-bindgen types on the next line, no ceremony. Not usable from Next.js App Router — top-levelawaitblocks the server render phase. /lazysubpath (@miden-sdk/miden-sdk/lazy,@miden-sdk/react/lazy) — synchronous import with no top-levelawait. The caller is responsible for awaiting WASM readiness before constructing any wasm-bindgen type. This is the correct entry for Next.js.
In raw TypeScript, gate every function body on MidenClient.ready():
import { MidenClient } from '@miden-sdk/miden-sdk/lazy';
export async function doSomething() {
if (typeof window === 'undefined') return;
await MidenClient.ready();
// Safe to construct wasm-bindgen types from here.
const client = await MidenClient.create({
rpcUrl: 'https://rpc.testnet.miden.io',
});
// …
}
In React, the @miden-sdk/react/lazy provider manages WASM readiness for you via the isReady flag returned by useMiden(). Gate any wasm-bindgen-touching code on isReady:
import { useMiden, useCreateWallet } from '@miden-sdk/react/lazy';
function Component() {
const { isReady } = useMiden();
const { createWallet } = useCreateWallet();
return (
<button
onClick={() =>
createWallet({
/* … */
})
}
disabled={!isReady}
>
{isReady ? 'Create wallet' : 'Initializing…'}
</button>
);
}
Never construct wasm-bindgen types (AccountId, Note, createP2IDNote, TransactionRequestBuilder, etc.) at module top level or in a render-body useMemo — always inside an effect, event handler, or async hook callback where WASM is already initialized. For display-only cases like shortening an address, slice the bech32 string directly (addr.slice(0, 8) + '…' + addr.slice(-4)); don't parse it with AccountId.fromBech32() just to get a prefix.
Node.js 22+ localStorage polyfill
If you run next dev under Node.js 22 or later, every page request will crash with:
TypeError: localStorage.getItem is not a function
This is a Node + Next.js interaction, not a Miden SDK issue. Node 22+ defines globalThis.localStorage as an object, but its methods (getItem, setItem, …) are undefined unless Node is launched with --localstorage-file. Next.js's dev overlay guards with typeof localStorage !== 'undefined', which passes on Node 22+, and then calls the missing methods.
Add this polyfill at the top of next.config.ts, before the config object:
{
const store = new Map<string, string>();
const poly = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => {
store.set(key, value);
},
removeItem: (key: string) => {
store.delete(key);
},
clear: () => {
store.clear();
},
get length() {
return store.size;
},
key: (index: number) => [...store.keys()][index] ?? null,
};
(globalThis as Record<string, unknown>).localStorage = poly;
}
This only affects next dev (SSR); static exports via next build are unaffected. The polyfill is harmless on Node ≤21 — it installs an in-memory stub that the dev overlay uses just like Node 22+'s (broken) built-in.
SDK API Patterns
Transaction return types
All transaction methods return an object, not a plain transaction ID:
// mint and consume return { txId, result }
const { txId } = await client.transactions.mint({ ... });
// send returns { txId, note, result }
// note is non-null when returnNote: true
const { txId, note } = await client.transactions.send({
...,
returnNote: true,
});
Waiting for confirmation
You can wait for a transaction to be committed in two ways:
// Option 1: Pass waitForConfirmation in the transaction call
await client.transactions.mint({
...,
waitForConfirmation: true,
});
// Option 2: Wait separately using waitFor
const { txId } = await client.transactions.mint({ ... });
await client.transactions.waitFor(txId); // accepts TransactionId object or hex string
Using transaction IDs in URLs
When displaying transaction IDs in explorer links, call .toHex():
const { txId } = await client.transactions.mint({ ... });
console.log(`https://testnet.midenscan.com/tx/${txId.toHex()}`);
Authentication
Create an authentication key using AuthSecretKey (inside an async function, after awaiting MidenClient.ready() so the wasm-bindgen constructor is live):
import { MidenClient, AuthSecretKey } from '@miden-sdk/miden-sdk/lazy';
export async function createAuth() {
await MidenClient.ready();
const seed = new Uint8Array(32);
crypto.getRandomValues(seed);
const auth = AuthSecretKey.rpoFalconWithRNG(seed);
return { seed, auth };
}
Pass auth and seed when creating contract accounts that require authentication.
Concurrency safety and waitForIdle()
As of @miden-sdk/[email protected], all mutating WebClient methods (transactions.execute, transactions.submit, syncState, account creation) and async proxy-fallback reads (getAccount, importAccountById, getAccountStorage, etc.) are internally serialized through a single promise chain. Consumers no longer need to maintain their own JS-level mutex, and the "recursive use of an object detected" wasm-bindgen panic caused by the 15-second auto-sync timer racing with user operations is gone.
For the rare case where you need to coordinate a non-WASM side effect (for example, clearing an in-memory auth key on wallet lock) with whatever SDK work is currently in flight, drain the queue first:
await client.waitForIdle(); // resolves when every serialized call has settled
clearMyAuthKeys();