Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/v/0.38.0/llms.txt
Full corpus: https://docs.registrystack.org/v/0.38.0/llms-full.txt

# Operate Registry Relay

> Deploy one sealed Registry package with read-only SQLite, authentication, auditing, limits, and a private listener.

Deploy one reviewed Relay package without allowing deployment configuration to change Registry
identity, Registry Core, source bindings, access profiles, wire formats, classifications, or disclosure.
As the operator, you own service paths, sources, secrets, issuers, audit retention, limits,
readiness, and replacement of complete revisions.

## When to use this

Use this guide after a data publisher supplies a package produced by `relayctl package`, the
matching SQLite source, and an approved change report.
Return to [Relay project authoring](../../configure/relay/) if any of those inputs changes.

Relay runs one Registry per process.
Deploy another Registry under a separate service when it has a different Authority or
administrative trust boundary.

## Before you start

Prepare a dedicated Unix service identity, a private listener behind Transport Layer Security
(TLS) termination, a sealed package, the matching snapshot or live read-only source, an audit
file path or a log collector on standard output, a cursor-encryption key, and a compatible OAuth
issuer for protected access profiles.
Do not place the package, source, secret, or audit path in a shared writable directory.
For snapshot mode, make the SQLite file immutable outside Relay, preferably with a read-only mount.
Relay verifies its captured digest before and after every statement, but a process cannot exclude a
privileged writer that changes and restores bytes entirely between both checks.

Relay validates trusted path components before use.
They must not be symbolic links or writable by group or world, and must be owned by root or the
service identity.
The service fails closed on non-Unix platforms where it cannot enforce those ownership and mode
checks.

## Bind deployment inputs without editing the package

The runtime file names local deployment bindings and must not restate or override governed policy:

```yaml
apiVersion: registry.registrystack.org/relay-runtime/v1alpha1
kind: RelayRuntimeConfig
listener: {bind: "127.0.0.1:8080"}
package: {root: /etc/relay/business/package}
secretProviders:
  file: {root: /run/secrets/relay}
sources: {companies: {path: /srv/registries/business.sqlite}}
audit:
  path: /var/lib/relay/business/audit.jsonl
cursor: {integrityKeyRef: secret:file/cursor-integrity-key, maximumAgeSeconds: 300}
limits: {requestTimeoutMilliseconds: 1500, concurrentQueries: 32}
quotas: {requestsPerMinute: 120, burst: 20}
```

`package.root` is an absolute path. Add `package.expectedDigest` with the package digest
`relayctl package` reported, the `sha256:` digest of the package's `SHA256SUMS`, to refuse any
other package at that path. A `secret:file/` reference resolves
under `secretProviders.file.root`, never beside the runtime file; a `secret:env/` reference needs
`secretProviders.environment: {}`. Other values may use `${VAR}` or `${VAR:-default}`
substitution, but a field ending in `Ref` and every value under `secretProviders` refuse it. A removed key such as `server`,
`packagePath`, `audit.sink`, or `authentication.issuer` is refused with the name of its
replacement, and `audit.integrityKeyRef` with a diagnostic saying to remove it.

A package with protected access declares its one issuer:

```yaml
authentication:
  oidc:
    issuer: https://identity.example.org
    audience: relay-business
    tokenTypes: [at+jwt]
    algorithms: [ES256]
```

`jwksSource` defaults to `kind: discovery`, which reads the issuer's
`/.well-known/openid-configuration`. Use `jwksSource: {kind: uri, uri: <exact JWKS URL>}` when the
keys are served elsewhere; token `iss` validation stays bound to `issuer` either way.

Leaving `authentication.oidc` out is valid only when every compiled access rule is public,
including Record access profiles and fixed statistical-dataset access rules. A package with either
kind of protected access needs the configured issuer at startup.
The issuer's verified claims may establish scopes, purpose, and row authority, but cannot enable an
operation or access profile the package did not compile.
A syntactically valid unknown access profile and a valid principal without its scope receive the
same concealed `404 resource.not_found` response. Relay does not fall back to a less restrictive
access profile.

Relay authenticates and encrypts the complete cursor payload with a fresh nonce. The payload binds
the source and contract revisions, operation, access profile, disclosure profile, filters, fixed
order, selected fields, authorization context, optional bbox, wire format, format profile, and
expiry. Treat cursors as opaque continuation tokens even though they contain no plaintext filter,
order, or bbox values.

`relay serve --runtime-config <file>` opens only the sealed package at `package.root`.
It rejects unsafe paths, a changed, missing, or extra package file (naming the file and
`relayctl package`), source-schema drift, missing mandatory audit
inputs, and incompatible runtime bindings before listening.
There is no deployment switch that disables fail-closed audit behavior.

Quota configuration has two values for the whole Relay deployment. `requestsPerMinute` sets the
refill rate and `burst` sets the short-term capacity. Relay keeps a separate bucket for each
compiled operation, so one busy operation cannot exhaust another. Every access profile of an
operation shares that operation's bucket. Version 1 does not add per-profile, per-client,
or distributed quota modes. Put those controls at the trusted gateway when the deployment needs
them.
The data, dataflow-structure, and data structure definition (DSD) routes for one statistical
dataset share its one compiled statistical-read operation and quota bucket.

## Choose the source profile already reviewed

The package selects the source profile.
Runtime cannot change it.

| Property | Snapshot | Live read-only |
| --- | --- | --- |
| Publisher updates while Relay runs | No | Yes, through a separate trusted publisher. |
| Source revision | Captured content digest | Explicitly unversioned. |
| List and cursor pagination | Supported | Not supported. |
| Cache revalidation | Available only for eligible public access profiles | Disabled. |
| Request consistency | Immutable file | One read transaction. |

Both profiles pin the SQLite schema fingerprint and deny writes, arbitrary SQL, schema drift, and
unbounded result behavior.
Live resources support only identifier read and named exact lookup.
Bounded bbox consultation is a named collection search, so it uses a reviewed snapshot source in
this profile. GeoJSON and JSON-FG do not change the source, access, audit, or cache rules. Their exact
response bytes receive distinct cache validators where public snapshot revalidation is eligible.

A compiled statistical dataset is also snapshot-only in Version 1. Its prepared dataflow and DSD
structure bytes belong to the sealed package, while bounded data responses are produced from the
matching immutable snapshot. Runtime configuration cannot switch that dataset to live read-only,
change its time granularity, or add another statistical format or query.

## Start and verify the service

Start the process using the exact runtime file:

```sh
sudo -u <relay-user> /usr/local/bin/relay serve --runtime-config /etc/relay/business/runtime.yaml
```

Relay reports its listener only after it verifies the package, source, issuer when configured,
audit sink, secrets, limits, and readiness.
Check the private endpoint from the proxy network boundary:

```sh
curl -fsS http://127.0.0.1:8080/health
curl -fsS http://127.0.0.1:8080/ready
```

Both return a successful response only after startup completed.
Publish the API through the operator-controlled TLS proxy or ingress, not by exposing the private
listener directly.

The release image probes `http://127.0.0.1:8080/health` by default.
When `server.bind` uses another port, set `RELAY_HEALTHCHECK_URL` to the loopback `/health` URL on
that port. The `relay healthcheck` command validates the configured URL before making the request.

## Retain value-free audit evidence

For every data operation, including anonymous public access, Relay writes an attempt event as a
`request` entry before SQLite access, and a refusal or a terminal release, unresolved, or
source-failed event as a `response` entry before returning. Both entries carry the operation id as
their `correlation`. The caller holds the exact serialized response bytes until the response entry's
write returns, so that entry gates their release; it does not describe or bind them.
An audit failure blocks source access or withholds the response with `503 audit.unavailable`.

Each line is one envelope, `{schema, eventId, time, phase, correlation, record}`, with schema
`registry.relay.audit/v2alpha2`; the package artifact `generated/artifacts/audit-event.schema.json`
describes one line. `audit.destination` is `file` by default: `path` is absolute or relative to
the runtime file, the file rotates at `rotateBytes` (100 MiB by default), and sealed files older
than `retainDays` (90 by default) are deleted when the writer opens or rotates. One Relay process
writes one path and holds a lock beside it, so give each replica its own path. A `stdout`
destination writes one flushed line per entry for the platform's log collector, and
`relay check --require-audit-under` refuses it because it has no path to prove.

For Record operations, audit binds the Registry, resource, operation, access profile, disclosure
profile, selected property identifiers or digest, processing handling, disclosure handling,
transform identifiers, contract revision, and source revision. Statistical audit uses the same
fail-closed attempt and terminal gates, but records the dataset's fixed access rule, exact data or
structure surface, and selected SDMX wire format rather than inventing an access profile.
It excludes tokens, selectors, raw principals, source values, response values, and raw Record
identifiers.
The log carries no hash chain or signature, so it is not tamper-evident on the host. Ship sealed
files, or the `stdout` stream, to append-only storage before retention deletes them, and restrict
read access to the audit path separately from ordinary service operations.

:::caution[Move the chained log aside before upgrading]
Relay does not detect lines written in the older chained format, and retention deletes sealed
files named `<path>.<sequence>`, which is how the older segments are named. Before the first start
with `audit.path`, archive the existing audit file and its segments to append-only storage, and
give the upgraded Relay a fresh path in a directory that holds no old segments.
:::

Operational logs are also value-free.
Use them for route template, status, latency, and trace correlation, not to recover request or
Registry data.

## Replace complete revisions

Relay does not hot-reload, merge, overlay, or fall back across packages.

1. Receive a newly reviewed package, compatible source, and change report.
2. Install them at new trusted revisioned paths without altering the active revision.
3. Start a candidate on a private listener and wait for readiness.
4. Send a smoke request through the production proxy policy.
5. Shift traffic, drain the previous process, and terminate it.
6. Retain the package, source revision, audit files, and change review under institutional policy.

Rollback activates a complete prior package only with its compatible source and runtime bindings.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| Startup refuses a path | A component is a symlink, has the wrong owner, or is writable | Move the deployment to trusted Unix paths and correct ownership and modes. |
| Startup reports a package or schema error | The package, source, or review revision does not match | Stop deployment and return the change to the authoring workflow. |
| A protected access profile returns `404 resource.not_found` | The issuer or token does not satisfy that profile's exact scope | Correct the issuer or caller authority. Do not expose a weaker profile as fallback. |
| A spatial request returns `406 format.unsupported` | The selected access profile does not disclose a primary geometry or the format request is unsupported | Select an entitled geometry-bearing profile or request JSON or JSON-LD. |
| A response is withheld | The terminal audit write failed and the writer stopped | Restore the audit storage, restart Relay, and wait for readiness before accepting traffic. |

## Next

- [Rotate credentials, keys, certificates, and trust](../advanced/rotate-credentials-and-trust/)
  to rotate the cursor-encryption or token issuer credentials this deployment used.
- [Retention and persistent state](../retention-and-persistent-state/) to plan audit file
  retention beyond what this guide covers.
- [Inspect and diagnose a running deployment](../advanced/inspect-and-diagnose/) to diagnose a
  failing readiness or authorization check after deployment.