JavaScript/React Demo App
This example shows a basic integration of @attestid/sdk into a React application built with Vite.
Live demo This exact app runs at demo.attestid.brimsage.co.ls. Open it on a phone to try the full flow before reading the code.
It includes:
- SDK initialisation
- A backend SDK-token endpoint that presents an mTLS client certificate
- The drop-in
AttestIDVerifiercomponent - A local development setup you can test from a phone
Use this project as a reference alongside Using the JavaScript / React SDK.
Application code
// src/App.tsx
import { AttestID } from '@attestid/sdk';
import { AttestIDVerifier } from '@attestid/sdk/react';
import '@attestid/sdk/styles.css';
// The demo has no login, so it uses a random id per page load.
// A real integration uses its own authenticated user's id.
const demoUserId = crypto.randomUUID();
const client = new AttestID({
// In local dev VITE_API_URL is unset, so the SDK calls this page's own
// origin and the Vite dev server proxies /v1 to the API.
apiUrl: import.meta.env.VITE_API_URL || window.location.origin,
getSdkToken: async (userId) => {
const res = await fetch('/api/attestid-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
const body = await res.json().catch(() => ({}));
// Surface the backend's real error instead of a generic "no sdk_token".
if (!res.ok) throw new Error(body?.message || `/api/attestid-session failed (HTTP ${res.status})`);
return body;
},
});
export default function App() {
return (
<div className="app">
<AttestIDVerifier
client={client}
defaultUserId={demoUserId}
// Testing aids: only in `npm run dev`, never in a production build.
allowPhotoUpload={import.meta.env.DEV}
allowDesktopCapture={import.meta.env.DEV}
onVerified={result => console.log('[AttestID] verified', result)}
onError={err => console.error('[AttestID] error', err)}
/>
</div>
);
}
The browser only ever sees a short-lived sdk_token. The API key and client certificate stay on the server.
Development proxy
For local development, a Vite plugin stands in for your backend: it exchanges the API key for an SDK token using the SDK's createSdkSession(), presenting the client certificate over mutual TLS.
This is for local development only. In production, put the same exchange in your real backend (the demo's production build uses a small Node service, deploy/session-server.mjs, for this).
// vite.config.ts (abridged)
import { defineConfig, loadEnv, type Connect, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import basicSsl from '@vitejs/plugin-basic-ssl';
import fs from 'fs';
import https from 'https';
import { createSdkSession, AttestIDError } from '@attestid/sdk';
// Node's fetch can't send a client certificate without undici, so this one
// request uses node:https. Files are read per request, so a renewed
// certificate takes effect without restarting Vite.
function mtlsFetch(url: string, init?: RequestInit): Promise<Response> {
const tls = {
cert: fs.readFileSync(process.env.ATTESTID_CLIENT_CERT_FILE!),
key: fs.readFileSync(process.env.ATTESTID_CLIENT_KEY_FILE!),
};
return new Promise((resolve, reject) => {
const req = https.request(
url,
{ method: init?.method ?? 'GET', headers: init?.headers as Record<string, string>, ...tls },
upstream => {
const chunks: Buffer[] = [];
upstream.on('data', c => chunks.push(c));
upstream.on('end', () =>
resolve(new Response(Buffer.concat(chunks), {
status: upstream.statusCode ?? 502,
headers: upstream.headers as Record<string, string>,
})),
);
upstream.on('error', reject);
},
);
req.on('error', reject);
if (init?.body) req.write(init.body as string);
req.end();
});
}
function attestidSessionProxy(): Plugin {
const handler: Connect.NextHandleFunction = async (req, res, next) => {
if (req.url !== '/api/attestid-session' || req.method !== 'POST') return next();
let raw = '';
for await (const chunk of req) raw += chunk;
// SECURITY: the demo has no login, so it trusts the userId the client
// sends. A real backend must take it from the authenticated session.
const { userId } = JSON.parse(raw || '{}');
try {
const session = await createSdkSession(
process.env.ATTESTID_API_URL!,
process.env.ATTESTID_API_KEY ?? '',
process.env.ATTESTID_ACCESS_KEY_ID ?? '',
userId,
{ fetch: mtlsFetch },
);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(session));
} catch (err) {
res.statusCode = err instanceof AttestIDError ? (err.status ?? 502) : 502;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: err instanceof Error ? err.message : 'Unknown error' }));
}
};
return {
name: 'attestid-session-proxy',
configureServer(server) { server.middlewares.use(handler); },
configurePreviewServer(server) { server.middlewares.use(handler); },
};
}
export default defineConfig(({ mode }) => {
// Load .env into process.env so the proxy above (which runs in Node) can read it.
Object.assign(process.env, loadEnv(mode, process.cwd(), ''));
const apiTarget = process.env.ATTESTID_API_URL ?? 'https://api.attestid.brimsage.co.ls';
// Proxy API calls through the dev server, so a phone on your Wi-Fi can reach them.
const apiProxy = Object.fromEntries(
['/v1', '/.well-known'].map(prefix => [prefix, { target: apiTarget, changeOrigin: true }]),
);
return {
// basicSsl: the camera only works on https (or localhost exactly), so a
// self-signed certificate lets you test from a phone on your network.
plugins: [react(), attestidSessionProxy(), basicSsl()],
server: { host: true, proxy: apiProxy },
preview: { host: true, proxy: apiProxy },
};
});
Environment variables
Frontend
# Only for a production build. Leave unset locally: the browser calls the dev
# server's own origin, which proxies to ATTESTID_API_URL.
# VITE_API_URL=https://api.attestid.brimsage.co.ls
Development proxy (server-only, never VITE_-prefixed)
ATTESTID_API_URL=https://api.attestid.brimsage.co.ls
ATTESTID_API_KEY=your_api_key_here
ATTESTID_ACCESS_KEY_ID=your_access_key_id_here
# Required: AttestID rejects every API-key request without a client certificate.
ATTESTID_CLIENT_CERT_FILE=./client.crt
ATTESTID_CLIENT_KEY_FILE=./client.key
Use test keys while you develop. Never commit .env, client.key or client.crt.
Getting the client certificate
Generate the key and CSR in the project folder, then issue the certificate in the dashboard:
openssl ecparam -name prime256v1 -genkey -noout -out client.key
openssl req -new -key client.key -subj "/CN=attestid-demo" -out client.csr
chmod 600 client.key
Paste the contents of client.csr into Client Certificates → Issue Certificate, and save the result as client.crt. Add *.key, *.csr and *.crt to .gitignore.
See Creating a Company & Getting Your Keys for your API credentials and Client Certificates (mTLS) for more on certificates.
Running the application
npm install
npm run dev # start the dev server (https, reachable on your network)
npm run build # production build
npm run preview # preview the production build
To test on a phone, open the Network URL Vite prints (https://<your-lan-ip>:5173) on a phone connected to the same Wi-Fi. Accept the one-time certificate warning: it's expected for the self-signed development certificate.
Verification checklist
Before deploying your application, check that:
AttestIDVerifierrenders correctly.- SDK tokens are retrieved from your backend, which presents its client certificate.
- Document capture and photo quality checks complete.
- The face check completes.
- Retry face check appears after a liveness-only failure.
- Your backend verifies
result.attestationbefore acting on the result. - Your webhook receiver verifies signatures and accepts either secret during a rotation.
allowPhotoUploadis off in production.- Production uses your backend, not the development proxy.