Start free

Next.js

Use HESO from a Next.js app: load the WASM verifier in the App Router to check receipts client-side, and gate or push receipts from route handlers on the server.

A Next.js app has two runtimes, and HESO has a surface for each. In the browser you verify receipts with the WASM package — no key ever reaches the client. On the server you gate receipts and talk to the cloud control plane with the TypeScript SDK. This guide wires up both, and ends with a note on how the HESO console itself does exactly this.

The two surfaces

Pick the package by where the code runs. The boundary is firm: the WASM verifier is verify-only and holds no signing key, so it is safe in a client component; the SDK reads your API key and calls the cloud, so it belongs on the server.

  • Browser — @hesohq/verify-wasm. A verify-only WASM module (ESM, wasm-bindgen --target web). Use it inside a "use client" component to re-run BLAKE3 and Ed25519 on a receipt locally. There is no signing in the browser.
  • Server — @hesohq/sdk (a thin wrapper over the @hesohq/core native addon). Use it in a route handler, server action, or middleware to gate a receipt and to push receipts, open approvals, and pull policy from the cloud.

Both surfaces call the same Rust core, so a verdict is byte-identical whether it is computed on your server or in a reviewer’s browser. You never re-implement crypto in JavaScript.

Verify in the browser

The WASM module ships as an async initializer plus a set of synchronous named exports. The default export is init(); you must await it once before calling anything else. Cache that promise so the .wasm is fetched and instantiated a single time, then call verifyActionReceipt with the receipt encoded as bytes.

components/receipt-verifier.tsx
'use client' import { useEffect, useState } from 'react'import init, { verifyActionReceipt, type ActionVerdict } from '@hesohq/verify-wasm' // Cache the init() promise module-wide so the WASM is fetched and// instantiated exactly once, no matter how many components mount.let ready: Promise<unknown> | null = nullfunction ensureWasm() {  if (!ready) ready = init()  return ready} export function ReceiptVerifier({ receipt }: { receipt: unknown }) {  const [verdict, setVerdict] = useState<ActionVerdict | null>(null)   useEffect(() => {    let alive = true    ensureWasm().then(() => {      // init() must finish before any named export is called.      const bytes = new TextEncoder().encode(JSON.stringify(receipt))      const v = verifyActionReceipt(bytes)      if (alive) setVerdict(v)    })    return () => {      alive = false    }  }, [receipt])   if (!verdict) return <p>Verifying…</p>  return (    <p>      {verdict.verdict} · {verdict.trust_level}    </p>  )}

verifyActionReceipt returns an ActionVerdict with two fields: verdict (the string "Valid" on success, or a failure variant such as "HashMismatch" or "TrustLevelMismatch:embedded=L1,derived=L0") and trust_level. The verifier resolves at the first failing gate — see offline verification for the full order.

Encode, don't pass the object

The WASM functions take bytes, not JavaScript objects. Encode the receipt with new TextEncoder().encode(JSON.stringify(receipt)) so the canonical-bytes recomputation runs on the exact JSON the receipt was signed over.

Serving the WASM

Because the package targets the web, init() fetches the .wasm file at runtime — the binary is not inlined into your JavaScript bundle. Your bundler must therefore serve that asset so the browser can load it. The package is ESM, so import it from a client component and let your build emit and serve the .wasm next to your other static assets.

  • Keep the import inside a "use client" module so the WASM never lands in the server bundle.
  • Call init() exactly once and reuse the resolved promise; calling a named export before init has resolved will fail.
What the browser verifier proves

A "Valid" verdict in the browser proves the operator authorized this action under a known policy — and, at L1, that a person approved it with a device-held key. The check runs entirely in the browser, with no call back to HESO; it records what was authorized, not whether the action succeeded downstream.

Gate in a route handler

On the server, use @hesohq/sdk. Call configure once at module load with your API key and endpoint, then gate a receipt before you act on it. Gating verifies via @hesohq/core and returns whether the receipt clears your minimum trust level. Use pushReceipt to record it in the cloud, which re-verifies the receipt before accepting it.

app/api/receipts/route.ts
import { NextResponse } from 'next/server'import { configure, gate, pushReceipt } from '@hesohq/sdk'import type { ActionReceipt } from '@hesohq/sdk' // Set the SDK up once at module load — before any cloud call.configure(process.env.HESO_API_KEY!, process.env.HESO_ENDPOINT!) export async function POST(req: Request) {  const receipt: ActionReceipt = await req.json()   // Verify locally via @hesohq/core, requiring a human co-signature.  const result = gate(JSON.stringify(receipt), 'L1')  if (!result.allowed) {    return NextResponse.json(      { error: 'gate_failed', verdict: result.verdict },      { status: 403 },    )  }   // The server re-verifies again before it accepts the receipt.  const push = await pushReceipt(receipt)  return NextResponse.json(push, { status: push.accepted ? 200 : 422 })}

gate returns a GateResult with allowed, trustLevel, and verdict. If you would rather throw than branch, assertGatedoes the same check and raises when the receipt is not allowed. Capturing and signing your own agent’s actions is the job of the Python SDK; in Node you verify and push receipts.

Keep the API key on the server

configure reads your API key. Source it from a server-only environment variable and never import @hesohq/sdk from a client component — it is not built for the browser. The browser only ever runs the verify-only WASM surface.

This very app

The HESO web console is itself a Next.js App Router app, and it uses @hesohq/verify-wasm for its in-browser receipt verifier and its policy editor — the same verifyActionReceipt, parsePolicy, and evaluatePolicy calls documented here, running entirely client-side. The pattern in this guide is how the product itself works.