# Access

## Introduction

Caller authorization always runs **before** a concurrency slot is taken.
Configure caller auth on the source's **Access** tab. Upstream secrets for managed
sources live under [Credentials](/docs/credentials).

Checks run in this order: IP policies → JWT / Cloudflare Access → access key.

## Access keys

An access key admits callers to one source. There are two kinds, and both go
in the same header:

- **Keys you create** under **Sources → Access → Create key**, or with the
  [Control API](/docs/api/control). Today they start with `nb_live_`.
- **Session tokens** that agents get from `start_source_session` over
  [MCP](/docs/mcp). They start with `sv_at_` and always expire. See
  [Session tokens](#session-tokens).

Send the key with `X-Swarmvia-Key`:

```bash
curl -sS "https://your-source.swarmvia.cloud/v1/records" \
  -H "X-Swarmvia-Key: $ACCESS_KEY"
```

On **managed** sources you may instead send
`Authorization: Bearer $ACCESS_KEY` or `Authorization: Swarmvia $ACCESS_KEY`.
`Authorization: Nthbouncer` is still accepted. `X-Nthpool-Key` is still accepted.

On **pass-through** sources, always use `X-Swarmvia-Key` for the access key so
`Authorization` can carry the upstream credential.

Missing or invalid keys return `401`. The message says why; see
[Understand admission errors](/docs/knowledge-base/admission-errors#401-unauthorized).

### Session tokens

When an agent calls `start_source_session`, swarmvia signs a short-lived
access token for that source and returns it with its `expires_at`. The token
works only on the source it was issued for, with the access it was issued
with (read or write), and acts for the member who connected the agent.

- **Lifetime:** 1 hour by default. The agent may ask for `24h` or `7d` with
  `expires_in`. A session token cannot be made non-expiring.
- **Revocation:** a token expires on its own, so nothing needs to end a
  session when the work is done. Revoke a token only if it leaked: on the
  **Access** tab, or with `revoke_source_session` from the agent. It stops
  working at once.
- **Expiry:** an expired token returns `401` with `access token expired`. The
  agent starts a new session and carries on. Tokens are not renewed in place.

A new token works on its first request: swarmvia checks its signature, so
there is nothing to wait for.

Keys you create are moving to short-lived tokens too. Until then they work as
they always have.

### Keys that act for a member

A key minted with **Acts for: Me** carries your identity. The source's logs
name you as the caller, and a [per-member credential](/docs/credentials#per-member-credentials)
uses your own connected account for its calls. Keys agents mint over MCP
act for the member who connected the agent. Leave **Acts for** off for
machine keys such as CI.

When a member is removed from the org or leaves it, the keys that act for
them are revoked. Machine keys are not affected.

### 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 access 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 source](/docs/sources/database) the HTTP method is not the write
gate. Every query is a `POST`, including a read. The write boundary is the
Postgres role swarmvia 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 (`source_query`, `source_schema`,
`source_count_rows`) are read-only; there is no write tool to hide.

## Origin verification (outbound HMAC)

Enable **Origin verification** on **Sources → Access**. swarmvia then signs
**every request it forwards** so your origin can prove traffic came through
swarmvia and reject direct callers.

The canonical string hashes only the first **1 MB** of the body and binds
`Content-Length`, so large uploads can stream. The secret is a source-level
`osk_…` value (shown once when you enable or rotate).

### What swarmvia sends upstream

After stripping every inbound `X-Swarmvia-*` header (callers cannot forge these),
swarmvia adds:

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

Canonical string (fields joined with `\n`):

```text
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
swarmvia 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 &gt; **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 (swarmvia 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](/docs/knowledge-base/origin-verification).

## Caller JWT (plan-gated)

Configure a JWKS URL (and optional issuer / audience) on Access. Prefer the
`X-Swarmvia-Jwt` header. On pass-through sources, 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-Swarmvia-Jwt`.

Worked setup:
[Configure Cloudflare Zero Trust](/docs/knowledge-base/cloudflare-zero-trust).

## IP policies (plan-gated)

Attach an [IP policy](/docs/ip-policies) to the source. Requests from a blocked
range, or from outside the allowed ranges when an allow policy is attached,
receive `403` before other auth checks. Policy edits take effect within a few
seconds.

## Next steps

- [Determine a request came from the proxy](/docs/knowledge-base/origin-verification)
- [Sources](/docs/sources)
- [IP Policies](/docs/ip-policies)
- [Quickstart](/docs/quickstart)
