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

# Configure an exact snapshot materialization

> Use one private materialization binding for a governed records API and exact evidence consultations in a Registry Stack project.

import ProjectStarterSequence from '../../../components/ProjectStarterSequence.astro';

Use this guide when Registry Relay must query an immutable local materialization instead of
opening a source connection for each consultation.
You will declare one logical entity, bind its physical source once in the private
environment, and reuse the published snapshot for a governed records API and exact `snapshot`
consultations.

{/* Evidence: crates/registryctl/tests/fixtures/project-authoring/snapshot-exact/ and
    crates/registryctl/tests/project_authoring.rs,
    records_and_snapshot_share_one_generated_materialization. */}

## When to use this

Use `snapshot` for an exact key lookup over a separately ingested local snapshot.
The consultation reports `materialized_snapshot` provenance and does not open a registry
connection during the request.

Background ingest can acquire more data than one consultation.
Record the complete materialization footprint and do not describe snapshot publication time as
source observation time.

## Before you start

You need:

- A Registry Stack project with an immutable CSV, XLSX, or other supported snapshot source.
- A logical entity schema and stable primary key.
- A private provider path and one physical column for every logical field.
- Reviewed records API scopes, purposes, filters, pagination, relationships, and aggregates.

Keep provider paths, worksheet or table names, and physical columns out of public entity and
integration files.

## Try the bundled snapshot starter

The starter proves an exact lookup over an immutable local materialization. Its offline fixture
does not open a source connection.

<ProjectStarterSequence starter="snapshot" />

## Define the logical entity

Create `entities/people.yaml` with logical names only:

```yaml
version: 1
id: people
revision: 1
primary_key: person_id
schema:
  type: object
  additionalProperties: false
  required: [person_id, registration_status, eligible]
  properties:
    person_id: { type: string, maxLength: 64 }
    registration_status: { type: [string, "null"], maxLength: 32 }
    eligible: { type: [boolean, "null"] }
materialization:
  max_records: 1000000
  max_bytes: 256MiB
  refresh: manual
  retain_generations: 2
```

The entity owns the reusable logical schema and bounded materialization footprint.
It rejects physical provider, path, table, worksheet, and column members. A records service is
optional and does not change the entity used by `snapshot`.

## Add the records service

Reference the entity and configure its optional records service in `registry-stack.yaml`:

```yaml
entities:
  people: { file: entities/people.yaml }
services:
  people-records:
    kind: records_api
    entity: people
    title: Population records
    sensitivity: personal
    access_rights: restricted
    update_frequency: daily
    api:
      scopes:
        metadata: people:metadata
        rows: people:rows
      purposes: [case-management]
      projection: [person_id, registration_status, eligible]
      pagination: { default_limit: 50, max_limit: 100 }
      filters:
        person_id: [eq]
        registration_status: [eq]
      required_principal_filters: [person_id]
      standards: { ogc_features: false, sp_dci: false }
```

This records service is the Relay-owned records API exposure of `people`. It preserves only its
declared scopes, purposes, projection, filters, pagination, relationships, aggregates, and
standards adapters.

## Define the exact snapshot integration

Create an integration that refers to logical entity fields:

```yaml
version: 1
id: population-person-snapshot
revision: 1
input:
  person_id:
    role: selector
    type: string
    maxLength: 12
    pattern: "^PER-[0-9]{8}$"
capability:
  snapshot:
    entity: people
    exact:
      person_id: { input: person_id }
    freshness: 24h
outputs: [registration_status, eligible]
```

The `snapshot` capability applies the complete exact selector to one immutable published handle and probes at
most two rows.
Two matches return `ambiguous`; Registry Relay does not choose one.

Add an evidence service and consultation in `registry-stack.yaml`, mapping `person_id` from one
declared request identifier. This Notary-owned service defines its service policy, claims,
disclosure, and optional credential profiles. Claims can select only the integration's closed
outputs through their authored `output` field.

## Bind the physical source once

In `environments/<environment>.yaml`, bind the logical entity to its private provider:

```yaml
entities:
  people:
    provider:
      type: csv
      path: /var/lib/registry/population.csv
      header_row: 1
    columns:
      person_id: subject_key
      registration_status: status_code
      eligible: benefit_eligible
    source_revision: population-export-v1
    generation: 2026-07-12
```

Every logical field has one physical column, and physical columns must be unique.
Change `generation` when the provider or mapping changes.

`registryctl build` emits one ingest plan and one materialization identity.
The records service and compatible snapshot profiles capture the same published handle
instead of creating another source path or copying the physical mapping.

## Verify shared materialization

Run the fixtures, semantic check, and build:

```sh
registryctl test --project-dir <project>
registryctl check \
  --project-dir <project> \
  --environment <environment> \
  --explain
registryctl build \
  --project-dir <project> \
  --environment <environment>
```

The test and build JSON reports contain `"status": "passed"` and `"status": "built"`.
The human-readable check report marks the project `valid`.
Confirm that the explanation names one logical materialization and that no reviewable integration
pack or consultation contract contains the provider path or physical column names.

If the snapshot is absent, stale, or does not match the active generation and digest, only the
dependent profiles become unready.
Registry Relay does not fall back to live source access.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| The entity schema rejects a provider or column | Physical mapping was placed in the public definition. | Move the provider and columns into the environment entity binding. |
| The environment rejects duplicate columns | Two logical fields map to one physical column. | Provide an injective physical mapping. |
| A provider change is rejected | The mapping changed without a new generation. | Advance the generation and repeat operator-security review. |
| Only snapshot profiles are unready | The shared materialization is missing, stale, or inconsistent. | Publish and verify the expected immutable generation without enabling live fallback. |

## Next

- Return to [Author an HTTP Registry Stack project](../author-registry-project/) for the
  ordinary review and unsigned-build workflow.
- Use [Configure a script source adapter](../configure-project-script-adapter/) only for a
  reviewed live HTTP recipe that declarative operations cannot express maintainably.