Skip to content

Deploying

The image, its configuration, the database grant the cassette needs, and how to point a running tapes at it.

Tapes does not start cassettes. A deployment starts the process, supplies its configuration and credentials, and tells tapes where to find its OpenAPI document.

public.ecr.aws/g4e5l3z3/papercomputeco/export-cassette:<release-tag>

The image tag is the release tag verbatim, v and all — copy one from releases and use it as written, e.g. …/export-cassette:v0.2.0. Pin a release; nightly is published but is not one.

The version is not a number anyone maintains. A release stamps the tag it is publishing at link time, and the manifest version, the image reference the manifest advertises, and the OpenAPI info block all derive from it. A source build reports 0.0.0, because a source tree is not a release and a plausible-looking number there would describe one that never happened.

It listens on 9998 by default and serves /ping, /openapi, and its API under /api/export.

Variable Default Meaning
CASSETTE_NAME export Installed name. Derives the route, the API prefix, and the database role.
CASSETTE_LISTEN 0.0.0.0:9998 Listen address.
TAPES_DATABASE_URL (empty) Read-only DSN for the tapes read model.

Without TAPES_DATABASE_URL the process still starts and serves its anchors, and the export endpoints answer 501. That is deliberate: a cassette that cannot reach its data should still be discoverable, so the failure reads as “not configured” rather than “not deployed.”

The manifest declares what this cassette needs, and the deployment provides it — core reads the declaration, publishes it, and grants nothing:

[depends]
core = "v1"
views = ["sessions", "span_links", "span_turns", "spans"]

Those are the tapes_v1.* contract views. The names hold stable across tapes’ projection-generation rotations, which is why a cassette reads them rather than the physical tables underneath.

The role name is derived, not chosen: cassette_ + the installed name, so cassette_export. A production deployment grants exactly:

GRANT USAGE ON SCHEMA tapes_v1 TO "cassette_export";
GRANT SELECT ON tapes_v1.sessions, tapes_v1.spans,
tapes_v1.span_turns, tapes_v1.span_links
TO "cassette_export";

SELECT only. This cassette declares no tables and gets no schema of its own.

provision.sql in the repository is the example-deployment equivalent, and it takes a shortcut worth knowing about: Postgres runs init scripts once, before tapes has migrated, so the tapes_v1 views do not exist yet and cannot be granted by name. It uses ALTER DEFAULT PRIVILEGES instead, which covers whatever tapes creates later. That is wider than the grant above, and the width is the price of a single-pass init script.

Upgrading an existing volume. A volume provisioned before the tapes_v1 views existed carries public-schema-only privileges, and the init script will not run again to widen them — reads fail with permission denied. Apply the explicit grant above once, after tapes has migrated. It is idempotent. Do not reach for docker compose down -v: that deletes the database, raw turns and read model included.

Tapes needs the exact URL of the metadata-bearing OpenAPI document:

.tapes/config.toml
cassettes = ["http://127.0.0.1:9998/openapi"]

or without editing the config:

Terminal window
tapes serve --cassettes=http://127.0.0.1:9998/openapi

Confirm it resolved:

Terminal window
curl localhost:8081/v1/cassettes
curl -OJ localhost:8081/v1/cassettes/export/sessions