Local development
Everything Certichain does can run on one laptop: a local Hardhat chain (chain id 31337) with the v0.3 contract sets and the access gate, and the Next.js app in development mode reading it. Only document storage needs an outside service (Pinata); the E2E harness replaces even that with a mock.
Prerequisites
| Tool | Version | Why |
|---|---|---|
| Node.js | 20.9 or later (Next.js 16 requires >=20.9.0); CI uses Node 22 | Root app, contracts, SDK, interop tests. The SDK package declares node >=20. There is no .nvmrc in the repository. |
| npm | The one that ships with Node | Each package has its own package-lock.json; use npm ci |
| Git | Any recent | |
| Chromium for Playwright | npx playwright install chromium | Only for npm run test:e2e |
| A browser wallet (MetaMask) | Any recent | To sign in and issue by hand; not needed for the verify page |
circom 2.2.2 | Optional | Only for zk:reproduce, which rebuilds the proving keys from source |
Install
npm ci # root: Next.js app, unit tests, shared code
(cd blockchain && npm ci) # contracts, Hardhat, deploy scripts
(cd packages/sdk && npm ci) # only for SDK work
(cd interop && npm ci) # only for the interoperability tests
The contract tests import shared TypeScript from src/lib, so the root install is needed for blockchain/ too.
A local chain with contracts
- Start the node in its own terminal:
cd blockchain && npx hardhat node. It prints twenty funded test accounts. They are public, well-known development accounts: use them only on this chain. - Deploy the v0.3 set:
cd blockchain && npm run deploy:v03:local. The first account becomes admin of the registries. Optional:GUARDIAN_ADDRESS=0x...names a guardian,ADMIN_DELAY_SECchanges the admin transfer delay (default 3 days). The script writessrc/contracts/v03/deployment.31337.json(gitignored) and exports the ABIs intosrc/contracts/v03/. - Register issuers:
ISSUERS="0xAddr=Name;0xAddr=Name" npm run issuers:v03:local. Addresses already registered are skipped. - Optional, for the ZK access features:
npm run deploy:access:localdeploys the pinned Poseidon library, the Semaphore verifier, Semaphore and the access gate, checks their code hashes and writessrc/contracts/zk/deployment.31337.json. - Optional smoke:
npm run smoke:v03:localregisters the first account as issuer, issues a valid and a revoked credential and reads both back.
A second contract set is added the same way as on a public network: ADD_SET=1 SET_LABEL=next npm run deploy:v03:local (see Contract set upgrade).
The legacy v0.2 contracts still deploy with npm run deploy:local, which writes blockchain/deployments/localhost.json and copies the v0.2 ABIs. See Legacy v0.2 contracts.
Run the app
Create .env.local in the repository root (gitignored; never commit it). The names and their meaning are in Configuration. For a v0.3 local setup the essentials are:
| Variable | Local value |
|---|---|
NEXT_PUBLIC_NETWORK_ID | 31337 |
NEXT_PUBLIC_CONTRACT_VERSION | v03 |
NEXT_PUBLIC_RPC_URL and RPC_URL | http://127.0.0.1:8545 |
SESSION_SECRET | Any random string of 32 or more characters, for issuer sign-in |
PINATA_JWT, NEXT_PUBLIC_PINATA_GATEWAY_URL | Your own test Pinata account, only if you upload documents |
Then npm run dev. Before it starts, predev runs npm run zk:public, which places the circuit files for tree depths 1 to 6 (about 21 MB) in the gitignored public/zk/: it keeps files that already match src/lib/zk/circuit-hashes.json, copies from blockchain/.zk-artifacts when present, and otherwise downloads from the pinned address. The first run can take a while.
Outside production the server keeps sessions, rate limits and monitor state in memory, so Redis is not needed. SIWE_ALLOWED_DOMAINS is required only in production.
In the wallet, add the network http://127.0.0.1:8545 with chain id 31337 and import one of the node's test accounts. Restarting the node wipes the chain, so redeploy and re-register; the wallet may also need its activity reset because the nonces start again.
The status lists, the verification API and the monitor stay off until their keys are set. For local trials use throwaway keys created for this purpose only, for example a fresh STATUS_LIST_SIGNER_KEY with NEXT_PUBLIC_STATUS_LIST_BASE_URL=http://localhost:3000 and the key's address as NEXT_PUBLIC_STATUS_LIST_PUBLISHER. The E2E harness does exactly this for its status list journey.
Unit tests and checks
| Command | Scope |
|---|---|
npm run test:run | Vitest over the root app, packages/sdk and the E2E support code (blockchain/, interop/ and the journeys excluded) |
npm run test:coverage | The same with coverage thresholds |
npm run lint, npm run typecheck | ESLint and tsc --noEmit |
cd blockchain && npm test | Hardhat contract tests |
cd blockchain && npm run coverage | Contract coverage; fails under 95% lines or 90% branches |
The full local bundle before a commit is in the handbook's verification checklist. Test details: Unit tests, Contract tests.
The E2E harness
npm run test:e2e runs Playwright against a world it builds itself (e2e/global-setup.ts):
- It refuses to start if something already answers on port 8545 or 3100, or if
src/contracts/v03/deployment.31337.jsonorsrc/contracts/zk/deployment.31337.jsonexists. Move your own local manifests away first. - The browser's upload and gateway requests are intercepted by an in-memory mock storage (
e2e/support/mock-upload.ts), so no Pinata account is needed. - The app server gets a throwaway status list signer and API signer and no server cache.
- One worker, 90 second timeout per test, Chromium only. Failures keep traces in the HTML report.
See End-to-end tests for the journeys.
Source files
package.json,blockchain/package.json,blockchain/hardhat.config.ts(loads../.env)blockchain/scripts/deploy-v03.ts,add-issuers-v03.ts,deploy-access-gate.ts,smoke-v03.ts,deploy.tsscripts/zk-public.mjs,src/lib/zk/circuit-hashes.jsonplaywright.config.ts,e2e/global-setup.ts,e2e/support/paths.ts,e2e/support/app-server.ts,e2e/support/mock-storage.ts- Handbook:
docs/process/commands.md,docs/process/verification-checklist.md
Tests
e2e/support/guards.test.ts,e2e/support/local-chain.test.ts(the harness refusals)src/lib/rpc/*.test.ts,src/lib/server/config.test.ts(settings readers)