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

# Use your own spreadsheet

> Derive a protected local registry API from the headers, stable key, disclosure policy, caller, and purpose of one reviewed workbook.

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

Use this tutorial after
[running the sample registry API](../publish-spreadsheet-secured-registry-api/).
You will derive every source and access value from a workbook you control,
validate the project, and prove denied and allowed behavior on the resulting
route.

<QuickstartMeta
  outcome="A protected local API over one reviewed spreadsheet, with the route, returned fields, purpose, and local caller derived from your own decisions."
  time="About 30 minutes for a small worksheet"
  level="Country implementer"
  prerequisites={['Linux amd64', 'Registryctl 0.15.0 with its matching image lock', 'Docker Engine with Compose v2', 'curl', 'A reviewed non-personal XLSX workbook', 'A text editor'] }
/>

Use a synthetic, public, or otherwise approved non-personal workbook for this
local evaluation. Do not use live country data, personal data, production
credentials, or an unreviewed export.

## Before you start

Make a reviewed derivative that contains:

- one selected worksheet with one header row;
- exactly two columns for this first adaptation;
- one stable, non-empty, unique text key;
- one text field approved for disclosure;
- no formulas in the selected worksheet; and
- at least one approved row whose key you can use as a local principal.

Registry Stack does not evaluate formulas, follow external workbook links, or
infer relationships between sheets. Review the
[XLSX readiness contract](../../products/registry-relay/xlsx-readiness-contract/)
before adapting a larger or less regular workbook.

Write down the decisions you will substitute for the angle-bracket
placeholders in this tutorial:

| Placeholder | Derive it from |
| --- | --- |
| `<entity-id>` and `<key-field>` | Stable lower-snake-case names for the selected worksheet and its unique key |
| `<released-field>` | A lower-snake-case name for the second, approved column |
| `<key-header>` and `<released-header>` | The two exact workbook header cells, including spaces and capitalization |
| `<worksheet-name>` | The exact worksheet tab name |
| `<matching-key-value>` | One approved, non-personal key cell that exists in the worksheet |
| `<purpose>` | A reviewed identifier for why callers may use this API |
| `<registry-id>` and `<service-id>` | Stable identifiers for the authored registry and its records policy |

The examples below quote exact workbook header cells. Keep the angle brackets
only while reading. Replace every placeholder in the authored files before
running validation.

Set the two values reused by shell commands in this tutorial. Replace the
quoted placeholders with your actual identifiers:

```sh
entity_id='<entity-id>'
request_purpose='<purpose>'
```

These assignments print nothing. Keep using the same shell for the remaining
commands.

## Create the project

Create a new project from the maintained spreadsheet starter:

```sh
registryctl init --from spreadsheet --project-dir reviewed-registry
cd reviewed-registry
```

Registryctl prints:

```text
Initialized Registry Stack project "fictional-public-works-registry".
  Directory: reviewed-registry
  Starter: spreadsheet (Registry Stack 0.15.0)
  Starter content: matches bundled digest
  Editor support: VS Code and Zed (reviewed-registry/.registry-stack-editor/manifest.json)
```

Registryctl follows those lines with a `Next` section that tells you to enter
the directory, check the local project, and start Relay. The starter is the
reviewed baseline you will adapt. The `cd` command prints nothing.

Copy your workbook into the project and give the entity file its authored
name:

```sh
cp /path/to/your-reviewed-workbook.xlsx data/reviewed-source.xlsx
mv entities/projects.yaml "entities/${entity_id}.yaml"
```

These commands print nothing when they succeed. Registryctl reads the copied
workbook but never writes its cells.

You will edit only these human-owned inputs:

| File | Decision it owns |
| --- | --- |
| `data/reviewed-source.xlsx` | The exact approved source bytes |
| `entities/<entity-id>.yaml` | Logical field names, stable key, strict types, and row limits |
| `environments/local.yaml` | Worksheet headers, workbook path, and local caller principals |
| `registry-stack.yaml` | Registry identity, service, purpose, scopes, filters, and returned fields |

Do not edit `.registry-stack/`. Registryctl generates that directory and may
replace it.

## Describe the two workbook fields

Replace `entities/<entity-id>.yaml` with this strict two-field schema after
substituting your three logical names:

```yaml
version: 1
id: <entity-id>
revision: 1
primary_key: <key-field>
schema:
  type: object
  additionalProperties: false
  required: [<key-field>, <released-field>]
  properties:
    <key-field>: { type: string, minLength: 1, maxLength: 128 }
    <released-field>: { type: string, minLength: 1, maxLength: 256 }
materialization:
  max_records: 10000
  max_bytes: 16MiB
  refresh: manual
  retain_generations: 2
```

`primary_key` names the stable key. Every selected column is required and
explicitly typed. `additionalProperties: false` makes a new source column a
review event instead of silently adding it to the entity.

Do not use a row number or mutable display name as the key. If either column
may legitimately be empty or needs a different type or length, make that
decision explicitly using the
[entity definition reference](../../reference/project-configuration/)
before continuing.

## Bind exact workbook headers

Replace `environments/local.yaml` with the following content after substituting
the entity, worksheet, header, and matching-key values:

```yaml
version: 1
entities:
  <entity-id>:
    provider:
      type: xlsx
      project_file: data/reviewed-source.xlsx
      path: /var/lib/registry/reviewed-source.xlsx
      sheet: "<worksheet-name>"
      header_row: 1
    columns:
      <key-field>: "<key-header>"
      <released-field>: "<released-header>"
    source_revision: reviewed-source-v1
    generation: "1"
relay:
  origin: https://records-relay.internal.invalid
  issuer: https://workload-issuer.internal.invalid
  jwks_url: https://workload-issuer.internal.invalid/.well-known/jwks.json
  audience: registry-relay
  allowed_clients: [local-reviewed-client]
  local_api_keys:
    match_principal: "<matching-key-value>"
    no_match_principal: registryctl_local_no_match
    scopes: [<entity-id>:metadata, <entity-id>:rows]
deployment:
  profile: local
  relay: { service: records-relay }
```

The keys on the left side of `columns` are the logical field names from the
entity schema. The quoted values on the right are exact header cells in the
workbook.

Set `match_principal` to an approved key cell that exists in the workbook.
Keep `registryctl_local_no_match` absent from the key column. These two
principals let you prove one-row and no-row behavior without creating
production identities.

Local API keys exist only for this local profile. Registryctl writes their raw
values to a private runtime directory. A deployed environment must use its
reviewed identity provider and secret management.

## Author the records policy

Keep the `version` and `starter` block that Registryctl created at the top of
`registry-stack.yaml`. Replace the `registry`, `entities`, and `services`
sections beneath it with:

```yaml
registry:
  id: <registry-id>

entities:
  <entity-id>:
    file: entities/<entity-id>.yaml

services:
  <service-id>:
    kind: records_api
    entity: <entity-id>
    title: Reviewed local records
    description: Protected view of the approved workbook derivative.
    owner: Local evaluation team
    sensitivity: confidential
    access_rights: restricted
    update_frequency: as_needed
    api:
      scopes:
        metadata: <entity-id>:metadata
        rows: <entity-id>:rows
        aggregate: <entity-id>:aggregate
        evidence_verification: <entity-id>:evidence_verification
      purposes: [<purpose>]
      projection: [<key-field>, <released-field>]
      pagination: { default_limit: 50, max_limit: 100 }
      filters: { <key-field>: [eq] }
      required_principal_filters: [<key-field>]
      standards: { ogc_features: false, sp_dci: false }
```

This policy derives the API contract:

- the request purpose header is `Data-Purpose: <purpose>`;
- the returned object contains only `<key-field>` and `<released-field>`;
- the allowed filter is equality on `<key-field>`; and
- the required key filter is bound to trusted caller identity.

The service identifier owns the policy but does not appear in the URL.
Registryctl derives the records URL from the entity identifier:

```text
http://127.0.0.1:4242/v1/datasets/<entity-id>/entities/<entity-id>/records
```

A caller-supplied query value cannot satisfy or replace the required
principal filter. Relay obtains that value from authenticated gateway
identity. A field is not readable merely because it exists in the workbook or
entity schema. It must also appear in the reviewed projection.

## Validate and start the protected API

Ask Registryctl to validate the workbook, authored policy, generated
configuration, release files, listener, and Docker runtime:

```sh
registryctl doctor --profile local
```

A successful result starts with:

```text
Registry Stack doctor: ok

Project: <absolute-project-path>
Profile: local
registry-relay: ok (0 errors, 0 warnings)
```

`<absolute-project-path>` is the path to `reviewed-registry` on your computer.
If the check fails, correct the workbook or one of the three authored YAML
files named in the diagnostic. Do not edit `.registry-stack/`.

A successful check proves that:

- the selected worksheet and headers exist;
- every selected row matches the strict field schema;
- required keys are present and unique;
- formulas, unexpected columns, unsafe paths, and invalid values were rejected;
- the route, scopes, purpose, filters, and projection form a valid Relay
  policy; and
- the matching release runtime can start on the loopback listener.

Start Relay:

```sh
registryctl start
```

After Docker progress, Registryctl prints exactly:

```text
PASS readiness: compiled workbook source is ready
Relay API:  http://127.0.0.1:4242
API docs:   http://127.0.0.1:4242/docs
```

Relay parses the workbook again during its initial load. The listener is
limited to `127.0.0.1:4242`. The generated configuration and workbook are
mounted read-only.

Do not run `registryctl smoke` for this adapted project. In v0.15, that command
checks the unmodified starter's `projects` route and sample purpose. Use the
derived requests below to check your authored route and purpose.

## Prove the anonymous request is denied

Substitute your entity and purpose in this request:

```sh
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
  -H "Data-Purpose: ${request_purpose}" \
  "http://127.0.0.1:4242/v1/datasets/${entity_id}/entities/${entity_id}/records"
```

Relay prints exactly:

```text
HTTP 401
```

This proves that the protected route rejects a missing credential. A valid
purpose header does not substitute for authenticated caller identity.

## Prove the matching caller is allowed

Load the private local keys without printing them:

```sh
set -a
. .registry-stack/runtime/local/secrets/local.env
set +a
```

These commands print nothing. Do not print, paste, commit, or retain the raw
keys outside this disposable local runtime.

Substitute the same entity and purpose:

```sh
curl -sS -w '\nHTTP %{http_code}\n' \
  -H "Authorization: Bearer $REGISTRYCTL_LOCAL_RELAY_MATCH_KEY_RAW" \
  -H "Data-Purpose: ${request_purpose}" \
  "http://127.0.0.1:4242/v1/datasets/${entity_id}/entities/${entity_id}/records"
```

Relay returns this structure using your actual field names and row values.
JSON whitespace and object-key order may differ:

```json
{
  "data": [
    {
      "<key-field>": "<matching-key-value>",
      "<released-field>": "<value-from-that-row>"
    }
  ],
  "pagination": {
    "has_more": false
  }
}
HTTP 200
```

This proves that the credential authenticates the local caller, the purpose
and scopes authorize the operation, the principal-bound key selects exactly
the matching row, and the projection limits the disclosed fields. The request
URL, purpose, key, and response fields all come from the authored values you
derived from the workbook and access decision.

## Stop and clean up

Stop the local runtime:

```sh
registryctl stop
```

Docker may print container shutdown progress. Registryctl prints no additional
success line after Compose exits successfully.

The authored project and workbook remain. Generated build and runtime
directories are disposable and can be regenerated.

## What you built

You built a loopback-only protected API over one reviewed XLSX worksheet. Its
entity and request route come from your logical entity name. Its source mapping
comes from exact worksheet headers. Its authorized row comes from the stable
key used as local principal identity. Its purpose and returned fields come
from explicit service policy.

You validated the workbook and policy, then confirmed the running API's denied
and allowed behavior.

## Next

- [Prepare the operator handoff](../../operate/) without copying workbook rows,
  raw keys, or private runtime details into public evidence.
- [Review the security guidance](../../security/) before connecting a real
  identity provider or widening access.
- [Look up project configuration](../../reference/project-configuration/)
  when you need more columns, types, fixtures, or environment-specific policy.

## Common source problems

| Problem | What to change |
| --- | --- |
| Wrong worksheet or header | Correct `sheet`, `header_row`, or the exact quoted header mapping. |
| Missing or unexpected column | Align the reviewed workbook derivative, entity schema, and column mapping. |
| Missing or duplicate key | Resolve the source identity problem. Registry Stack will not choose one duplicate. |
| Invalid type or null required value | Correct the workbook derivative or deliberately change the schema. |
| Formula or external reference | Replace it with an approved static value in the derivative. |
| Path traversal or workbook symlink | Copy the reviewed file under `data/` and use that regular file. |
| An unintended field appears in `projection` | Remove it before building, or obtain the required review. |