Lof Web Integration Guide
This guide walks through integrating a Lof circuit into a React + Node.js stack, from writing the circuit to serving the prover and verifier assets.
1. Author your circuit
Create a .lof file (for example, src/circuits/age_proof.lof) inside your project and implement your circuit logic there:
proof AgeProof {
// Public inputs
input current_year: field;
input minimum_age: field;
input is_of_age: field;
// Private witness
witness birth_year: field;
// The prover must assert they are of age; this bit is also constrained to 0/1.
assert is_of_age * (1 - is_of_age) === 0;
assert is_of_age === 1;
// Compute the prover's age from the witness and ensure it meets the requirement.
let age = current_year - birth_year in
assert age >= minimum_age;
// Return the eligibility bit so downstream systems can re-use it.
is_of_age
}
2. Compile the bundle
Produce a web-ready bundle of proving and verification assets:
lof compile src/circuits/age_proof.lof \
--target wasm \
--output dist/age_proof
When wasm-pack is on PATH, this command rebuilds both the prover bindings and the witness calculator for you. The resulting bundle lives at dist/age_proof/web/age_proof/ and includes:
build/age_proof.r1cskeys/age_proof_pk.binandkeys/age_proof_vk.binwitness/age_proof_witness_wasm.js(+ supporting.wasm)prover/lofit.js(+ supporting.wasm)
Serve everything in dist/age_proof/web/age_proof/ as static assets (for example, copy it into your app’s public/ directory or deploy it to a CDN).
3. Install the runtime toolkits
Add the officially published helpers to your web app:
npm install @lof-lang/toolkit-browser @lof-lang/toolkit-node
These packages know how to locate the compiled bundle and abstract the witness/prover/verifier orchestration.
4. Connect the Node.js backend
Use the Node toolkit to load the verifier and expose an HTTP endpoint:
import express from 'express';
import path from 'node:path';
import { loadVerifier, verifyProof } from '@lof-lang/toolkit-node';
const bundlePath = path.resolve('dist/age_proof/web/age_proof');
const verifier = await loadVerifier({ bundlePath });
const app = express();
app.use(express.json());
app.post('/api/age/verify', async (req, res) => {
const { proofBytes, publicInputs } = req.body;
const verified = await verifyProof(
verifier,
Uint8Array.from(proofBytes),
publicInputs
);
res.json({ verified });
});
app.listen(3000);
loadVerifier auto-discovers the verification key and the prover/lofit.js module inside the bundle. verifyProof accepts either ordered arrays or keyed objects for publicInputs, so you can pass through the object returned by the browser toolkit.
5. Wire up the browser client
The browser toolkit handles witness calculation, proof generation, and packaging of the inputs for the verifier:
import { createLofCircuit } from '@lof-lang/toolkit-browser';
const ageProofCircuit = await createLofCircuit({
bundleRoot: '/static/age_proof',
publicSignals: ['current_year', 'minimum_age', 'is_of_age'],
witnessSignals: ['birth_year']
});
export async function proveAge(inputs: {
current_year: string;
minimum_age: string;
birth_year: string;
}) {
const { proofBytes, publicInputs } = await ageProofCircuit.generateProof({
...inputs,
is_of_age: '1'
});
const response = await fetch('/api/age/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
proofBytes: Array.from(proofBytes),
publicInputs
})
});
return response.json();
}
createLofCircuit can discover every artifact (r1cs, proving key, witness module, prover module) from bundleRoot, so you only need to keep that path in sync with wherever you host the compiled bundle. generateProof returns the proof bytes as a Uint8Array plus a ready-to-send publicInputs object for the backend.
With these pieces in place, you can iterate on your .lof circuit, rerun lof compile --target wasm, redeploy the static bundle, and both the browser and backend integrations will continue to work without code changes.