Reference
Your registry lists your packages in the datumhue.app store under your namespace and, while it runs, delivers them to the people who install them. It takes new versions from the people your publish rules allow, and keeps a record of who bought each paid package, so that it delivers the package to its buyers alone.
A version is published two ways: from a folder of packages when the
registry starts (--seed-from), or with pkg push from the Platform
workstation. Both publish a sealed #true app as compiled code, never
its Lua source. A published (@scope, name, version) never changes: to
publish new content, raise the version. pkg yank withdraws a version
from the store and keeps its content and history.
Your registry answers for one namespace: the @scope you claimed when
you subscribed, which namespace.capability admits it for.
The first page walks through the first start and
everyday publishing; this page is the reference behind it. Examples use
the Linux and macOS spelling: on Windows the two programs carry .exe.
The launcher
The platform accepts a registry of its current release only, and refuses an older one when it connects. The launcher keeps yours current. On every start it:
- Reads where releases are published from
release-feed.txtbeside it, or fromDATUMHUE_RELEASE_FEED_URLwhen that variable is set. Set to an empty value, the variable turns updates off; then install each release yourself, since the platform refuses an older registry. - Installs a newer launcher and a newer registry when the feed has
them, each only once its signature and download check out, and
keeps the previous programs beside them as
datumhue-launcher.bakanddatumhue-registry.bak. - Starts the
datumhue-registrybeside it, the programrelease-feed.txtnames, with every argument you gave it.DATUMHUE_CLIENT_PATHnames a registry installed elsewhere.
When an update fails, because the machine is offline or a download does not check out, the launcher prints why and starts the registry you have.
The platform moves to each new release at once and stops admitting the release before it. A running registry then stops and hands over to its launcher, which waits up to five minutes for the new release on the feed, installs it, and starts the registry again with the same arguments. A service manager or a container sees no restart, so this works however the registry runs. A registry started without its launcher, or with updates turned off, stops instead; see Troubleshooting.
Keep the registry's folder writable by the user that runs it: the launcher installs releases there.
Subcommands
Every argument after datumhue-launcher reaches the registry unchanged:
datumhue-launcher keygen [-o <path>] [--plaintext] [--env] [--force]
datumhue-launcher show-pubkey [-f <path>] [--env]
datumhue-launcher serve [-c <config>] [--seed-from <dir>]
[-a <aggregator-url>]
Run keygen and show-pubkey on ./datumhue-registry directly, as
Get started does: they need no update. Run serve through the launcher
so every start is current. The flag defaults name the delivered files,
so from the registry's folder no path flag is needed.
keygen
Generate a new secret key (the registry's long-lived identity) and store it on disk.
datumhue-registry keygen [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--output <OUTPUT>, -o | identity.key | Path to write the secret key file, the name the delivered configuration expects |
--plaintext | off | Store the key unencrypted. Not recommended for production |
--env | off | Read the encryption passphrase from DATUMHUE_KEYSTORE_PASSPHRASE instead of prompting. Fails if the variable is unset or empty |
--force | off | Replace an existing key file at the output path. Without it, keygen refuses to overwrite: the key on disk is the identity the platform admitted, and replacing it means a key rotation |
With neither --plaintext nor --env, keygen asks for a passphrase
and encrypts the key with it; an empty passphrase writes it plaintext.
A lost passphrase cannot be recovered: replace the
key instead. keygen prints the
public key, which you paste into the onboarding page, or into the
rotation page for a replacement key. While the admitted key is in
place, make the replacement with --force, or with -o and a new path.
show-pubkey
Print the public key of a key file: to paste it into the rotation page again, or to compare it with the key the platform admitted.
datumhue-registry show-pubkey [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--file <FILE>, -f | identity.key | Path to the secret key file |
--env | off | Read the decryption passphrase from DATUMHUE_KEYSTORE_PASSPHRASE instead of prompting. Plaintext keys ignore it |
serve
Start the registry.
datumhue-registry serve [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--config <CONFIG>, -c | provider.kdl | Path to the configuration file |
--seed-from <SEED_FROM> | none | Load every package in this folder into the catalog before serving. Without it, the catalog is the one the last run kept |
--aggregator-url <AGGREGATOR_URL>, -a | none | Override the aggregator-url from the configuration file |
serve loads the key secret-key-file names. When that file is
missing, serve makes a new key and starts with it. The platform has
not admitted that key, so it refuses the registry's announcements, and
the capability file does not match it. The registry logs the key it
uses as Package Provider Node ID: …: when it is refused, compare
that key with the public key you onboarded first.
An encrypted key needs DATUMHUE_KEYSTORE_PASSPHRASE set when serve
starts, and a plaintext key needs it unset; otherwise the start fails,
and the message says which the key is.
serve then waits for the namespace's Write grant, logging
Waiting for the Write capability over '@studio'... until the
capability file holds one. With --seed-from it loads the folder's
packages; a version already published with other content is skipped
with a warning ("already published with different content — keeping
the published artifact and skipping the seed copy"): raise the version
in package.kdl to publish new content. Catalog holds <n> package(s)
reports the result.
The registry then announces your namespace to the platform
(Announcer started (TTL: <n>s), then Announced namespace '@studio' (TTL: <n>s) at every renewal) and takes publishes, yanks and prices.
When the capability file also holds the issuing grant, it sells paid
packages (Paid-package entitlement handlers ready …); without it, it
logs Paid-package entitlements disabled (no Read issuing capability)
and serves free packages only. Ctrl+C, or the SIGTERM a service manager
sends, stops it cleanly.
Configuration
The settings file is KDL with one provider block. A key the block does
not know stops the start. A relative path resolves against the folder
that holds the file.
The provider.kdl you received on activation:
provider {
secret-key-file "./identity.key"
capability-file "./namespace.capability"
namespace "@studio"
aggregator-url "https://<platform host>"
relay-url "https://<platform host>"
announce-ttl 300
blob-store-path "./blobs"
catalog-store-path "./catalog.redb"
publish-rules {
// Fill in who may push to your namespace: `login` prints your
// sign-in's `sub` and issuer URL. For example:
// issuer "https://accounts.google.com" {
// allow claim="sub" equals="<your sub>" rights="rw"
// }
}
}
| Key | Required | Default | Description |
|---|---|---|---|
secret-key-file | yes | none | The registry's key. Made at the start when missing, as a key the platform has not admitted; see serve above |
capability-file | yes | none | The capability file the platform issued for your key. The registry rewrites it as grants renew, so keep it writable: the registry refuses to start otherwise |
namespace | yes | none | The namespace this registry announces: your @scope, exactly as the platform delivered it |
aggregator-url | yes | none | HTTPS URL of the platform, as delivered |
relay-url | yes | none | The URL the registry reaches the platform through: the same URL, as delivered. The start stops if the platform does not offer it |
discovery-url | no | the platform's | Where the registry publishes its address and looks up others |
announce-ttl | no | 300 | How long each announcement stays valid: seconds, or a duration such as "5m", at least 2 seconds. The registry announces again after half of it |
webrtc-bind-addr | no | none | A fixed UDP address for direct browser connections, such as "0.0.0.0:7844". Set it with webrtc-public-addr when the registry runs in a container or behind port forwarding; without it, the registry finds its addresses itself |
webrtc-public-addr | no | none | The host:port browsers reach webrtc-bind-addr at. Every address it resolves to must route to that socket |
blob-store-path | no | ./provider_blobs | The folder of the package files the registry serves (the delivered settings name ./blobs). The registry must be able to write it, or it stops at start |
catalog-store-path | no | ./catalog.redb | The catalog: every published version |
entitlement-store-path | no | ./entitlements.redb | Who owns each paid package |
publish-rules | no | publishing off | The rules for who may publish, yank and price packages |
Selling paid packages needs no setting beyond the capability file; the registry takes purchase records from the datumhue.app store alone.
The capability file
namespace.capability carries three grants for your namespace, each
issued for your registry's public key:
- Announce — admits the registry to the platform and lets it register your namespace.
- Write — lets the registry add to your catalog. Workstations
accept a catalog entry only from a registry that holds this grant, so
no one else can publish under your namespace.
servewaits at start until it is there. - Read (the issuing grant) — lets the registry give buyers proof that they own a paid package. Without it, paid packages cannot be sold.
The running registry renews the grants itself, rewriting the file and
logging renewed capability for namespace '@studio', so the file needs
no backup. A registry that stays stopped until its grants lapse needs
fresh files: the rotation page linked from your connection email sends
them by email along with a key rotation, or the platform's operator can
issue them again for your current key.
The grants belong to the key they were issued for. After a rotation,
replace the old file with the newly delivered namespace.capability
beside the new identity.key, keep your provider.kdl, which holds
your publish rules, and start the registry again.
publish-rules block
The rules in publish-rules decide, from the publisher's sign-in, who
may publish, yank and set prices. With no rules, or an empty block,
publishing is off: the registry still serves your catalog, and
--seed-from still loads packages at start. The registry reads the
rules when it starts, so restart it after changing them.
provider {
// ... the other keys ...
publish-rules {
issuer "https://accounts.google.com" {
// You, by the `sub` that `login` prints.
allow claim="sub" equals="110248495921238986420" rights="rw"
// A collaborator, limited to one scope.
allow scope="@studio" claim="email" \
equals="artist@example.com" rights="rw"
}
}
}
The platform signs people in with Google, so the rules sit in the block
of Google's issuer URL, https://accounts.google.com. login prints
that URL beside your sub, the identifier Google keeps for your
account. Bind rules to sub, or to email, the verified address the
publisher signed in with. An address Google has not verified matches no
rule.
Each allow line gates publishing into a scope. Packages carry a
@<scope>/<name> identity; a rule binds to one scope, every scope, or
the scopes a token claim lists.
issuer "<issuer URL>" {
require claim="<claim-name>" <match-op>=<value>
allow [scope="@<scope>"] claim="<claim-name>" \
<match-op>=<value> rights="rw"
allow [scope="@<scope>"] signed-in rights="rw"
allow scope-in-claim="<claim>" rights="rw"
}
Every require of the block must hold before any of its rules counts.
allow signed-in admits everyone the issuer signs in: on Google, that
is every Google account, so bind it to a scope with care.
Scope binding — choose at most one form:
- No scope key — the rule authorizes publishing under every
@scope. Fine for yourself; for collaborators, bind the scope. scope="@<scope>"— binds the rule to one scope. A publisher this rule authorizes may publish only under that@scope.scope-in-claim="<claim>"— an identity owns the scopes its named token claim lists. The requested scope, without its@, must equal (string claim) or appear in (array claim) the named claim. This form takes noclaimand no match operator.
Claim matching (for require and the claim= rules) — exactly one
match operator:
equals="<value>"— the claim string equals the value, or the claim is an array containing the value as a string elementequals=#trueorequals=#false— the claim is that boolean; a string"true"never matches itarray_contains="<value>"— the claim is an array containing the value
Rules add up: any rule that covers the requested scope and matches the
claims allows the change. rights is always rw here: a publish, a
yank and a price change all write, so the registry refuses to start on
a rule that grants r, and quotes the rule. It also refuses
allow anyone, since publishing always takes a sign-in, and a rule
outside an issuer block.
A refused publisher learns the name of the claim or scope that failed,
or iss when no block names its sign-in service, never the value the
rule expects. The registry log gives the same reason.
Publish from the workstation
Publish, withdraw and price your packages from the Platform workstation,
through its launcher, in the workstation's folder (on macOS the launcher
is DatumHue.). pkg push,
pkg update, pkg outdated, pkg show, pkg yank and pkg price
address the registry of the package's @scope, which on the platform is
the namespace you subscribed. --namespace (or
DATUMHUE_BOOTSTRAP_NAMESPACE) names another one, for a package
published under a scope other than the namespace that serves it. A
dependency in a package's requires resolves in its own @scope's
namespace too, so another creator's package needs no namespace=.
# Once; opens the browser (--no-browser prints the address instead).
./datumhue-launcher login
./datumhue-launcher pkg push <package-folder>
./datumhue-launcher pkg yank @studio/name 1.2.0 --reason "broken saves"
./datumhue-launcher pkg yank @studio/name 1.2.0 --undo
# --currency defaults to usd.
./datumhue-launcher pkg price @studio/name 4990
# Shows the listing; needs no sign-in.
./datumhue-launcher pkg price @studio/name
A yanked version leaves the store's listings and no longer matches a
version range, but stays downloadable: a package that names that exact
version, or already uses it, keeps it. --undo lists it again. A price
belongs to the store: change it at any time without a new version. The
amount is in the currency's smallest unit.
pkg push exits with code 2 when the platform or your registry rejects
the sign-in, 3 when publishing is off or the sign-in fails its issuer's
block (no block names its sign-in service, or a require does not
hold), 10 when no allow rule of that block lets the person signed in
publish into the package's @scope, 5 when the platform could not be
reached or the sign-in could not be checked (try again shortly), and 11
when the version is already published with other content. pkg yank
exits with the same codes for the same refusals, and pkg price exits
3 where pkg push exits 3 or 10.
Network surface
| Direction | Purpose |
|---|---|
| inbound from workstations | Your catalog, and package downloads; a paid package only to its buyer's registered device |
| inbound from buyers' devices | Proofs of purchase, and their renewals |
| inbound from publishers | pkg push, pkg yank and pkg price |
| inbound from the platform | Each sale and refund; buyers' devices as they are registered and removed |
| outbound to the platform | Announcing the namespace, every half announce-ttl; capability renewal; finding the store and your namespace's other registries |
| outbound to the release feed | The launcher's update check, at every start |
The registry opens no TCP port: every connection reaches it through the platform. A workstation and your registry connect directly over UDP where they can reach each other, and through the platform's relay where they cannot, so a registry on a home connection needs no port forwarding. It takes 4096 connections at once; past that a connection is turned away as busy.
Publishers' workstations present a pass the platform signs, valid for at most five minutes. Keep the computer's clock set automatically: the registry refuses a pass dated more than a minute ahead of its clock.
Persistent state
| Path | Owner | Purpose |
|---|---|---|
secret-key-file (identity.key) | you | The registry's identity. A new key needs a key rotation on the rotation page |
capability-file (namespace.capability) | the platform | Admits this registry and lets it write its catalog. Renews itself while the registry runs; issued again at a key rotation, and when it lapses while the registry is stopped |
blob-store-path | this registry | The package files it serves. --seed-from rebuilds those still in its folder |
catalog-store-path | this registry | The catalog: every published version and its yank history |
entitlement-store-path | this registry | Who owns each paid package, and their registered devices |
| the registry's folder | the launcher | Installed releases, and the previous ones as .bak |
Keep every one on persistent storage:
- Lost package files leave catalog entries that workstations fail
to download.
--seed-frombrings back only the packages still in its folder, so a version published withpkg pushwhose package is no longer there cannot be downloaded again. - A lost key is replaced by a new one at the next start, which the
platform has not admitted: it refuses the registry's announcements
until you rotate to that key on the rotation page and place the new
namespace.capabilitybeside it. - Lost ownership records stop buyers downloading and renewing what they bought until the platform sends your registry the sales again: the last 30 days' within an hour, older ones when you ask its operator.
Run it as a service
Linux
/, with the zip
unpacked to /opt/datumhue-registry and the delivered files beside the
programs:
[Unit]
Description=DatumHue creator registry
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=datumhue
Group=datumhue
WorkingDirectory=/opt/datumhue-registry
ExecStart=/opt/datumhue-registry/datumhue-launcher serve --config provider.kdl
Environment=DATUMHUE_KEYSTORE_PASSPHRASE=
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/datumhue-registry
[Install]
WantedBy=multi-user.target
ReadWritePaths must cover the registry's folder: the launcher installs
updates there, and the registry rewrites namespace.capability and its
stores there. If the key is encrypted, DATUMHUE_KEYSTORE_PASSPHRASE
must hold its passphrase or the start fails: fill in the line above, or
load the passphrase from a credentials file with LoadCredential= or
EnvironmentFile=, so that the unit itself does not show it. Drop the
line for a plaintext key. identity.key must belong to the service
user, with mode 0600.
Docker
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -r -s /usr/sbin/nologin registry \
&& mkdir -p /registry /data && chown registry:registry /registry /data
COPY --chown=registry:registry \
datumhue-launcher datumhue-registry release-feed.txt /registry/
RUN chmod 755 /registry/datumhue-launcher /registry/datumhue-registry
USER registry
WORKDIR /data
ENTRYPOINT ["/registry/datumhue-launcher"]
CMD ["serve", "--config", "/data/provider.kdl"]
The container expects provider.kdl, identity.key,
namespace.capability and the stores on a persistent /data volume.
Updates the launcher installs stay inside the container: a new
container starts from the release in the image and updates again at
its first start. For an encrypted key, pass the passphrase in when the
container starts (-e DATUMHUE_KEYSTORE_PASSPHRASE=… or a secret);
without it the start fails. When the container's own addresses cannot
be reached from outside, set webrtc-bind-addr and
webrtc-public-addr and publish that UDP port, so browsers connect to
it directly. Without them the registry still serves through the
platform's relay.
Windows and macOS
Run the same command from a terminal opened in the registry's folder — on Windows:
.\datumhue-launcher.exe serve --config provider.kdl
To keep the registry running across logins, wrap that command in the scheduled task or service mechanism of your choice, with the working directory set to the registry's folder. The key-file permission check applies on Linux and macOS only; on Windows, keep the registry's folder private to your account.
Security notes
- Publish rules are the boundary. Anyone a rule allows can publish
new versions under the scopes it covers, and yank versions and change
prices there. Bind collaborators' rules to a named scope
(
scope="@studio") rather than to every scope. Published versions never change, so a stolen sign-in cannot alter one, but it can publish new versions and yank existing ones. - Your registry never sees a sign-in token. A publisher's pass works only on your registry, only from the workstation that asked for it, and for five minutes at most.
- Secret-key encryption. The interactive
keygenflow encrypts the key at rest. A key generated on firstserveis plaintext unlessDATUMHUE_KEYSTORE_PASSPHRASEis set; set it for any long-running deployment, or place the key on an encrypted volume. - Private key permissions. On Linux and macOS the registry refuses
to start if
secret-key-fileis more permissive than owner-only (0600).keygenand first-run generation write the correct mode; if you copy the file, match it (chmod 600). - The key is the credential.
identity.keyis the one thing the platform trusts; the capability file works only with that key. The platform never asks for the key file. If it is lost, or you suspect someone else has it, replace it on the rotation page: the platform revokes the old key as it admits the new one, so the old key can no longer announce your namespace or renew its grants. - Your registry never sees a buyer's sign-in. It knows each buyer by a reference the platform makes for your namespace alone, and each of their devices by a key the device holds for your namespace alone.
- Releases are verified. The launcher installs only releases signed
by DatumHue, whose download matches what was signed, so whoever serves
the feed named in
release-feed.txtcannot hand it another program.
Troubleshooting
Waiting for the Write capability … never ends. The capability
file holds no Write grant for the namespace provider.kdl names.
Confirm capability-file points at the delivered namespace.capability,
and that namespace in provider.kdl is exactly the namespace in that
file.
The platform refuses the registry's announcements. Check that the
startup line Package Provider Node ID: … shows the public key you
onboarded. A
different key means identity.key was missing at a start and the
registry made a new one: restore the key from your backup, or rotate to
the new key on the rotation page and place the emailed
namespace.capability beside it, keeping your provider.kdl. A file
that lapsed while the registry was stopped is issued again the same way.
The registry does not start on an encrypted key. The start fails with "the key at … is encrypted but DATUMHUE_KEYSTORE_PASSPHRASE is not set.": set the passphrase in that variable. The opposite message ("… is set but the key at … is stored plaintext.") means the variable is set for a plaintext key: unset it.
… has insecure permissions … The key file is readable by others.
chmod 600 identity.key, and make sure the service user owns it.
The registry stops with "network version mismatch …". The
platform runs another release. Run serve through
datumhue-launcher with updates on: it installs the platform's
release and starts the registry again. Started on its own, the
registry stops with a message that names the launcher.
the aggregator does not advertise relay … relay-url was edited.
Restore the value from the delivered provider.kdl; it is the same URL
as aggregator-url.
Workstations cannot find packages. Check that the registry
announced: the log shows Announcer started (TTL: <n>s) once at start,
then Announced namespace '@studio' (TTL: <n>s) at every renewal. Check
that the catalog holds packages: Catalog holds <n> package(s) at start
gives the count. With none, run serve --seed-from <folder> once, or
check that your publishes arrived.
Publishes denied (pkg push exits 3 or 10). The registry's access
log, its lines on the log target datumhue_audit, has an outcome=deny
line for the refused publish, with the scope as resource and the claim
that failed as claim, or - when every rule is bound to another
@scope. On exit 10, check the rule's equals value against the sub
or address the publisher signed in with, and a scoped rule's scope
against the package's @scope. On exit 3, check that publish-rules is
not empty, that its block names https://accounts.google.com, and that
the block's require lines hold. Restart the registry after changing
the rules.
Buyers cannot buy or download a paid package. If the log at start
says Paid-package entitlements disabled (no Read issuing capability),
the capability file lacks its third grant: use the whole file the
platform delivered. If a buyer who paid cannot download, your registry
has not received the sale yet: the platform sends a sale of the last
30 days again within the hour, and its operator can send any sale at
once.
The catalog is empty or smaller after a restart. The stores must
be on persistent storage, and provider.kdl must still name the same
files. A container without a volume loses them at every restart, and
--seed-from brings back only the packages in its folder. Move the
stores onto persistent storage.
The platform refuses the registry after a long stop. The installed
release is older than the platform's, and the launcher could not
update it. Check that the launcher reaches the feed (it prints why it
skipped an update), that DATUMHUE_RELEASE_FEED_URL is not set to an
empty value, and that the registry's folder is writable. If the
launcher itself is too old to read the feed, download the current
registry zip the way you got this one, and copy your identity.key,
namespace.capability, provider.kdl and stores beside the new
programs.
The launcher finds no registry to start. datumhue-registry is
missing from the registry's folder, or DATUMHUE_CLIENT_PATH names a
wrong path: restore the program from the zip, or unset the variable.
When the launcher looks for the workstation instead
(workstation not found at …), release-feed.txt, which names the
program to start, is missing or empty: restore it from the zip.