Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.43.0.
Take the project you authored to a running deployment, then change, retain, and upgrade it. Every
step runs schedulingctl or scheduling against one runtime file.
What you provide
Section titled “What you provide”- PostgreSQL 17 or newer, reachable over TLS. Registry Scheduling requires TLS on every database connection and refuses one it cannot protect.
- An OpenID Connect issuer whose access tokens carry the read scope and, for bookings, the task grant another system issues.
- A directory of secret files owned by the runtime user, each with mode 0400 or 0600 and written without a trailing newline.
- A TLS edge in front of the listener. Scheduling serves plain HTTP on a private address and refuses a public one.
Install the runtime
Section titled “Install the runtime”The installer puts scheduling and schedulingctl in ~/.local/bin. The runtime is published
for Linux amd64 only; on Linux arm64 and macOS arm64 the installer installs schedulingctl alone.
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/scheduling-install.sh | bashschedulingctl --versionThat installer is published with the
latest release and installs that
release; to install another one, fetch the installer from that release’s assets, because a published
installer refuses a different SCHEDULING_VERSION. It checks every downloaded asset against the
release’s SHA256SUMS but does not prove where the release came from. For that, follow the release
verification guide for the tag, then rerun the installer with SCHEDULING_ASSET_DIR set to the
verified directory.
The runtime is also published as the image ghcr.io/registrystack/scheduling:<tag>. Deploy it by
the digest the release manifest names, not by the tag. The image runs /usr/local/bin/scheduling
as UID 65532, reads /etc/registry-scheduling/runtime.yaml, and carries the matching
schedulingctl. To run an operator command with the image’s exact bytes, override the entrypoint
and mount the runtime file, the package, and the secrets read-only and the audit directory
writable, each at the location your runtime.yaml names:
docker run --rm \ --entrypoint /usr/local/bin/schedulingctl \ -v /etc/registry-scheduling/runtime.yaml:/etc/registry-scheduling/runtime.yaml:ro \ -v /etc/registry-scheduling/package:/etc/registry-scheduling/package:ro \ -v /run/secrets/registry-scheduling:/run/secrets/registry-scheduling:ro \ -v /var/lib/registry-scheduling:/var/lib/registry-scheduling:rw \ "ghcr.io/registrystack/scheduling@sha256:<verified-digest>" \ apply --runtime-config /etc/registry-scheduling/runtime.yaml --operator-reference CHG-1234apply needs the migration credential and writes its own audit file beside audit.path, and
records apply connects with the same credential. Mount the migration credential only for those
two commands, and keep it out of the serving container.
Provision PostgreSQL
Section titled “Provision PostgreSQL”Create the roles and the database, then hand the schema to the migration role:
CREATE ROLE scheduling_migration LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;CREATE ROLE scheduling_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;CREATE DATABASE scheduling;Set each role’s password with \password scheduling_migration and \password scheduling_runtime
in psql, which prompts for it instead of taking it in a statement.
In the scheduling database:
REVOKE ALL ON DATABASE scheduling FROM PUBLIC;GRANT CONNECT ON DATABASE scheduling TO scheduling_migration, scheduling_runtime;ALTER SCHEMA public OWNER TO scheduling_migration;REVOKE ALL ON SCHEMA public FROM PUBLIC;Grant the runtime role nothing yourself. apply grants it exactly what serving needs: usage on
the schema, read and write on the Scheduling tables, usage and select on the outbox identity
sequence, and read on the activation ledgers. It reissues the grants on every run, so a table a
later migration adds is covered. The runtime requires TLS on its database connection and refuses
one it cannot verify; database.trustedRootCertificateRef names the CA bundle when a private CA
fronts PostgreSQL.
Write the runtime file
Section titled “Write the runtime file”schedulingctl init wrote runtime.example.yaml beside your policy. Copy it to the host as
runtime.yaml and edit it:
package.root:/etc/registry-scheduling/package, the directory you pass toschedulingctl package --output. Every path in the file must be absolute and must not pass through a symbolic link. So must the--runtime-configpath; a relative one is refused withplatform.runtime-config.path.identity.databaseId: a name for the one database this deployment owns. The firstapplyrecords it, and every later command refuses a database that names another.listener:bind, plustlsTermination: operator-controlled-upstreambehind your TLS edge.development-loopbackis refused on any address but loopback.secretProviders.file.rootand the*Refkeys: secret references such assecret:file/runtime-database-url, resolved under that root. Write each file withprintf '%s': a trailing newline is part of the value, and a URL file that ends in one makesserveexit 3 withFATAL: database "scheduling\n" does not exist.database.runtimeUrlRefanddatabase.migrationUrlRef: two PostgreSQL URLs. Give them different roles. With one role for both, the serving credential could rewrite the activation record, andapply,status, andservesay so.authentication.oidc: yourissuer,audience, and the exactallowedClientsthat may call.taskGrantStatus: either a list of the task authorities Scheduling asks about every grant before it books, orunchecked-final-fifteen-minutes, which asks nothing and accepts only a grant with 15 minutes or less left.audit:hashKeyRef, a key you generate withopenssl rand -hex 32, plus either a filepathordestination: stdout.destinations: where reminders and the hook events your policy names are delivered.
Scheduling configuration lists every key and default.
Check the file offline against your project. Nothing is read from the database or the network:
schedulingctl check ./scheduling --runtime-config /etc/registry-scheduling/runtime.yamlAuthoring check passed.databaseAccess: falsenetworkAccess: falsePackage the policy
Section titled “Package the policy”schedulingctl package ./scheduling --output /etc/registry-scheduling/packagePackage written.packageDigest: sha256:<digest>runtimeConfigurationIncluded: falsesecretsIncluded: false--output refuses a directory that already exists. The package holds scheduling.yaml and a
SHA256SUMS file. Set package.expectedDigest to the digest if the runtime must refuse any other
package.
Plan and apply
Section titled “Plan and apply”plan connects with the runtime credential and writes nothing:
schedulingctl plan --runtime-config /etc/registry-scheduling/runtime.yamlActivation planned; nothing was written.changesPending: truedatabaseIdCheck: initialpendingSchemaVersions: [100]policy: {"activeDigest":null,"candidateDigest":"sha256:<digest>","revision":2,"revisionAdvances":true}refusals: []apply connects with the migration credential and, in one transaction, creates the schema,
publishes the policy, grants the runtime role its access, and records the activation. The operator
reference is kept only as a keyed hash; --backup REF records the backup you took first.
schedulingctl apply --runtime-config /etc/registry-scheduling/runtime.yaml \ --operator-reference CHG-1234Package activated.applyOrder: 1effects: {"policyDigest":"sha256:<digest>","policyRevision":2,"policyRevisionAdvanced":true,"schedulingIdAdopted":true}schemaVersionsApplied: [100]Callers send the published policyRevision with each booking. Applying the active package again is
refused with schedulingctl.activation.package-already-active.
Apply the environment records
Section titled “Apply the environment records”The supply lives in the database, not in the package. Apply records.yaml:
schedulingctl records apply --runtime-config /etc/registry-scheduling/runtime.yaml \ /etc/registry-scheduling/records.yamlEnvironment records applied.applied: {"exceptions":1,"locations":2,"members":3,"pools":2,"windows":0}records apply replaces every record the database holds with the document you pass. A location,
pool, member, window, or closure the document leaves out is gone. It refuses only to move, remove,
or shrink a window below its live holds and appointments, and names the window and the deficit.
scheduling --runtime-config /etc/registry-scheduling/runtime.yaml serveStartup verifies the package against the activation record. It refuses a schema with pending
versions, a package that was not applied, and a database that names another databaseId. It logs
verified Scheduling package with the digest but not the address it listens on. Ask the readiness
route instead:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8105/readyz200/readyz answers unavailable while PostgreSQL is unreachable, while the schema is not the one
this release expects, and while the audit destination refuses writes, because no booking proceeds
without its audit entry.
Read the deployment
Section titled “Read the deployment”status names the active package and every activation:
schedulingctl status --runtime-config /etc/registry-scheduling/runtime.yamlActivation status read.pendingSchemaVersions: []schemaVersion: 100verifiedPackageDigest: sha256:<digest>intents lists the reminders and confirmations nobody is delivering: every intent, when no
reminder destination is configured, and any intent a destination refused until its attempts ran
out.
schedulingctl intents --runtime-config /etc/registry-scheduling/runtime.yamlUndelivered delivery intents listed.intents: [{"appointmentRevision":1,"attempts":1,"deliveryState":"local","purpose":"confirmation",...}]Change the policy
Section titled “Change the policy”Edit the project and run schedulingctl test. Package into a new directory, set package.root
to it, then plan and apply. The apply publishes the next policy revision, and a caller still
holding the old one is refused with policy.changed and reads the service again.
An offering with live holds or appointments must keep its service, location, mode, and pool or
window. A change to any of them is refused with schedulingctl.activation.policy-in-use; let the
bookings end or move them first. Any earlier package can be applied again as a new activation.
Retention
Section titled “Retention”The runtime runs its own retention sweep. Three keys under retention each default to seven
days, a placeholder rather than a recommendation. Each is the longest the data is kept; the sweep
erases it once the period has passed:
receiptRetentionDays: how long an idempotency receipt lasts. A retry after that is refused withidempotency.expired.claimOwnerRetentionDays: how long after an appointment ends, or a hold lapses, the caller’s issuer and subject stay on it.hookPayloadRetentionDays: how long a hook event’s payload is kept, for retries while it is undelivered and for inspection, through the database, once it is dead-lettered.
audit.retentionDays (default 90) removes rotated audit files.
Once claimOwnerRetentionDays passes, the appointment no longer belongs to anyone: its former
owner can no longer read, list, or recover it, and no receipt for it can be replayed. Set the key
to the period your callers need to look a booking up again.
Upgrade
Section titled “Upgrade”Before 1.0, a release reads only the state its immediate predecessor wrote, so upgrade one release
at a time: install the next release, run plan, which lists the pending schema versions, take a
backup, apply with --backup, then restart serve. A runtime started against a schema older
than its own refuses to start until you apply.
A schema migration cannot be undone by applying an earlier package. Restore the backup you named in
--backup instead. A release that starts a fresh baseline says so in its notes and must be applied
to a new, empty database; the old ledger is not carried over.
- How Registry Scheduling works explains holds, task grants, and recovery from the caller’s side.
- Registry Scheduling API lists the routes and problem codes.