Access

Introduction

Caller authorization always runs before a concurrency slot is taken. Configure caller auth on the pool's Access tab. Upstream secrets for managed pools live under Credentials.

Checks run in this order: IP allowlist → JWT / Cloudflare Access → pool key → HMAC (when required).

Pool keys

Create keys under Pools → Access → Create key. Send the secret with X-Nthpool-Key:

curl -sS "https://your-pool.nthpool.cloud/v1/records" \
  -H "X-Nthpool-Key: $POOL_KEY"

On managed pools you may instead send Authorization: Bearer $POOL_KEY or Authorization: Nthbouncer $POOL_KEY.

On pass-through pools, always use X-Nthpool-Key for the pool key so Authorization can carry the upstream credential.

Missing or invalid keys return 401.

Write blocking

Each key either blocks writes (default) or has writes allowed. With writes blocked, the key admits GET, HEAD, and OPTIONS only. Every other method returns 403 before the request reaches your origin, and before it takes a concurrency slot.

On Access, under the pool keys, you can add methods this origin uses for reads. SuiteQL and GraphQL are POST. Paths belong to that method only. Leave a method's paths blank to allow it everywhere, or list prefixes (/services/rest/query/) so other POSTs stay blocked.

Leave writes blocked for callers that only fetch: retrieval agents, dashboards, search indexers. It means a prompt injection or a buggy loop cannot delete a record no matter what it sends.

Keys minted before this option existed have writes allowed and are unchanged.

The lock API is not affected: acquiring, renewing, and releasing a concurrency slot are coordination calls, not writes to your origin, so a writes-blocked key can still do them.

On a database pool the HTTP method is not the write gate. Every query is a POST, including a read. The write boundary is the Postgres role the pool connects as. In v1 that is always the read role, so a writes-allowed key and a writes-blocked key connect the same way. A statement that tries to write is refused by the database (403, SQLSTATE 25006), not by the key's method list. MCP query tools (pool_query, pool_schema, pool_count_rows) are read-only; there is no write tool to hide.

Inbound HMAC (optional)

When Require signed requests is enabled on Access, each key gets a signing secret (nbsk_…, shown once). Callers must send:

Header Value
X-Nthpool-Timestamp Unix timestamp (seconds)
X-Nthpool-Nonce Unique nonce for the request
X-Nthpool-Signature Hex HMAC-SHA256 over the canonical string

Canonical string (fields joined with \n):

METHOD
PATH
QUERY
SHA256(BODY)
TIMESTAMP
NONCE

PATH is the request path (no host). QUERY is the query string without ? (empty string when there is no query). SHA256(BODY) is the hex SHA-256 of the raw body bytes (empty body → SHA-256 of empty input).

Limits

Limit Behavior
Clock skew ±300 seconds Outside the window → 401
Nonce uniqueness Replay within the window → 401
Body size 10 MB Larger bodies → 413 payload_too_large (signing must hash the whole body)

HMAC-signed requests are not eligible for the response cache.

Origin verification (outbound HMAC)

Enable Origin verification on Pools → Access. The pool then signs every request it forwards so your origin can prove traffic came through the pool and reject direct callers.

Outbound signing uses the same header names as inbound HMAC, but a different canonical string: it hashes only the first 1 MB of the body and binds Content-Length, so large uploads can stream. The secret is a pool-level osk_… value (shown once when you enable or rotate).

What the pool sends upstream

After stripping every inbound X-Nthpool-* header (callers cannot forge these), the pool adds:

Header Value
X-Nthpool-Timestamp Fresh epoch seconds
X-Nthpool-Nonce Fresh UUID (new on every attempt, including retries)
X-Nthpool-Signature Hex HMAC-SHA256 of the outbound canonical string with osk_…

Canonical string (fields joined with \n):

METHOD
PATH
QUERY
SHA256(BODY_PREFIX)
CONTENT_LENGTH
TIMESTAMP
NONCE

PATH / QUERY are what the origin receives (forwarded verbatim). BODY_PREFIX is the first min(body length, 1 MiB) raw bytes (empty body → SHA-256 of empty input). CONTENT_LENGTH is the decimal full body length the pool forwards (0 when empty).

Limits

Limit Behavior
Prefix window 1 MB Only the first mebibyte is hashed; the rest streams
Declared Content-Length on large bodies Preferred; bodies over 1 MB stream without a 10 MB reject
No Content-Length and body > 10 MB 413 payload_too_large (length cannot be bound without buffering)
Clock skew at origin Match ±300 seconds

Nonce replay defense at the origin is optional (the pool already mints a fresh nonce per attempt). Retry-on-429 still needs a re-readable body under 10 MB; larger bodies with a declared length run as a single streamed attempt and are still prefix-signed.

Rotation

Mint a new secret, update the origin to accept both old and new, then Activate in the dashboard. Discard a pending secret if you abort the rotation.

Worked verification code: Determine a request came from the proxy.

Caller JWT (plan-gated)

Configure a JWKS URL (and optional issuer / audience) on Access. Prefer the X-Nthpool-Jwt header. On pass-through pools, a bearer JWT may also be accepted in Authorization when that does not conflict with upstream auth needs.

Cloudflare Access (plan-gated)

Configure team domain and application audience on Access. Send the Access JWT as Cf-Access-Jwt-Assertion or X-Nthpool-Jwt.

Worked setup: Configure Cloudflare Zero Trust.

IP allowlists (plan-gated)

Attach an IP policy to the pool. Requests from addresses outside the allowlist receive 403 before other auth checks. Policy edits take effect within a few seconds.

Next steps