Custom API instances
The official Web app accepts a custom origin only when the Pinchana project has issued an unexpired Ed25519 certificate for that exact origin. This prevents the settings field from becoming an arbitrary server-side request proxy.
Certificate envelope and claims
Section titled “Certificate envelope and claims”The API returns this public envelope from GET /web/identity:
{"payload":"BASE64URL_JSON_CLAIMS","signature":"BASE64URL_ED25519_SIGNATURE"}Decoded claims must contain:
| Claim | Requirement |
|---|---|
issuer |
exactly pinchana-project |
protocol |
exactly 1 |
origin |
exact normalized API origin |
turnstile_site_key |
non-empty public widget key for this Web hostname |
issued_at |
Unix seconds; no more than five minutes in the future |
expires_at |
Unix seconds; must be in the future |
Production origins must use HTTPS and contain no credentials, path, query, or fragment. Development permits HTTP. The Web deployment pins the project public key in server-only PINCHANA_INSTANCE_PUBLIC_KEY and verifies Ed25519 over the raw payload bytes.
- A user enters an origin in Settings.
- Browser code fetches
${origin}/web/identitywith CORS. - It posts the origin and envelope to same-origin
POST /api/instance. - The BFF verifies signature, claims, exact origin, and time bounds.
- It stores
payload.signaturein the HttpOnlypinchana_instancecookie until certificate expiry, clears the old session, and starts Turnstile with the certified site key.
Operator setup
Section titled “Operator setup”After approval, an operator receives the one-line envelope—not the signing key. For the standard Docker Compose deployment, copy the envelope into the API repository’s .env and quote the complete JSON value:
PINCHANA_INSTANCE_CERTIFICATE='{"payload":"BASE64URL_CLAIMS","signature":"BASE64URL_SIGNATURE"}'PINCHANA_INSTANCE_CERTIFICATE_FILE=The API deployment also needs the private Turnstile secret matching the public site key embedded in the certificate, plus the Web hostname and action:
TURNSTILE_SECRET_KEY=REPLACE_WITH_PRIVATE_TURNSTILE_SECRETTURNSTILE_EXPECTED_HOSTNAME=pinchana.example.comTURNSTILE_EXPECTED_ACTION=turnstile-spin-v1TURNSTILE_SESSION_SECRET=REPLACE_WITH_AT_LEAST_32_RANDOM_CHARACTERSApply and verify the configuration through the final public HTTPS origin:
docker compose up -d --force-recreate servercurl --fail --silent https://api.example.com/web/identityThe response must contain the installed payload and signature. A 503 means the certificate was not available inside the server container. The API’s /web/identity response is public, cached for five minutes, and allows all origins because the certificate is already public and cryptographically verified.
To use a file instead, mount it read-only into the server container and set PINCHANA_INSTANCE_CERTIFICATE_FILE to the container path. Leave the inline variable empty:
services: server: volumes: - ./secrets/instance-certificate.json:/run/secrets/pinchana-instance-certificate.json:ro environment: PINCHANA_INSTANCE_CERTIFICATE_FILE: /run/secrets/pinchana-instance-certificate.jsonThe inline value takes precedence when both sources are configured.
Maintainer issuance
Section titled “Maintainer issuance”The private Ed25519 key stays offline and outside every repository and deployment. A maintainer can generate a key pair and use the API repository’s signing script after reviewing an operator:
openssl genpkey -algorithm Ed25519 -out /secure/pinchana-instance-private.pemopenssl pkey -in /secure/pinchana-instance-private.pem -pubout -out pinchana-instance-public.pem
node scripts/sign-instance-certificate.mjs \ /secure/pinchana-instance-private.pem \ https://api.example.com \ REPLACE_WITH_TURNSTILE_SITE_KEY \ 90The command prints the one-line envelope to standard output. Its second argument is the exact final HTTPS API origin, with no path, query, credentials, or fragment. Its third argument is the public Turnstile site key for the Web hostname—not the private Siteverify secret. Validity may be 1 through 366 days.
Issue and install a replacement before expires_at, then recreate the API server container. Once an old certificate expires, the Web app rejects it and users must select the instance again after renewal.