Containers
A "container" here means a separately running part of the system: something that executes code or holds data on its own. This page lists them, then shows how they connect.
The dotted line is optional: alerts need a configured webhook. Inside the chain box, the timelock is deployed but does not yet administer the active set's registries (see Contract versions and sets); a second pinning provider can sit beside the first once configured. The sections below describe each box in detail.
Browser app
| Technology | Next.js 16 App Router, React 19, ethers v6, Tailwind CSS v4 |
| Code | src/app/ (pages and routes), src/components/, src/services/, src/lib/ |
| Runs | In the user's browser. Interactive pages are client components. The root layout, the home page, About and the legal pages render on the server and hydrate only their interactive parts (for example the home lookup form). |
| Responsible for | Every user interaction; hashing, encryption and decryption; commitments and typed data; reading the chain; building packages, disclosure files, VC and Open Badge exports, compact codes and import results. |
| Stores | Nothing durable on a server. In the browser: scan caches (src/services/indexing/storage-scan-cache.ts), the import progress of a run, and on /wallet the phone wallet entries in IndexedDB (src/lib/present/idb-backend.ts). Derived encryption keys live in memory for the page session only (src/services/documents/wallet-key-session.ts). |
| Configured by | NEXT_PUBLIC_* variables, read through src/lib/chain.ts, and the bundled manifests in src/contracts/. |
The app chooses its contract system from NEXT_PUBLIC_CONTRACT_VERSION (v03 or, by default, the legacy v02, src/lib/contract-version.ts) and its chain from NEXT_PUBLIC_NETWORK_ID. For v0.3 it loads the manifest for that chain id from src/contracts/v03/deployment.<chainId>.json, so no contract address is configured by hand.
Prover Web Worker
src/workers/access-prover.worker.ts builds Semaphore proofs for the access gate off the main thread, so the page stays responsive during the one to two seconds a proof takes. It loads circuit files (semaphore-<depth>.wasm and .zkey, depth 1 to 6) from the site's own /zk/ path and checks each against a pinned SHA-256 before use (src/lib/zk/circuit-files.ts, hashes in src/lib/zk/circuit-hashes.json). The files are placed in public/zk/ by npm run zk:public, which runs before dev and build. See Private access proofs.
Service worker
public/sw.js keeps two pages, /wallet and /present, usable without a network. It caches only those pages, the app icons, the web manifest and content-hashed files under /_next/static/. Chain reads, API calls and every other page always go to the network, so a credential's status is never served from this cache. See Phone wallet.
Wallet
The user's wallet (MetaMask in practice, any EIP-1193 provider in principle) holds the private key. The app asks it for three kinds of things, always through src/services/wallet/wallet-service.ts:
| Request | Used for |
|---|---|
eth_requestAccounts, chain id, account change events | Connecting, detecting the role, reacting to account and network switches |
personal_sign | The Sign-In with Ethereum message before the first upload |
eth_signTypedData_v4 | Credential signatures, invitations, encryption key derivation, student salts, disclosure attestations and requests, badge keys |
eth_sendTransaction | Issuing, status changes, accepting invitations, registering record types, access admissions and proofs, the guardian pause |
The app never sees or stores a private key. A contract wallet (ERC-1271) can be an issuer on chain, but the website cannot issue through one yet, because key derivation needs a wallet that signs the same message the same way every time.
Server: Next.js API routes
| Technology | Next.js route handlers on the Node runtime, deployed on Vercel |
| Code | src/app/api/, helpers in src/lib/server/ |
| Responsible for | Sign-in (SIWE nonce and verify, session cookie), accepting and pinning encrypted containers, finding and removing an issuer's stored copies, the signed verification API, signed status lists, the monitor and the monthly report |
| Stores | Nothing of its own beyond the shared store below. Sessions are signed cookies. |
| Configured by | Server-only variables (SESSION_SECRET, PINATA_JWT, RPC_URLS, API_SIGNER_KEY, STATUS_LIST_SIGNER_KEY, CRON_SECRET, OPS_* and others) |
| Route | Purpose | Page |
|---|---|---|
GET /api/auth/nonce, POST /api/auth/verify | SIWE sign-in for issuers | Sign-in and sessions |
POST /api/upload | Accept an encrypted container from a signed-in issuer and pin it | Upload and documents |
GET, DELETE /api/documents | Find or remove the issuer's stored copies of a document | Upload and documents |
/api/v1/* | Signed verification API and its OpenAPI document | Verification API |
/api/status-lists/* | Signed Bitstring Status List credentials | Status lists |
/api/ops/check, /api/ops/report | Monitor and monthly report | Monitoring endpoints |
Features that need a key stay off until the key is set: the verification API (API_SIGNER_KEY), status lists (STATUS_LIST_SIGNER_KEY and the public base URL and publisher address), and the monitor (CRON_SECRET, and OPS_WEBHOOK_URL for alerts).
Shared store: Redis
src/lib/server/rate-limit.ts gives every route one key-value store. With UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (or the Vercel KV names) it is Upstash Redis. Outside production it falls back to an in-memory store; in production without Redis, routes that need the store answer with a configuration error instead of running without limits. It holds single-use sign-in nonces, rate-limit counters, and the monitor's state, daily counts and alert history.
Pinning service and gateways
Encrypted containers are pinned on IPFS through Pinata by the upload route (src/lib/server/pinata.ts), and read back by the browser through the gateway in NEXT_PUBLIC_PINATA_GATEWAY_URL. A second pinning provider can be configured behind the PinProvider interface (src/lib/server/pinning/), with SECOND_PIN_PROVIDER and PIN_READ_GATEWAYS; no provider adapter is chosen yet. Because containers are encrypted and bound to their document hash, a pinning service can lose or withhold a file but cannot read or alter one undetected. See Storage and IPFS.
JSON-RPC endpoints
The browser reads the chain through NEXT_PUBLIC_RPC_URLS (or the single NEXT_PUBLIC_RPC_URL), and the server through RPC_URLS (or RPC_URL). Both use one provider factory with ordered failover (src/lib/rpc/provider.ts). Lists such as the issuer dashboard and the holder inbox are built from eth_getLogs scans in bounded block windows with retries (src/services/indexing/), so the endpoints must allow log queries. See Reading the chain.
Contracts
| Contract | Role | Page |
|---|---|---|
IssuerRegistry | Registered issuers, names, guardian, pause, key lineage | Issuer registry |
CredentialRegistry | Anchors and signatures; the entry point that exposes the other registries | Credential registry |
StatusRegistry | Lifecycle and history | Status registry |
HolderRegistry | Wallet bindings by issuer invitation | Holder registry |
RecordTypeRegistry | Issuer-defined record types | Record type registry |
CertichainTimelock | Delay for admin actions, run by the Safe | Timelock |
VersionAnnouncer | Advisory notices about contract sets | Version announcer |
CertichainAccessGate with Semaphore | Private access permissions and receipts | Access gate |
AcademicRecords (v0.2) | The legacy single-contract system | Legacy v0.2 |
CredentialRegistry holds immutable references to the issuer, status, holder and record type registries, so one address is enough to find a whole set. The access gate is bound at deployment to one credential registry and reads the issuer, status and holder registries through it.
The Safe
The admin of the v0.3 registries on Sepolia is a Safe (2 of 3 owners). Admin actions are built as Safe transaction batches (files under blockchain/timelock-files/ and the scripts in blockchain/scripts/), signed by the owners and executed from the Safe. The timelock is deployed so the Safe proposes, a delay passes, and anyone executes; see Governance for what is in place today.
Offline and outside tools
| Tool | Where | What it does |
|---|---|---|
| Credential verifier script | blockchain/scripts/verify-credential.ts | Checks a package, VC or badge file offline, then against a chain |
| Import checker | npm run import:check | Validates a bulk import file without a wallet |
| Access CLI | npm run access in blockchain/ | Node prover and admission tool for the access gate |
| SDK | packages/sdk | A client that accepts verification API answers only from a pinned signer |
| Interop suite | interop/ | An independent verifier library checks exported credentials |
See Integrations.
Source files
src/lib/chain.ts,src/lib/contract-version.ts,src/contracts/v03/manifest.tssrc/services/wallet/wallet-service.ts,src/services/documents/wallet-key-session.tssrc/workers/access-prover.worker.ts,src/lib/zk/circuit-files.ts,scripts/zk-public.mjspublic/sw.jssrc/lib/server/rate-limit.ts,src/lib/server/config.ts,src/lib/server/pinata.ts,src/lib/server/pinning/src/lib/rpc/provider.ts,src/services/indexing/blockchain/contracts/v03/,blockchain/contracts/zk/