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

# Run your first protected registry API

> Install Registryctl, start the maintained spreadsheet sample, and prove that caller identity controls which records Relay returns.

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

Registry Relay can publish a protected, read-only API over a spreadsheet
without copying the workbook into a second database. In this tutorial, you
will start the maintained synthetic sample and prove that an anonymous caller
is denied while an authorized caller receives only its own projected record.

<QuickstartMeta
  outcome="A protected API over a synthetic workbook, available only on your computer, with denied and authorized requests you can inspect."
  time="About 15 minutes after Docker is installed"
  level="Local single-node"
  prerequisites={['Linux amd64', 'Bash', 'Docker Engine with Compose v2', 'curl', 'Outbound HTTPS access to GitHub', 'Port 4242 available on loopback'] }
/>

The workbook and caller identifiers in this tutorial are synthetic. Do not put
personal data or production credentials in this project.

## Before you start

This complete local runtime is supported on Linux amd64. Registryctl also has
Linux arm64 and macOS arm64 command-line assets for authoring, validation, and
build tasks, but the complete runtime is not supported on those hosts in this
release.

Confirm that Docker Engine and Compose v2 are available:

```sh
docker info >/dev/null
docker compose version
```

The first command prints nothing when Docker is available. The second prints a
Compose v2 version, for example:

```text
Docker Compose version v2.x.x
```

This proves that the local container provider is reachable. Registryctl will
perform its own stricter runtime and project checks before starting Relay.

## Install Registryctl

Download and run the Registryctl 0.15.0 installer:

```sh
tag=v0.15.0
curl -fsSLO "https://github.com/registrystack/registry-stack/releases/download/${tag}/registryctl-${tag}-install.sh"
bash "./registryctl-${tag}-install.sh"
registryctl --version
```

The installer reports its chosen install directory. The last command prints
exactly:

```text
registryctl 0.15.0
```

The installer downloads release files over TLS and verifies their checksums.
A checksum detects a damaged or mismatched download, but does not authenticate
who published it. Follow the tag-frozen
[release verification guide](https://github.com/registrystack/registry-stack/blob/v0.15.0/release/VERIFY.md)
when you need signature and provenance verification before installation.

If your shell cannot find `registryctl`, add the install directory reported by
the installer to `PATH`.

## Create the sample project

Create the maintained spreadsheet starter in a new directory:

```sh
registryctl init --from spreadsheet --project-dir my-first-api
cd my-first-api
```

Registryctl prints:

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

Next:
  cd my-first-api
  registryctl doctor --profile local
  registryctl start
```

The starter digest proves that the authored sample files match the copy
bundled with this Registryctl release. The `cd` command prints nothing.

You do not need to copy product configuration or edit generated Relay YAML.
These are the files you will encounter:

| Path | What it controls |
| --- | --- |
| `registry-stack.yaml` | The API purpose, caller scopes, filters, and returned fields |
| `entities/projects.yaml` | The stable key and field types |
| `environments/local.yaml` | The workbook, worksheet, columns, and local test callers |
| `data/public_works_projects.xlsx` | The synthetic source data |
| `.registry-stack/build/local/` | Generated product configuration. Review it, but do not edit it. |
| `.registry-stack/runtime/local/` | Private, disposable local runtime files |

The `.registry-stack/` directory is ignored by version control. On Unix,
Registryctl gives runtime secret directories mode `0700` and raw credential
files mode `0600`.

## Check your computer and project

Check the project, release files, workbook, listener, and Docker runtime:

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

A successful check 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 `my-first-api` on your computer.
Additional provider and release-file lines identify the exact inputs
Registryctl checked.

This proves that the authored project can be compiled, the workbook passes
strict validation, the generated result matches the authored inputs, the
listener is loopback-only, and the matching release runtime is available.
If it fails, follow the reported correction before continuing.

## Start the API

Start the exact compiled project:

```sh
registryctl start
```

Container progress lines depend on your Docker version. After Relay becomes
ready, 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
```

This proves that Relay loaded the compiled workbook source and is ready on the
loopback listener. The generated Compose document mounts both the compiled
Relay configuration and workbook read-only.

## Check the API

Run the sample's maintained checks:

```sh
registryctl smoke
```

Registryctl prints exactly:

```text
PASS allowed public health check
PASS allowed match source is ready
PASS denied anonymous records request
PASS denied wrong local API key
PASS allowed matching principal returns one record
PASS wrong principal safely returns no match
```

Together, these checks prove that the public health route is available, the
protected route rejects missing and invalid credentials, the matching caller
receives one row, and a valid caller bound to an absent key receives no other
row.

Registryctl saves a machine-readable report at
`.registry-stack/runtime/local/smoke-results.json`. It contains check names,
paths, status classes, and pass states. It does not contain raw keys or
workbook rows.

## Make one denied request

Call the records route without a credential:

```sh
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
  -H "Data-Purpose: public-works-case-management" \
  http://127.0.0.1:4242/v1/datasets/projects/entities/projects/records
```

Relay prints exactly:

```text
HTTP 401
```

The API is ready, but this protected route does not accept an anonymous
caller. Supplying a purpose does not substitute for caller identity.

## Make one allowed request

Load the private local caller keys into this shell:

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

These commands print nothing. Do not print, paste, or commit the environment
file.

Use the matching synthetic caller to make the same request:

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

Relay returns this structure, followed by the exact status line shown. JSON
whitespace and object-key order may differ:

```json
{
  "data": [
    {
      "project_id": "pw_001",
      "district_code": "north-01",
      "sector": "water",
      "status": "active"
    }
  ],
  "pagination": {
    "has_more": false
  }
}
HTTP 200
```

The credential authenticates the local caller. The requested purpose and
scopes authorize the records operation. Relay then derives the required
`project_id` filter from that caller's principal and returns only the four
fields in the reviewed projection.

A caller cannot satisfy this required filter by adding another
`project_id` to the URL. The principal-bound filter comes from trusted gateway
identity, not from caller-controlled query parameters. The smoke check's
no-match result proves the safe alternative: a valid caller whose principal
does not match a workbook key receives an empty collection.

## See what you can edit

Open the following files in your text editor:

| Authored input | Decision to inspect |
| --- | --- |
| `registry-stack.yaml` | Purpose, operation scopes, projection, allowed filters, and principal-bound filters |
| `entities/projects.yaml` | Stable key, strict record schema, and materialization limits |
| `environments/local.yaml` | Workbook binding, exact worksheet headers, and synthetic local principals |

Then ask Registryctl to explain the effective plan:

```sh
registryctl check --project-dir . --environment local --explain
```

The output begins:

```text
Registry Stack project: fictional-public-works-registry (valid)
Environment: local
Baseline: initial_without_baseline
Semantic changes: claim, integration, service_policy, operator_security, disclosure
```

Later sections report one records API service, the effective scopes and
disclosure plan, and redaction counts. This proves that Registryctl validated
the human-authored project and derived a reviewable effective plan.

Project fields compile as sensitive regardless of whether their physical
provider is XLSX, CSV, Parquet, or PostgreSQL. A field is not externally
readable merely because it exists in the source or entity schema. It must also
appear in the reviewed service projection.

## Stop and clean up

Stop the local runtime:

```sh
registryctl stop
cd ..
```

Docker may print container shutdown progress. Registryctl prints no additional
success line after Compose exits successfully. `cd ..` returns you to the
directory that contains `my-first-api`, ready for the next tutorial.

This stops and removes the local containers and network. It keeps the authored
project and workbook. The generated build and runtime directories are
disposable and can be regenerated from the authored project.

## What you built

You ran a loopback-only Relay API over a synthetic XLSX workbook. You proved
that:

- the anonymous and invalid-key paths are denied;
- an authorized principal receives only its own matching record;
- a valid non-matching principal cannot see another record; and
- the API returns only fields selected by authored disclosure policy.

The workbook, entity schema, environment binding, and service policy remain
human-owned. Registryctl generates and validates the product configuration
that Relay consumes.

## Continue

[Verify a live registry-backed claim](../verify-claim-registry-api/) to add
Registry Notary to this `my-first-api` project and evaluate a bounded claim
through the running Relay path.

When you are ready to replace the sample,
[use your own spreadsheet](../use-your-spreadsheet/) to derive the entity,
workbook mapping, disclosure policy, local principal, purpose header, and
request URL from a reviewed workbook.

Before a broader adoption, [decide when Registry Stack
fits](../../start/when-to-use/) and review the supported product and trust
boundaries.

## Troubleshooting

| Symptom | Cause | Resolution |
| --- | --- | --- |
| `registryctl` is not found | The install directory is not on `PATH`. | Add the directory printed by the installer, usually `~/.local/bin`, to `PATH`. |
| The installer reports an unsupported platform | No Registryctl asset is published for that OS and CPU. | Use a published CLI platform for authoring, or Linux amd64 for this complete local runtime. |
| `doctor` cannot find Docker or Compose v2 | Docker Engine is absent or stopped. | Start Docker and confirm `docker info` and `docker compose version`, then retry. |
| `doctor` rejects the image lock | Registryctl and the image lock are absent, changed, or from different releases. | Rerun the same versioned installer. Do not substitute a mutable image tag or another release's lock. |
| `doctor` reports a workbook failure | The declared file, sheet, header, strict columns, types, formulas, or primary key failed validation. | Correct the human-owned workbook, entity, or environment file. Do not edit generated Relay input. |
| A generated artifact integrity check fails | A file under `.registry-stack/build/` or `.registry-stack/runtime/` was edited or replaced. | Remove the disposable generated closure and rerun `registryctl doctor`. Reapply intended changes only in human-owned files. |
| A request returns `401` | The credential is missing or invalid. | Load the generated local environment without printing it and use the matching local key. |
| A request returns `403` | The caller lacks the reviewed scope or purpose. | Review the service and environment bindings. Do not widen scopes merely to bypass the denial. |
| The documented recovery does not solve the problem | The local tutorial does not cover every host or workbook condition. | Open a [public Registry Stack issue](https://github.com/registrystack/registry-stack/issues/new/choose) with the command, stable code, operating system, and Registryctl version. Do not include keys, workbook rows, or private paths. |