Skip to main content

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:

  1. Issuer uploads. Sign an issuer in with their wallet, accept encrypted document containers, pin them, and find or remove an issuer's own copies.
  2. Verification for machines. Answer signed, read-only questions about credentials (the v1 API), off until a signer key is set.
  3. Status lists. Serve signed Bitstring Status List credentials derived from the chain, off until a publisher key is set.
  4. Operations. A monitor and a monthly report for a scheduler, off until a scheduler secret is set.

Route table​

Method and pathAuthPurposePage
GET /api/auth/noncenone (per-address limit)A one-time sign-in nonceSign-in and sessions
POST /api/auth/verifySIWE message and signatureCheck the sign-in, check the issuer role, set the session cookieSign-in and sessions
POST /api/uploadIssuer session cookiePin an encrypted document container (v0.3) or a PDF, PNG or JPEG (v0.2)Upload and documents
GET /api/documentsIssuer session cookieFind the signed-in issuer's pins of one documentUpload and documents
DELETE /api/documentsIssuer session cookieRemove the issuer's older copies of a document, keeping oneUpload and documents
GET /api/v1/credentials/{id}none, or an API keySigned chain facts about one credentialVerification API
POST /api/v1/verifynone, or an API keySigned verdict on a VC or Open Badge fileVerification API
GET /api/v1/statusnone, or an API keySigned API version, chain, sets and signerVerification API
GET /api/v1/openapi.jsonnoneThe OpenAPI 3.1 documentVerification API
OPTIONS /api/v1/*noneCORS preflightVerification API
GET /api/status-lists/{chainId}/{registry}/{issuer}/{purpose}/{list}none (per-address limit)One signed status list credentialStatus lists
GET /api/ops/checkScheduler bearer secretRun the monitor checks and alert on changesMonitoring endpoints
GET /api/ops/reportScheduler bearer secretThe monthly service report, optionally posted onceMonitoring 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.

FeatureReaderRequired settingsAnswer while off
Sign-in, upload, documentsreadAuthConfig, 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 store500 CONFIG_MISSING "Upload service not configured"
Document lookup and removalas aboveadditionally PINATA_JWT (the key pair is not enough)500 CONFIG_MISSING
Verification APIreadApiConfig (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=true503 CONFIG_MISSING "The verification API is not available on this deployment."
Status listsreadStatusListConfig (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 manifest503 CONFIG_MISSING "Status lists are not available on this deployment."
Monitor and reportreadOpsConfig (src/lib/server/ops/config.ts)CRON_SECRET (32 characters or more), NEXT_PUBLIC_NETWORK_ID, an RPC endpoint, and in production a Redis store401 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
Status as of 2026-10-24

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​

CodeStatusRaised by
BAD_REQUEST400 (405 for a wrong method on ops routes)Malformed body, query, id, nonce, file or month
UNAUTHORIZED401No or invalid session, failed sign-in, invalid API key, wrong scheduler secret
FORBIDDEN403Wallet not an issuer, session for another wallet, container for another wallet
PAYLOAD_TOO_LARGE413Upload over 4 MB, API body over 65,536 bytes
UNSUPPORTED_MEDIA_TYPE415v0.2 upload not PDF, PNG or JPEG; API file not sent as application/json
REGISTRY_NOT_LISTED422API file names a registry the API does not answer for
DOCUMENT_HASH_MISMATCH422The uploaded document does not match the stated hash
PLAINTEXT_NOT_ACCEPTED400A v0.3 upload that looks like a plain PDF, PNG or JPEG
INVALID_CONTAINER400A v0.3 upload that is not a valid encrypted container
KEEP_NOT_FOUND409Document removal named a copy the issuer does not have
NOT_FOUND404Unknown status list
RATE_LIMITED429Any budget used up; carries Retry-After
CONFIG_MISSING500 or 503A feature is off or its store is missing
UPSTREAM_ERROR502 or 503Pinata, the chain or the alert channel failed; 503 answers carry Retry-After: 30
INTERNAL_ERROR500The 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 (safeErrorSummary in src/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​

PageWhat it covers
Sign-in and sessionsSign-In with Ethereum, nonces, the session cookie, role re-checks
Upload and documentsContainer and plaintext uploads, Pinata, the second pin, document lookup and removal
Verification APIHow the v1 API is built: runtime, caching, sets, signing, limits
Status listsHow signed status lists are derived from the chain and served
Monitoring endpointsThe monitor checks, alerts, history and the monthly report
Security controlsRate limits, the store, headers, configuration validation and logging rules

Source files​

  • src/app/api/**/route.ts
  • src/lib/server/http.ts, config.ts, rate-limit.ts, limits.ts
  • src/lib/server/api/config.ts, src/lib/server/status-lists/config.ts, src/lib/server/ops/config.ts
  • example.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.ts
  • src/lib/server/api/config.test.ts, src/lib/server/status-lists/config.test.ts, src/lib/server/ops/config.test.ts