Skip to main content

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​

TechnologyNext.js 16 App Router, React 19, ethers v6, Tailwind CSS v4
Codesrc/app/ (pages and routes), src/components/, src/services/, src/lib/
RunsIn 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 forEvery 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.
StoresNothing 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 byNEXT_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:

RequestUsed for
eth_requestAccounts, chain id, account change eventsConnecting, detecting the role, reacting to account and network switches
personal_signThe Sign-In with Ethereum message before the first upload
eth_signTypedData_v4Credential signatures, invitations, encryption key derivation, student salts, disclosure attestations and requests, badge keys
eth_sendTransactionIssuing, 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​

TechnologyNext.js route handlers on the Node runtime, deployed on Vercel
Codesrc/app/api/, helpers in src/lib/server/
Responsible forSign-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
StoresNothing of its own beyond the shared store below. Sessions are signed cookies.
Configured byServer-only variables (SESSION_SECRET, PINATA_JWT, RPC_URLS, API_SIGNER_KEY, STATUS_LIST_SIGNER_KEY, CRON_SECRET, OPS_* and others)
RoutePurposePage
GET /api/auth/nonce, POST /api/auth/verifySIWE sign-in for issuersSign-in and sessions
POST /api/uploadAccept an encrypted container from a signed-in issuer and pin itUpload and documents
GET, DELETE /api/documentsFind or remove the issuer's stored copies of a documentUpload and documents
/api/v1/*Signed verification API and its OpenAPI documentVerification API
/api/status-lists/*Signed Bitstring Status List credentialsStatus lists
/api/ops/check, /api/ops/reportMonitor and monthly reportMonitoring 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​

ContractRolePage
IssuerRegistryRegistered issuers, names, guardian, pause, key lineageIssuer registry
CredentialRegistryAnchors and signatures; the entry point that exposes the other registriesCredential registry
StatusRegistryLifecycle and historyStatus registry
HolderRegistryWallet bindings by issuer invitationHolder registry
RecordTypeRegistryIssuer-defined record typesRecord type registry
CertichainTimelockDelay for admin actions, run by the SafeTimelock
VersionAnnouncerAdvisory notices about contract setsVersion announcer
CertichainAccessGate with SemaphorePrivate access permissions and receiptsAccess gate
AcademicRecords (v0.2)The legacy single-contract systemLegacy 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​

ToolWhereWhat it does
Credential verifier scriptblockchain/scripts/verify-credential.tsChecks a package, VC or badge file offline, then against a chain
Import checkernpm run import:checkValidates a bulk import file without a wallet
Access CLInpm run access in blockchain/Node prover and admission tool for the access gate
SDKpackages/sdkA client that accepts verification API answers only from a pinned signer
Interop suiteinterop/An independent verifier library checks exported credentials

See Integrations.

Source files​

  • src/lib/chain.ts, src/lib/contract-version.ts, src/contracts/v03/manifest.ts
  • src/services/wallet/wallet-service.ts, src/services/documents/wallet-key-session.ts
  • src/workers/access-prover.worker.ts, src/lib/zk/circuit-files.ts, scripts/zk-public.mjs
  • public/sw.js
  • src/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/