Server routes
Certichain's server is a set of Next.js route handlers in src/app/api/, deployed with the web application (on Vercel for the Sepolia demo). It holds as little as possible. There is no database: the chain is the source of truth, documents live on IPFS through a pinning service, and the only server-side state is short-lived counters, sign-in nonces and monitor history in a key-value store.
The server has four jobs:
- Issuer uploads. Sign an issuer in with their wallet, accept encrypted document containers, pin them, and find or remove an issuer's own copies.
- Verification for machines. Answer signed, read-only questions about credentials (the v1 API), off until a signer key is set.
- Status lists. Serve signed Bitstring Status List credentials derived from the chain, off until a publisher key is set.
- Operations. A monitor and a monthly report for a scheduler, off until a scheduler secret is set.
Route table
| Method and path | Auth | Purpose | Page |
|---|---|---|---|
GET /api/auth/nonce | none (per-address limit) | A one-time sign-in nonce | Sign-in and sessions |
POST /api/auth/verify | SIWE message and signature | Check the sign-in, check the issuer role, set the session cookie | Sign-in and sessions |
POST /api/upload | Issuer session cookie | Pin an encrypted document container (v0.3) or a PDF, PNG or JPEG (v0.2) | Upload and documents |
GET /api/documents | Issuer session cookie | Find the signed-in issuer's pins of one document | Upload and documents |
DELETE /api/documents | Issuer session cookie | Remove the issuer's older copies of a document, keeping one | Upload and documents |
GET /api/v1/credentials/{id} | none, or an API key | Signed chain facts about one credential | Verification API |
POST /api/v1/verify | none, or an API key | Signed verdict on a VC or Open Badge file | Verification API |
GET /api/v1/status | none, or an API key | Signed API version, chain, sets and signer | Verification API |
GET /api/v1/openapi.json | none | The OpenAPI 3.1 document | Verification API |
OPTIONS /api/v1/* | none | CORS preflight | Verification API |
GET /api/status-lists/{chainId}/{registry}/{issuer}/{purpose}/{list} | none (per-address limit) | One signed status list credential | Status lists |
GET /api/ops/check | Scheduler bearer secret | Run the monitor checks and alert on changes | Monitoring endpoints |
GET /api/ops/report | Scheduler bearer secret | The monthly service report, optionally posted once | Monitoring endpoints |
All handlers are dynamic (export const dynamic = "force-dynamic"), so Next.js never pre-renders or caches them at build time. The v1 and ops routes also declare runtime = "nodejs", and the ops routes maxDuration = 60 (seconds).
Off until configured
Each feature reads its own settings when a request arrives and is off when a required one is missing. "Off" is a clear answer, never a crash. Names are listed in example.env.
| Feature | Reader | Required settings | Answer while off |
|---|---|---|---|
| Sign-in, upload, documents | readAuthConfig, readPinataCredentials, getStore (src/lib/server/config.ts, rate-limit.ts) | SESSION_SECRET (32 characters or more), NEXT_PUBLIC_NETWORK_ID, an RPC endpoint (RPC_URLS, RPC_URL or the public ones), NEXT_PUBLIC_CONTRACT_ADDRESS on v0.2, SIWE_ALLOWED_DOMAINS in production, Pinata credentials (PINATA_JWT, or PINATA_API_KEY and PINATA_API_SECRET_KEY), and in production a Redis store | 500 CONFIG_MISSING "Upload service not configured" |
| Document lookup and removal | as above | additionally PINATA_JWT (the key pair is not enough) | 500 CONFIG_MISSING |
| Verification API | readApiConfig (src/lib/server/api/config.ts) | API_SIGNER_KEY, NEXT_PUBLIC_NETWORK_ID, an RPC endpoint, a v0.3 manifest for the chain, and in production a Redis store unless API_ALLOW_LOCAL_LIMITS=true | 503 CONFIG_MISSING "The verification API is not available on this deployment." |
| Status lists | readStatusListConfig (src/lib/server/status-lists/config.ts) | STATUS_LIST_SIGNER_KEY, NEXT_PUBLIC_STATUS_LIST_BASE_URL, NEXT_PUBLIC_NETWORK_ID, an RPC endpoint, a v0.3 manifest | 503 CONFIG_MISSING "Status lists are not available on this deployment." |
| Monitor and report | readOpsConfig (src/lib/server/ops/config.ts) | CRON_SECRET (32 characters or more), NEXT_PUBLIC_NETWORK_ID, an RPC endpoint, and in production a Redis store | 401 UNAUTHORIZED "Not allowed." without a valid secret; 500 CONFIG_MISSING "The monitor is not configured." without a store; with the secret but no chain settings, a run whose only result is a failing config check |
Sign-in and upload are configured on the Sepolia deployment, and a live smoke test (issue with an encrypted upload, open the document, revoke) passed on 2026-10-17. A full live check of sharing, key replacement and removal of old copies is still open. The verification API, status lists and the monitor are built but stay off until the deployment owner sets their keys and secrets (API_SIGNER_KEY, STATUS_LIST_SIGNER_KEY, CRON_SECRET and the related names). No second pinning service is named yet, so the second pin is off too.
A malformed optional setting is treated differently from a missing one: it is a configuration error that must be seen. For example a malformed API_SIGNER_KEY makes the API answer 503 and log the problem, and a malformed OPS_WEBHOOK_URL becomes a failing monitor check. Messages never repeat the value, which can be a secret.
Response conventions
Plain envelope
Every route answers JSON in one shape (src/lib/server/http.ts), except two kinds of success body: a status list route returns the list credential itself, and the OpenAPI route returns the document.
{ "data": { "nonce": "3f1c0e8a9b7d4c2e1f0a9b8c7d6e5f40", "expiresAt": "2026-10-24T09:05:00.000Z" } }
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests" } }
The browser reads both through readEnvelope (src/services/upload.ts), which throws the server's message when data is missing or the status is not 2xx.
The verification API wraps its success answers in a signed envelope instead: { data, meta, signature }. Its errors use the plain error shape and are not signed. See API envelope.
Error codes
| Code | Status | Raised by |
|---|---|---|
BAD_REQUEST | 400 (405 for a wrong method on ops routes) | Malformed body, query, id, nonce, file or month |
UNAUTHORIZED | 401 | No or invalid session, failed sign-in, invalid API key, wrong scheduler secret |
FORBIDDEN | 403 | Wallet not an issuer, session for another wallet, container for another wallet |
PAYLOAD_TOO_LARGE | 413 | Upload over 4 MB, API body over 65,536 bytes |
UNSUPPORTED_MEDIA_TYPE | 415 | v0.2 upload not PDF, PNG or JPEG; API file not sent as application/json |
REGISTRY_NOT_LISTED | 422 | API file names a registry the API does not answer for |
DOCUMENT_HASH_MISMATCH | 422 | The uploaded document does not match the stated hash |
PLAINTEXT_NOT_ACCEPTED | 400 | A v0.3 upload that looks like a plain PDF, PNG or JPEG |
INVALID_CONTAINER | 400 | A v0.3 upload that is not a valid encrypted container |
KEEP_NOT_FOUND | 409 | Document removal named a copy the issuer does not have |
NOT_FOUND | 404 | Unknown status list |
RATE_LIMITED | 429 | Any budget used up; carries Retry-After |
CONFIG_MISSING | 500 or 503 | A feature is off or its store is missing |
UPSTREAM_ERROR | 502 or 503 | Pinata, the chain or the alert channel failed; 503 answers carry Retry-After: 30 |
INTERNAL_ERROR | 500 | The monitor or report could not run |
Logging
Server logs name what failed without leaking secrets or personal data:
- Pinata failures log the HTTP status and Pinata's request id only.
- Role-check failures log a summary from which URLs (which can carry RPC keys) are removed (
safeErrorSummaryinsrc/lib/server/role-check.ts). - The v1 API, status lists and the monitor log error names (and codes), never messages that could quote a key, an endpoint address or a file.
- Uploaded files and API request bodies are never logged.
See Security controls for the full list of server protections.
Pages in this section
| Page | What it covers |
|---|---|
| Sign-in and sessions | Sign-In with Ethereum, nonces, the session cookie, role re-checks |
| Upload and documents | Container and plaintext uploads, Pinata, the second pin, document lookup and removal |
| Verification API | How the v1 API is built: runtime, caching, sets, signing, limits |
| Status lists | How signed status lists are derived from the chain and served |
| Monitoring endpoints | The monitor checks, alerts, history and the monthly report |
| Security controls | Rate limits, the store, headers, configuration validation and logging rules |
Source files
src/app/api/**/route.tssrc/lib/server/http.ts,config.ts,rate-limit.ts,limits.tssrc/lib/server/api/config.ts,src/lib/server/status-lists/config.ts,src/lib/server/ops/config.tsexample.env(names only)
Tests
src/lib/server/http.test.ts,src/lib/server/config.test.ts,src/lib/server/limits.test.ts,src/lib/server/rate-limit.test.tssrc/lib/server/api/config.test.ts,src/lib/server/status-lists/config.test.ts,src/lib/server/ops/config.test.ts