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

# Query a spatial registry from QGIS

> Start a synthetic PostGIS registry, connect QGIS with an installation credential, and refresh the map after an authenticated write.

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

If you are evaluating Base Registry Engine's spatial read path, this tutorial starts a synthetic
service-site registry on PostGIS and connects QGIS as a read-only installation client. PostGIS is the
PostgreSQL extension that adds geometry types and spatial predicates. Base Registry Engine requires it
only when a profile that lists records also declares a bounding box query, and the `--spatial` launcher
flag supplies it by starting a PostGIS image in place of the plain PostgreSQL one. You will
display authorized Point records, pan within a bounded map area, import one record through the
normal API, and see the new Point after QGIS renews its Registry Mint credential.

<QuickstartMeta
  outcome="One protected QGIS Point layer that pages, pans, renews its credential, and refreshes after an authenticated Base Registry Engine write."
  time="About 35 minutes, plus the PostGIS image download"
  level="Local evaluation with synthetic data on loopback"
  prerequisites={[
    'The checkout and binaries from Create and query your first registry',
    'QGIS 4.2.1 with GDAL 3.12.4',
    'Running Docker, and two terminals',
    'OpenSSL, Python 3.11 or later, and uv',
    'A text editor',
  ]}
/>

## Before you start

[Create and query your first registry](../first-breg/) installs `breg`, `bregctl`, and `mint` and clones
the repository at the tag that matches them. Open a terminal at the root of that `breg-tutorial`
checkout and confirm the binaries answer:

```sh
bregctl --version
mint --version
```

Run every shell command from the root of that clone. Keep two terminals open: the first runs Base
Registry Engine and Registry Mint, while the second performs the refresh write. What this tutorial
adds to that setup is QGIS, which needs a desktop session.

:::caution[Use synthetic data only]
The quickstart creates local keys, tokens, and a disposable database under
`products/breg/quickstart/.run/`. A later quickstart run replaces that directory.
Do not put production data or credentials in this exercise.
:::

{/* Evidence: products/breg/quickstart/run.sh, spatial_postgres_image;
    crates/registry-breg/src/generated_ddl.rs, requires_postgis(). */}

## Start the spatial registry

In the first terminal, start the spatial quickstart:

```sh
products/breg/quickstart/run.sh --installed --spatial
```

The `--installed` flag makes the launcher use the `breg`, `bregctl`, and `mint` you installed
rather than building them from the checkout. The launcher first prints
`== Using installed breg, bregctl, and mint from PATH` with the three paths it resolved. The first
run also downloads the pinned PostGIS image. Wait for this banner; the ports, the operator token
suffix, and the absolute paths vary:

```text
Base Registry Engine spatial service-site quickstart is ready.
  Base Registry Engine: http://127.0.0.1:<breg-port>
  Registry Mint:   http://127.0.0.1:<mint-port>
  Project:         <clone>/products/breg/quickstart/.run/project
  Runtime config:  <clone>/products/breg/quickstart/.run/runtime.yaml
  QGIS OAPIF URL:  http://127.0.0.1:<breg-port>/v1/gis
  Collection:      service-site.installation-map-reader
  QGIS client id:  qgis-installation-central
  QGIS secret:     <clone>/products/breg/quickstart/.run/secrets/qgis-client-secret
  Operator token:  <clone>/products/breg/quickstart/.run/secrets/operator-token-<suffix>
  Map token:       <clone>/products/breg/quickstart/.run/secrets/map-token
  Logs:            <clone>/products/breg/quickstart/.run/logs
```

This tutorial uses the QGIS OAPIF URL, the collection, the client id, and the client secret. The
operator and map tokens are the launcher's own short-lived credentials; the import step mints a
fresh one instead of reusing them.

Leave the first terminal running. The launcher has compiled, tested, packaged, and applied the
synthetic service-site project. It has also seeded Point records and checked native JSON,
GeoJSON, and the QGIS-compatible collection route. QGIS calls this provider OAPIF, short for
Open Geospatial Consortium (OGC) API Features. Base Registry Engine exposes only the compatibility
routes named in this tutorial and publishes an empty conformance list.

{/* Evidence: products/breg/quickstart/run.sh;
    products/breg/quickstart/support/quickstart.py, spatial_smoke() and QGIS_CLIENT_ID;
    products/breg/scripts/test_quickstart.py. */}

## Set a bounded QGIS map view

Start an empty QGIS project. Select the project coordinate reference system control in the
bottom-right corner and choose **OGC:CRS84 (WGS 84 (CRS84))**.

In the QGIS Locator, enter `go 100.55,13.75` and select the result in the current project
coordinate reference system. Set the status-bar **Scale** to `1:5000`. Switch the coordinate
display to **Extents** and confirm that the longitude span is less than `0.24` degrees and the
latitude span is less than `0.19` degrees.

The `installation-map-reader` profile caps a bounding box at `0.25` longitude degrees by `0.20`
latitude degrees. The cap limits how much of the map one query may ask for. It does not decide
which records you may see: that is the profile's row boundary, which compares each record's
`zone` with the `service_zones` claim in the installation credential. The smaller view leaves
room for QGIS decimal serialization and for canvas aspect-ratio changes. A world view exceeds the
cap and Base Registry Engine refuses the query with `query.invalid`.

{/* Evidence: products/breg/acceptance/spatial-service-sites/registry.yaml;
    crates/registry-breg/src/query.rs, parse_bbox();
    crates/registry-breg/src/api/mod.rs, first_page_spatial_query();
    crates/registry-breg/tests/postgres_spatial_read.rs,
    real_postgres_spatial_bbox_reads_preserve_authority_and_geojson_audit(). */}

## Store the installation credential in QGIS

1. Open **Settings > Options > Authentication > Configurations**, then select
   **Add a new authentication configuration**. Set a QGIS master password if prompted.
2. Name the configuration `Base Registry Engine spatial quickstart`. Set **Resource URL** to the
   printed Base Registry Engine origin without `/v1/gis`.
3. Select **OAuth2 authentication**, **Grant flow: Client Credentials**, and
   **Resource access token method: Header**.
4. Set **Token URL** to `http://127.0.0.1:<mint-port>/token`, using the printed Registry Mint
   port. Set **Client ID** to `qgis-installation-central`.
5. Open the printed `qgis-client-secret` file in a local editor and copy its value into
   **Client secret**. Leave **Scope** empty and leave **Persist between launches** off, then save
   the configuration.

The client secret stays in QGIS's authentication database. A saved QGIS project refers to the
configuration by its `authcfg` identifier; the secret and bearer token do not belong in the
layer URL or project file.

:::caution[Keep the client secret local]
Do not paste the secret into shell history, a QGIS connection URL, logs, screenshots, or support
messages. The quickstart creates the secret file with owner-only permissions.
:::

{/* Evidence: products/breg/quickstart/run.sh;
    products/breg/quickstart/support/quickstart.py,
    _mint_client_secret_client() and _require_owner_only_regular();
    products/breg/scripts/test_quickstart.py. */}

## Add the protected Point layer

1. Open **Layer > Data Source Manager > WFS / OGC API - Features**, then select **New**.
2. Name the connection `Base Registry Engine spatial quickstart`. Set **URL** to the printed
   **QGIS OAPIF URL** and select the saved OAuth2 configuration under **Authentication**.
3. Keep GET and feature paging enabled. Set **Page size** to `25`, then save the connection.
4. Select the connection under **Server Connections** and select **Connect**.
5. Select `service-site.installation-map-reader`, enable
   **Only request features overlapping the view extent**, and select **Add**.

QGIS draws Point features near the map center. Press `F6` to open the attribute table. The
table includes `id`, `label`, `mapLabel`, `nativeId`, `serviceKind`, `siteCode`, and `zone`.

The installation credential carries a `service_zones: central` claim, so the collection returns
only records whose `zone` is `central`. The layer is read-only, requests at most 25 records per
page, and sends the visible extent as a bounding box. Base Registry Engine applies the same scope,
purpose, row, and field rules to the QGIS-compatible route as it applies to native reads.

The bounding box is a filter inside that row boundary, not the boundary itself. If you leave
**Only request features overlapping the view extent** off, QGIS sends no bounding box and pages
through every record the row boundary allows, 25 at a time; the server allows at most 100 records
per page on this collection.

{/* Evidence: crates/registry-breg/src/api/gis.rs, collections(), collection(), items(),
    authorize_gis_collection(), parse_items_query() and query_options();
    products/breg/acceptance/spatial-service-sites/registry.yaml;
    crates/registry-breg/tests/postgres_spatial_read.rs,
    real_postgres_spatial_bbox_reads_preserve_authority_and_geojson_audit(). */}

## Pan within the bounding box cap

Close the attribute table and use the QGIS pan tool to move a short distance east or west. Keep
the status-bar extent within the longitude and latitude spans used in the earlier map setup.

The visible Points move and reload. If QGIS reports `query.invalid`, reduce the scale value to
`1:2500`, return to `100.55,13.75`, and reload the layer with `F5`.

The `/v1/gis` surface supplies the route shape QGIS needs, including collection discovery,
absolute paging links, and GeoJSON features. Its conformance list is empty because Base Registry
Engine does not claim full Open Geospatial Consortium API Features conformance.

{/* Evidence: crates/registry-breg/src/api/gis.rs, landing(), conformance(), api(), items()
    and absolute_href(); crates/registry-breg/tests/postgres_spatial_read.rs. */}

## Import a Point through Base Registry Engine

Keep QGIS open. In the second terminal, validate and import the supplied refresh record:

```sh
spatial_run="$PWD/products/breg/quickstart/.run"
spatial_input="$PWD/products/breg/acceptance/spatial-service-sites/fixtures/qgis-refresh-service-site.jsonl"
bregctl data validate \
  --package "$spatial_run/build/package" \
  --entity service-site --profile service-site-admin --operation create \
  --input "$spatial_input"

spatial_refresh_token="$spatial_run/secrets/refresh-token-$(openssl rand -hex 8)"
mint token \
  --url "$(cat "$spatial_run/mint-origin")/token" \
  --client-id generic-quickstart \
  --key "$spatial_run/keys/operator/signing-p256-private-jwk" |
  python3 products/breg/quickstart/support/quickstart.py \
    store-token --out "$spatial_refresh_token"
bregctl data import \
  --package "$spatial_run/build/package" \
  --breg-url "$(cat "$spatial_run/breg-origin")" \
  --access-token-file "$spatial_refresh_token" \
  --entity service-site --profile service-site-admin --operation create \
  --input "$spatial_input" \
  --checkpoint "$spatial_run/qgis-refresh-checkpoint.json"
```

The two summaries look like this; the digests depend on the project:

```text
data validate succeeded
package revision: sha256:<package-revision>
schema fingerprint: sha256:<schema-fingerprint>
entity: service-site
profile: service-site-admin
operation: create
input bytes: 305
items: 1
chunks: 1
maximum items: 100
maximum bytes: 262144
```

```text
data import succeeded
package revision: sha256:<package-revision>
schema fingerprint: sha256:<schema-fingerprint>
entity: service-site
profile: service-site-admin
operation: create
input bytes: 305
items: 1
completed chunks: 1
committed items: 1
complete: true
```

Run the block as one unit: every token the spatial quickstart mints, including the operator token
this block requests, expires after 60 seconds. The checkpoint records each committed chunk, so an
interrupted import resumes without sending a completed chunk again. If you run the import a
second time with the same checkpoint, it sends nothing and reports `committed items: 0` with
`complete: true`. The operator token stays in an owner-only file and is never copied into QGIS.

{/* Evidence: crates/registry-bregctl/src/lib.rs;
    products/breg/quickstart/support/quickstart.py, store_token();
    products/breg/acceptance/spatial-service-sites/fixtures/qgis-refresh-service-site.jsonl. */}

## Refresh and confirm credential renewal

Return to QGIS, select the layer, and press `F5`. Open the attribute table with `F6`. Use
**Select features using an expression** with this expression:

```text
"siteCode" = 'SVC-QGIS-REFRESH'
```

Select the matching feature and move the selection to the top. QGIS shows one record with these
values:

```text
label        QGIS refresh example
mapLabel     SVC-QGIS-REFRESH
serviceKind  clinic
siteCode     SVC-QGIS-REFRESH
zone         central
```

Wait at least 95 seconds, then pan within the bounded extent and press `F5` again. The protected
layer reloads without asking you to paste a token. Spatial quickstart access tokens last 60
seconds; QGIS uses the stored client credential to request a replacement from Registry Mint.

{/* Evidence: products/breg/quickstart/support/quickstart.py,
    SPATIAL_TOKEN_LIFETIME_SECONDS and _mint_client_secret_client();
    products/breg/acceptance/spatial-service-sites/fixtures/qgis-refresh-service-site.jsonl. */}

## Stop the local services

Close the QGIS project. In the first terminal, press `Ctrl+C`. The launcher stops Base Registry Engine,
Registry Mint, and the disposable database container. The files under
`products/breg/quickstart/.run/` remain until the next quickstart replaces them.

If you do not plan to repeat the tutorial, remove the QGIS server connection and OAuth2
configuration so the disposable client secret does not remain in your QGIS authentication
database.

{/* Evidence: products/breg/quickstart/run.sh, cleanup(). */}

## What you built

- A local Base Registry Engine package with synthetic CRS84 Point records and PostGIS-backed bbox reads
- A protected QGIS layer using an OAuth2 client credential stored outside the project file
- A row-bounded map layer whose queries stay inside the profile's bounding box cap, with page size 25 and flat authorized attributes
- An authenticated API write that appeared after QGIS Refresh
- A protected layer reload after the short-lived access token expired

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| The first run takes several minutes on Apple Silicon | The pinned PostGIS image uses `linux/amd64`, so Docker Desktop may run it under emulation | Leave the launcher running; later runs reuse the downloaded image |
| The launcher exits before the banner and names `breg`, `bregctl`, or `mint` as required in `--installed` mode | The installer's directory is not on your `PATH`, or you skipped one of the installers in [Create and query your first registry](../first-breg/) | Add `~/.local/bin` to `PATH` in this terminal, rerun the installer you skipped, then start the launcher again |
| QGIS reports `query.invalid` while adding or panning the layer | The visible extent exceeds the profile's bounding box cap, often after QGIS adjusts the canvas aspect ratio | Return to `100.55,13.75`, set scale to `1:2500`, confirm the displayed spans are less than `0.24` and `0.19`, then press `F5` |
| QGIS asks for credentials or reports an OAuth2 error | The token URL, client ID, secret, or authentication selection does not match the current quickstart | Confirm the printed ports, use client ID `qgis-installation-central`, copy the current owner-only secret, leave Scope empty, and keep the first terminal running |
| The connection opens but no Point appears | The project starts at a world extent or outside the synthetic fixture area | Set project CRS to `OGC:CRS84`, locate `100.55,13.75`, reduce the scale, and reload |
| Refresh does not add `SVC-QGIS-REFRESH` | The import did not complete, QGIS still has an attribute filter, or the layer cache has not reloaded | Confirm `complete: true`, clear table filters, select the layer, and press `F5` |

## Next

- [Create and query your first registry](../first-breg/) covers Base Registry Engine
  records, updates, refusals, and modules without the GIS client.
- [Author a registry project](../../configure/breg/) explains how to replace the
  supplied fixture with your own entities, access profiles, and spatial grants.
- [Base Registry Engine API reference](../../reference/breg-api/#spatial-adapter) describes
  the `/v1/gis` routes, the bounding box rules, and the GeoJSON encoding.
- [Deploy a registry](../../operate/breg/) covers running Base Registry Engine
  against your own PostgreSQL with PostGIS.
- [QGIS authentication](https://docs.qgis.org/4.2/en/docs/user_manual/auth_system/auth_overview.html)
  describes the client credential store used by this tutorial.