Operating gim-hub
此内容尚不支持你的语言。
Configuration
Section titled “Configuration”gim-hub reads its configuration from environment variables and, for every setting except the bootstrap ones, from values stored in its database (see Stored settings). Empty variables count as unset. An invalid environment variable stops the hub with an error naming the variable.
The Tier column says where a setting may come from: env-only (bootstrap:
the environment only), stored live (environment or stored value, a stored
change applies within about a second) or stored restart (environment or
stored value, a stored change applies at the next start). An environment-only
deployment behaves exactly as before 0.4.0.
| Variable | Tier | Default | Meaning |
|---|---|---|---|
GIM_HUB_LISTEN |
env-only | 0.0.0.0:8080 |
Socket address of the client API |
GIM_HUB_DATA_DIR |
env-only | ./data |
Data directory (see below) |
GIM_HUB_DATABASE_PATH |
env-only | $GIM_HUB_DATA_DIR/hub.db |
SQLite database file |
GIM_HUB_EVENT_RETENTION_DAYS |
stored live | 7 |
Events older than this are pruned (minimum 1) |
GIM_HUB_KEYPACKAGE_LOW_THRESHOLD |
stored live | 10 |
A device is sent keypackages.low when its one-time KeyPackages drop below this |
GIM_HUB_MAX_MESSAGE_BYTES |
stored live | 262144 |
Largest accepted MLS message (minimum 1) |
GIM_HUB_MAX_BLOB_BYTES |
stored live | 104857600 |
Largest accepted blob (minimum 1) |
GIM_HUB_LONGPOLL_MAX_WAIT_SECONDS |
stored live | 30 |
Upper bound for long-poll wait |
GIM_HUB_STATUS_PAGE |
stored live | true |
Serve an HTML status page (version, build revision, platform, hub ID, public URL) at / and /status; false answers 404 there |
GIM_HUB_SESSION_TTL_DAYS |
stored live | 30 |
Lifetime of device bearer tokens (minimum 1) |
GIM_HUB_PUBLIC_READ_PER_IP_PER_MIN |
stored live | 120 |
Unauthenticated GET /v2/users/{user_id}, .../device-list and .../hosting-record requests per client IP (IPv6: per /64) per minute (minimum 1) |
GIM_HUB_PUBLIC_READ_PER_USER_PER_MIN |
stored live | 60 |
Unauthenticated reads of one user_id per minute, across all clients (minimum 1) |
GIM_HUB_RECOVERY_READ_PER_IP_PER_MIN |
stored live | 20 |
Unauthenticated GET /v2/users/{user_id}/recovery-backup requests per client IP (IPv6: per /64) per minute, separate from the public reads (minimum 1) |
GIM_HUB_RECOVERY_READ_PER_USER_PER_MIN |
stored live | 10 |
Recovery backup reads of one user_id per minute, across all clients (minimum 1) |
GIM_HUB_TRUST_FORWARDED_FOR |
stored live | false |
true keys the per-IP limit by the left-most X-Forwarded-For address; set only behind a reverse proxy that overwrites the header |
GIM_HUB_USER_POLICY |
stored live | open |
Who may use this hub: open (everyone), allow (only listed users) or deny (everyone except listed users); see User policy |
GIM_HUB_PUBLIC_URL |
stored restart | unset | Canonical client-API base URL (scheme://host[:port]), required when GIM_HUB_FEDERATION=true. Normalized at startup; see below |
RUST_LOG |
env-only | info |
Log filter (tracing_subscriber EnvFilter syntax, e.g. gim_hub_api=debug,info) |
GIM_HUB_PUBLIC_URL is the URL clients reach this hub’s client API at, as listed for it by
clients and other hubs. It is signed into GET /v2/hub?nonce= proofs and into sign-in
challenges (AuthChallenge v2), so it must be exactly the URL clients use. It is normalized
before use: the scheme must be http or https (lowercased), the host (DNS name, IPv4 or
bracketed IPv6) is lowercased, a port equal to the scheme default (80, 443) is dropped and a
single trailing / is removed. Userinfo, any other path, a query, a fragment or a result
longer than 65535 bytes is a startup error, as is an unset URL with GIM_HUB_FEDERATION=true.
gim-core applies the same rules; the shared cases are in testdata/identity/public-url.json. Example:
HTTPS://Hub.Example.EU:443/ becomes https://hub.example.eu. Without federation it is
optional; a hub without it issues version 1 challenges and answers GET /v2/hub?nonce= with 400.
User policy
Section titled “User policy”GIM_HUB_USER_POLICY selects which users the hub admits; the user list is the table
user_policy_list (one row per user ID, with added_at, added_by and an optional note). The
list is kept in every mode and only enforced in allow and deny:
| Mode | Admitted |
|---|---|
open (default) |
every user; the list is not enforced |
allow |
only users on the list |
deny |
every user except those on the list |
The list holds user IDs of any hub, local or remote. A user the policy does not admit:
- cannot register: the first
PUT /v2/users/{user_id}/device-listof an unknown user answers 403user_not_admitted(an existing user’s later uploads are accepted); - cannot sign in:
POST /v2/auth/challengeandPOST /v2/auth/sessionanswer 403user_not_admitted({"error":{"code":"user_not_admitted","message":"user is not admitted on this hub"}});user_suspendedtakes precedence when both apply; - loses its sessions and pending sign-in challenges whenever the policy changes (admin API, CLI picked up by the settings poll, or startup), as a suspension does;
- is refused by this hub’s relay:
ClaimKeyPackagerequested for it,DeliverWelcomesent by it, andSubmitCommit/SubmitMessageit sends to a group this hub owns answerUNAUTHORIZEDwithclient_codeuser_not_admitted(s2s.md). Its own hub reports the refusal to the client: 403not_admitted_by_peerfor a forwarded Commit or message, the target undernot_admittedfor a KeyPackage claim; a refused Welcome is dropped.Deliverof groups owned by other hubs is never filtered.
Only metadata is used: the policy matches user IDs, never message content.
allow with an empty list admits nobody: no user can register or sign in. The hub logs a
warning at startup and on every change that leads to it, and the admin UI shows one. Fill the
list before switching to allow.
The mode is a stored setting (environment > stored > default; set by the environment, it is
read-only in the admin API and CLI). The list is edited with the admin API
(admin.md, “User policy”) or gim-hub config users (CLI); every change is
audited (users_policy_add, users_policy_remove, users_policy_update; a mode change is a
setting_update). Admin API changes apply before the response; CLI changes within about a
second.
Federation
Section titled “Federation”Federation is off unless GIM_HUB_FEDERATION=true; every other variable in this table is ignored
while it is off. Design: federation.md.
| Variable | Tier | Default | Meaning |
|---|---|---|---|
GIM_HUB_FEDERATION |
stored restart | false |
true starts the libp2p host and federation |
GIM_HUB_P2P_LISTEN |
stored restart | /ip4/0.0.0.0/udp/4001/quic-v1,/ip4/0.0.0.0/tcp/4001 |
Comma-separated listen multiaddrs (TCP only when a PSK is set) |
GIM_HUB_P2P_EXTERNAL_ADDRS |
stored restart | unset | Comma-separated multiaddrs advertised to peers (default: the actual listen addresses that are not unspecified) |
GIM_HUB_P2P_BOOTSTRAP |
stored restart | unset | Comma-separated bootstrap multiaddrs, /p2p/<PeerId> optional |
GIM_HUB_FEDERATION_ALLOWLIST |
stored restart | unset | Allowlist file, reloaded on SIGHUP; unset: the stored allowlist (see below) |
GIM_HUB_P2P_PSK_FILE |
stored restart | unset | libp2p swarm.key file; set = private network (pnet), TCP only |
GIM_HUB_P2P_PSK |
stored restart (secret) | unset | The swarm.key content itself (same format, same effect); GIM_HUB_P2P_PSK_FILE wins when both are set. Write-only: never shown by the admin API or the CLI |
GIM_HUB_RUNTIME_CERT_DAYS |
stored restart | 30 |
Runtime certificate lifetime (minimum 2) |
GIM_HUB_S2S_TIMEOUT_SECONDS |
stored restart | 10 |
Timeout of one hub-to-hub request (minimum 1) |
GIM_HUB_DHT_REPUBLISH_HOURS |
stored restart | 12 |
Republish cadence of hosted users’ hosting records (minimum 1) |
GIM_HUB_HOSTING_CACHE_SECONDS |
stored live | 600 |
Freshness of cached remote hosting records |
GIM_HUB_DEVICE_LIST_CACHE_HOURS |
stored live | 24 |
Freshness of cached remote DeviceLists |
GIM_HUB_OUTBOX_TTL_HOURS |
stored live | 168 |
Lifetime of queued outbound hub-to-hub entries (minimum 1) |
GIM_HUB_OUTBOX_MAX_ENTRIES_PER_PEER |
stored live | 100000 |
Outbound backlog cap per peer hub, entries (minimum 1) |
GIM_HUB_OUTBOX_MAX_BYTES_PER_PEER |
stored live | 1073741824 |
Outbound backlog cap per peer hub, bytes (minimum 1) |
GIM_HUB_OUTBOX_BACKOFF_MAX_SECONDS |
stored live | 300 |
Maximum retry backoff per peer hub (minimum 1) |
GIM_HUB_UNHOSTED_GRACE_HOURS |
stored live | 168 |
How long a user who removed this hub from their hosting record keeps read access before their data is purged (minimum 1; multihoming.md §7.3) |
GIM_HUB_PUSH_FAILOVER_SECONDS |
stored restart | 30 |
A co-host hub without a successful ping or hub-to-hub exchange for this long is down; a fallback hub of a multi-homed user pushes only while every hub before it is down (minimum 1; multihoming.md §8) |
GIM_HUB_FETCH_BLOB_PER_MINUTE |
stored live | 600 |
Blob chunk requests (FetchBlob, up to 4 MiB each) served per peer hub and minute (minimum 1; federation.md §5.6) |
The allowlist lists one peer hub_id per line, optionally followed by static multiaddrs without
/p2p/; # starts a comment and the hub’s own hub_id is ignored:
# hub_id [multiaddr ...]hciqgtrdldzdkwq3c7fkmzvld7djgm3waetwtedhujnndujzulpgk6vq /ip4/10.0.0.12/tcp/4001 /ip4/10.0.0.12/udp/4001/quic-v1Allowlist, bootstrap and external addresses may be /ip4/, /ip6/, /dns4/ or /dns6/
multiaddrs. DNS names are resolved with the system resolver (/etc/resolv.conf, /etc/hosts) at
dial time, over TCP and QUIC alike (with a PSK, TCP only).
Where the allowlist comes from:
GIM_HUB_FEDERATION_ALLOWLISTset (environment or stored): that file. The admin API and the CLI show it read-only (source: file).- Otherwise, a non-empty
federation_allowlisttable in the database (source: stored), edited withPUT /admin/api/federation/allowlistorgim-hub config allowlist set <file>. A change applies at once (admin API) or within about a second (CLI), like SIGHUP. - Otherwise, the legacy default file
$GIM_HUB_DATA_DIR/allowlistif it exists (source: file, not read-only): the first edit through the admin API or the CLI stores the entries in the table, which is used from then on; the file is left alone. - Otherwise the allowlist is empty, which stops startup.
A missing or empty allowlist, an invalid line, or a QUIC address together with a PSK
(GIM_HUB_P2P_PSK_FILE or GIM_HUB_P2P_PSK) stops startup. On SIGHUP the file is re-read; an invalid file is logged and
the previous allowlist stays in force; hubs no longer listed are disconnected. With federation
enabled the data directory also holds runtime_key (32-byte Ed25519 seed of the libp2p host key,
mode 0600) and runtime_cert (its certificate, signed by hub_key and renewed automatically when
less than a third of its lifetime remains). Deleting both while the hub is stopped rotates the
PeerId; allowlists are unaffected because they name hub_ids. gim-hub hub-id prints the
hub_id (creating hub_key if missing), which peers need for their allowlists.
Federation acceptance test
Section titled “Federation acceptance test”scripts/fed-test.sh is the closed-federation acceptance test (CI job federation). It needs
only Docker:
- builds the hub image from the
Dockerfileand the mock push server image fromDockerfile.push-mock(GIM_FED_SKIP_BUILD=1reusesGIM_FED_IMAGEandGIM_FED_PUSH_MOCK_IMAGE); - generates fresh hub root keys in the hubs’ volumes with
gim-hub hub-idand writes the allowlists into.tmp/fed-<pid>/(nothing key-related is committed); - starts
deploy/federation/compose.yaml:hub-a,hub-b,hub-callowing each other andhub-x, which is on nobody’s allowlist (each withGIM_HUB_PUBLIC_URL=http://hub-<n>:8080, the base URL of the driver), on an isolated network with fixed IPs (GIM_FED_SUBNET, default10.231.64; change it if that /24 is taken), andpush-mock(push-mock.md), the target of every hub’s push webhook with a secret generated for the run; - runs the driver
crates/gim-hub/tests/fed_compose.rs(cargo test -p gim-hub --test fed_compose -- --ignored) in arust:1.99-bookwormcontainer attached to that network. The driver speaks the client API to every hub with openmls clients and stops and starts hubs through the mounted Docker socket. Scenarios: hub-b’sGET /v2/hub?nonce=proof (verified with its root key and URL; every sign-in also checks that the challenge is v2 for the hub’s URL), hosting-record resolution, a cross-hub direct message, a Commit adding a revoked device of a remote user (400revoked_device), a group across three hubs with add and remove from a non-owning hub, a concurrent Commit conflict (one 201, one 409stale_epoch), a member-hub outage (50 messages, no loss, no duplicates), an owner-hub outage (503peer_unavailable, then delivery), the refusal ofhub-x, a leave by Proposal and webhook push (one signed delivery per message at the mock, none for a Proposal, a registration answered 410 deleted) and the user policy (hub-b switched withdocker exec ... gim-hub configtodenyand thenallow: a remote user’s claim reportednot_admitted, its message to a hub-b group refused with 403not_admitted_by_peer, a listed local user’s session deleted and sign-in refused with 403user_not_admitted; back toopen, the message is delivered and sign-in works); - prints the hub logs on failure and always removes the containers, network and volumes.
All containers, the network and the volumes carry the label ai-agent=true. Other variables:
GIM_FED_PROJECT (compose project, default gim-fed), GIM_FED_CARGO_HOME (cargo home of the
driver container), GIM_FED_TIMEOUT (driver timeout in seconds) and GIM_FED_RUN_LABEL (value of
the ai-agent-run label also set on every container, network and volume, default the project
name).
GIM_FED_PSK=1 bash scripts/fed-test.sh runs the PSK variant (deploy/federation/compose.psk.yaml,
not run in CI): hub-a, hub-b and hub-c share a pnet key generated for the run and use TCP only;
hub-x has no key. It first checks that a hub with the key and a QUIC listen address refuses to
start, then runs the same scenarios; hub-x must never get a Hello to hub-a.
Multi-homing
Section titled “Multi-homing”A user is multi-homed when their hosting record lists several hubs
(multihoming.md). It needs nothing beyond federation
(GIM_HUB_FEDERATION=true): every listed hub must allow the others, and the owning hubs of the
user’s groups must allow all of them. Two variables apply (table above):
GIM_HUB_UNHOSTED_GRACE_HOURS(default 168): after a user removes this hub, their devices may still sign in and read (drain their cursor) for this long; every other write is 403hosting_record_required. The hourly retention task then purges the user’s data and keeps the record as a tombstone.GIM_HUB_PUSH_FAILOVER_SECONDS(default 30): only the user’s primary hub pushes; a fallback hub starts pushing once every hub before it has been unreachable for this long and stops when it sees the primary again. Lower it for faster failover at the cost of double pushes during short blips (devices collapse them oncollapse_id).
Known limits: a group whose owning hub is down cannot sequence (503 peer_unavailable through
every hub; owning-hub migration is out of scope); blobs are not replicated: a hub fetches a
remote blob from its origin on first download, so while the origin is down only copies fetched
before are served; an added hub serves no history before its replica seed; pushes for messages
that arrive in the failover window after the primary stops are not sent late; the client must
PUT a new record to the added hub before the other hubs, or an owner that only caches the
user’s record may seed the added hub too early and the seed is dropped (§4.3).
GIM_FED_SUITE=mh bash scripts/fed-test.sh runs the multi-homing acceptance suite
(multihoming.md §11, MH1-MH9; driver crates/gim-hub/tests/multihoming_compose.rs; CI job
federation) with the same harness: it starts only hub-a, hub-b and hub-c with the
override deploy/federation/compose.mh.yaml (GIM_HUB_UNHOSTED_GRACE_HOURS=1,
GIM_HUB_PUSH_FAILOVER_SECONDS=5, GIM_HUB_PUSH_LOG_ENDPOINT=true) and observes pushes through
GET /v2/debug/push-log. GIM_FED_SUITE=all runs the federation suite and then the
multi-homing suite on a fresh stack with the same image. The run cannot advance the hubs’
clocks, so the purge after the grace period is covered by the in-process test
crates/gim-hub-api/tests/multihoming_records.rs, not by the compose suite.
Push design, webhook contract and payload: push.md. The hub integrates no push
platform: every push for a registration whose provider is not log is POSTed, signed, to the push
server at GIM_HUB_PUSH_WEBHOOK_URL, which owns platform delivery and credentials. Without the URL
those pushes are logged as skipped. The log provider is always available. A URL without a valid
secret stops startup. Keep the secret out of the image (e.g. inject it from /run/secrets).
| Variable | Tier | Default | Meaning |
|---|---|---|---|
GIM_HUB_PUSH_MEMBER_EVENTS |
stored live | false |
Also push group.member_added / group.member_removed (silent) |
GIM_HUB_PUSH_MINIMAL |
stored live | false |
true sends minimal wake-up payloads {"v":1,"type":"wake","collapse_id"} only (no group, sequence or MLS message; see push.md) |
GIM_HUB_PUSH_QUEUE_CAPACITY |
stored live | 10000 |
Most devices with a queued push; further pushes are dropped (minimum 1) |
GIM_HUB_PUSH_LOG_ENDPOINT |
stored live | false |
Serve the unauthenticated, test-only GET /v2/debug/push-log; never enable in production |
GIM_HUB_PUSH_WEBHOOK_URL |
stored live | unset | http or https URL of the push server; every non-log push is POSTed there. Unset: those pushes are skipped |
GIM_HUB_PUSH_WEBHOOK_SECRET |
stored live (secret) | unset | HMAC-SHA256 key of X-Gim-Signature; required with the URL, at least 32 bytes |
GIM_HUB_PUSH_WEBHOOK_TIMEOUT_SECONDS |
stored live | 10 |
Timeout of one webhook request; a timeout is retried (minimum 1) |
Admin API
Section titled “Admin API”The admin API and UI listen on a separate socket, off unless
GIM_HUB_ADMIN_LISTEN is set; nothing admin-related is served on
GIM_HUB_LISTEN. Setup, the security model and every endpoint:
admin.md.
| Variable | Tier | Default | Meaning |
|---|---|---|---|
GIM_HUB_ADMIN_LISTEN |
env-only | unset | Socket address of the admin listener (recommended 127.0.0.1:8081 behind a TLS proxy); unset disables it and the other GIM_HUB_ADMIN_* variables are ignored |
GIM_HUB_ADMIN_TOKEN_FILE |
env-only | unset | File holding the admin token (trimmed, at least 32 characters); recommended, e.g. a Docker/compose secret mounted as a file |
GIM_HUB_ADMIN_TOKEN |
env-only | unset | The admin token itself (same rules); mutually exclusive with GIM_HUB_ADMIN_TOKEN_FILE. With neither set, token login is disabled |
GIM_HUB_ADMIN_TRUSTED_HEADER |
env-only | unset | Identity header set by a reverse proxy (e.g. X-Forwarded-User); requires GIM_HUB_ADMIN_TRUSTED_PROXIES |
GIM_HUB_ADMIN_TRUSTED_PROXIES |
env-only | unset | Comma-separated CIDRs whose X-Forwarded-For and identity header are trusted |
GIM_HUB_ADMIN_COOKIE_SECURE |
env-only | false |
true sets Secure on the session cookie |
With GIM_HUB_ADMIN_LISTEN set, at least one of GIM_HUB_ADMIN_TOKEN,
GIM_HUB_ADMIN_TOKEN_FILE and the trusted header is required; otherwise the
hub does not start. Prefer the file: an environment token is visible in
docker inspect and /proc/<pid>/environ, is inherited by child processes,
and is plaintext in lode.toml [env].
Stored settings
Section titled “Stored settings”Every setting marked stored live or stored restart in the tables above can also be stored in
the database (table settings), through the admin API (admin.md, “Settings”) or
the CLI below. Bootstrap settings (env-only: listeners, data directory, database path, admin
token and proxy settings) are never stored; they are needed before the database is open or
protect the admin API itself.
- Precedence: environment variable > stored value > default. A setting set by the environment is read-only: the admin API answers 409 “set by environment” and the CLI refuses it; unset the variable to manage the setting in the database.
- Validation: a stored value is checked with the same parser as the environment variable, in the context of the other settings, before anything is written (e.g. a webhook URL needs a valid secret first; enabling federation needs a public URL). An invalid value is refused with the parser’s message and nothing is stored.
- Secrets (
GIM_HUB_PUSH_WEBHOOK_SECRET,GIM_HUB_P2P_PSK) are write-only: the admin API returnsvalue: nullandis_set, the CLI prints(set)/(unset), the audit log***. They are stored in plaintext in the database, like the rest of its content; protect the data directory accordingly or keep secrets in the environment (e.g./run/secrets). - Apply: a
stored livechange takes effect in the running hub (rate limits, push options and webhook, caches, outbox limits, retention, message and blob limits, …); astored restartchange is reported asrestart_pendinguntil the hub restarts. - Reload: every write bumps a version in the database; the hub polls it every second, so a change made by the CLI (or another process) applies within about one second. A change through the admin API applies before the response.
- An invalid stored value found at startup (e.g. written by a newer release or by hand) is
logged at
errorand ignored, so the default applies; the hub still starts. An invalid environment variable still stops startup. - Every change is audited in the admin audit log (
setting_update,setting_unset,allowlist_update,users_policy_add,users_policy_remove,users_policy_update), with actorclifor the CLI. - PSK:
GIM_HUB_P2P_PSK_FILEnames aswarm.keyfile;GIM_HUB_P2P_PSKholds the same content (stored as a secret, or in the environment). The file wins when both are set. Both apply at restart.
gim-hub config reads and writes the stored settings of the database named by the bootstrap
environment (GIM_HUB_DATA_DIR, GIM_HUB_DATABASE_PATH); run it with the hub’s environment, e.g.
docker exec <container> gim-hub config list. It works while the hub runs (SQLite WAL); the hub
picks changes up within about a second.
gim-hub config list # every setting: value, source (env/stored/default), applygim-hub config get KEY # one setting with its description and defaultgim-hub config set KEY VALUE # validate and storegim-hub config unset KEY # delete the stored value (the default applies)gim-hub config allowlist list # the effective allowlist and its sourcegim-hub config allowlist set FILE # validate FILE (allowlist format) and store it in the tablegim-hub config users list # the user policy mode and its source, then the user listgim-hub config users add USER [NOTE] # list a user: a user ID or @handle (a handle on this hub)gim-hub config users remove USER_ID # remove a user from the listgim-hub config users set FILE # replace the list with FILE: one user ID per line, # commentsA key set in the CLI’s environment is refused by set and unset (exit 1), as the environment
would override the stored value. allowlist set is refused while GIM_HUB_FEDERATION_ALLOWLIST
names a file. users set validates every line first and stores nothing on an invalid one (the
message names the line); users that stay listed keep their metadata. users add of a listed
user and users remove of an unlisted one exit 1. The mode is set with gim-hub config set GIM_HUB_USER_POLICY allow|deny|open. Invalid values exit 1 with the parser’s message and store nothing. Every change,
and every invalid value refused, is audited with actor cli (a refused environment key is not:
nothing was attempted).
Data directory
Section titled “Data directory”$GIM_HUB_DATA_DIR/├── hub.db SQLite database (WAL mode, with hub.db-wal and hub.db-shm)├── hub_key 32-byte Ed25519 seed of the hub root key, mode 0600├── backups/ Pre-migration database copies, `hub-<from_schema>-<version>.db`└── blobs/ Encrypted blob contents, one file per blob: uploads and cached copies of other hubs' blobs; `<hub_id>_b_<random>` (legacy uploads: `b_<random>`); `.*.tmp` files are unfinished writeshub.dbis created on first start; schema migrations are embedded in the binary and applied automatically (tracked inPRAGMA user_version).- Before applying pending migrations to an existing database (schema above 0)
the hub copies it with SQLite’s online backup to
backups/hub-<from_schema>-<version>.db: the schema it is migrating from and the release version doing it (see “Running under lode” for the version). Only the newest threehub-*.dbfiles are kept; older ones are deleted after each new backup. A fresh database is not backed up. The directory is always underGIM_HUB_DATA_DIR, even with a customGIM_HUB_DATABASE_PATH. - A database with a newer schema than the binary knows is refused before
anything is written to it: the hub exits non-zero with
database schema version N is newer than this binary supports (up to M); restore a backup or run a newer release. hub_keyis generated on first start. It is the hub ROOT key and determines the hub’shub_id(gim-core identity spec): the audience of sign-in challenges, the owner prefix of group IDs and the hub named in users’ hosting records. Replacing it changeshub_id, so it must be kept secret, backed up with the database and never replaced on a running deployment. Thehub_idis logged at startup.- Back up
hub.dbwith SQLite’s online backup (sqlite3 hub.db ".backup out.db") or while the hub is stopped; copying the file alone while running can miss the WAL.
Retention
Section titled “Retention”An hourly background task (also run at startup) deletes events older than
GIM_HUB_EVENT_RETENTION_DAYS and expired device-link bundles, and purges the
data of users who removed this hub more than GIM_HUB_UNHOSTED_GRACE_HOURS ago
(their hosting record is kept as a tombstone). A client whose cursor is older than the
newest pruned event receives 410 cursor_expired with the head cursor and
must resync. Pending Welcomes are kept until acknowledged, independently of
event retention.
Blobs have no retention: uploaded blobs and the cached copies of other hubs’
blobs (fetched on first download with federation, remote_blobs) are kept
until the data directory is cleaned up by the operator. A cache eviction policy
for the copies is future work; an evicted copy would simply be fetched again
from its origin.
Docker
Section titled “Docker”The Dockerfile builds a release binary in rust:1.99-bookworm and runs it on
debian:bookworm-slim as the unprivileged user gimhub (uid 10001).
GIM_HUB_LISTEN=0.0.0.0:8080andGIM_HUB_DATA_DIR=/dataare set in the image; port 8080 is exposed and/datais a volume. Mount a named volume or a host directory writable by uid 10001 there, and back it up as described above (it holdshub_key).- Port 4001 (TCP and UDP) is exposed for federation (
GIM_HUB_P2P_LISTENdefault). - The container
HEALTHCHECKrunsgim-hub healthcheck, which sendsGET /healthzto theGIM_HUB_LISTENport (loopback if the address is unspecified) and exits non-zero unless it gets 200. No HTTP client is installed in the image. - Other variables from the table above are passed with
-e.
docker build -t gim-hub .docker run -d --name gim-hub -p 8080:8080 -v gim-hub-data:/data \ -e GIM_HUB_EVENT_RETENTION_DAYS=14 gim-hubscripts/docker-smoke.sh builds the image and checks a running container, then
does the same for the mock push server (signed webhook 200, bad signature 401);
CI runs it on every push. scripts/docker-build.sh IMAGE push-mock builds
Dockerfile.push-mock.
Build notes:
- The admin UI (
admin-ui/) is embedded in the binary:gim-hub-adminfails to compile unlessadmin-ui/dist/index.htmlexists (“run scripts/admin-ui.sh build first”). The Dockerfile builds it from source in anoven/bun:1.4.3stage (.dockerignoreexcludesadmin-ui/distandnode_modules); for a localcargo build, runscripts/admin-ui.sh buildfirst (scripts/check.shdoes). - The builder caches only the cargo registry and git checkouts.
target/is not cached: a BuildKit cache mount is shared by every checkout built on the same daemon, and cargo judges freshness by mtime, so a sharedtarget/can yield a binary built from another checkout’s sources. Docker’s layer cache is keyed on the build context content and stays safe. gim-hub versionprintsgim-hub <version> (<rev>): the release version (see “Running under lode”) and theGIM_HUB_BUILD_REVbuild argument (unknownif unset).scripts/docker-build.sh IMAGE, used bydocker-smoke.shandfed-test.sh, passes the checkout’s revision (with a-dirty-<hash>suffix for uncommitted changes to tracked files) and fails unless the built image reports exactly that revision.
Running under lode
Section titled “Running under lode”lode (v0.1.0) can supervise the hub: it
downloads and verifies signed release assets, runs the hub, commits a version
once the hub reports ready and rolls back to the last good version when a new
one fails. The hub implements the lode app contract only when LODE_DIR is
set; without it nothing changes.
-
Image and config.
Dockerfile.lodeis adebian:bookworm-slimimage with the lode binary (andlode-cli, the same binary) asENTRYPOINT, running as the non-rootgimhubuser, with two volumes:/srv/lode(LODE_DIR:lode.toml,versions/,state.json) and/data(the hub data). Start fromdeploy/lode/lode.toml: put it at/srv/lode/lode.tomland pick the[update].assetof the host architecture. Its[trust]enforces signatures and trusts the release key of the official gimhq/gim-hub releases:trusted_keys = ["f22e33416e94c5ba:jeWP-q9Mz89Vxm5Zq5Ss0kiPtBcthFKSlb0jfKCh1rs"]Unsigned releases install only with
require_signature = "off", for local testing.[update].policy = "check"only records a newer release (lode-cli status); install it withlode-cli update, or set"auto".Terminal window docker build -t gim-hub-lode - < Dockerfile.lodedocker run -d --name gim-hub --stop-timeout 40 -p 8080:8080 \-v gim-hub-lode:/srv/lode -v gim-hub-data:/data gim-hub-lodedocker exec gim-hub lode-cli status -
Environment. lode injects
LODE_DIR,LODE_INSTANCE,LODE_ACTIVE_VERSION,LODE_READINESS,LODE_WORKDIRandLODE_CONFIG. TheGIM_HUB_*variables come from the container env or thelode.toml[env]table (defaults; the container env wins). KeepGIM_HUB_DATA_DIRoutsideLODE_DIR: lode replaces and prunes the version directories, and the hub’s cwd is the version directory. -
Admin listener. The admin UI and API (
admin.md) are embedded in every release asset and served on their own port, never on 8080. Enable them inlode.toml[env](commented example) or the container env:GIM_HUB_ADMIN_LISTEN=0.0.0.0:8081inside the container (127.0.0.1:8081when the hub runs directly on a host), a token file (GIM_HUB_ADMIN_TOKEN_FILE=/data/admin.token) and/or the trusted proxy header, andGIM_HUB_ADMIN_COOKIE_SECURE=truebehind TLS. Do not publish 8081 (no-p 8081:8081): only the reverse proxy on the container’s network reaches it;deploy/lode/traefik-admin.ymlroutes an admin host through traefik’soidcmiddleware. The UI’s lode panel reads and writes$LODE_DIR/state.jsonlikelode-cli(check, update, restart, rollback). -
Readiness (
[supervise].readiness = "state"). After the migrations ran, the client API listener is bound and federation (if enabled) started, the hub setsstate.ready = $LODE_INSTANCEin$LODE_DIR/state.jsonunderstate.json.lock, keeping lode’s fields. A failure to write it is logged and the hub keeps serving; lode then rolls back afterready_timeout(60 s in the example). A version that exits withinhealth_grace(15 s) is rolled back too. -
Stop. On SIGTERM the hub stops accepting connections, finishes in-flight requests and exits 0, well within
stop_timeout = 30. Give the container a longer stop grace than that (--stop-timeout 40,stop_grace_period: 40s). -
Version reporting.
GET /healthz,GET /v2/hub(version) andgim-hub versionreportLODE_ACTIVE_VERSIONwhen supervised (the version lode installed, e.g.0.4.0), else the package version (0.4.0).gim-hub versionadds the build revision:gim-hub 0.4.0 (<rev>). The same version names the pre-migration backups. -
Rollback and the database. lode rolls back the binary, not the data. Only an older release whose schema is not behind the database can run on it: an older binary refuses a newer schema and exits non-zero, so lode reports the failure instead of serving on a migrated database. To go back to a release with an older schema, restore its backup:
- Stop the hub (
docker stop gim-hub), or sethold = trueinstate.jsonso lode does not restart it. - Pick the backup made when the newer release migrated the database:
backups/hub-<from_schema>-<version>.dbholds schema<from_schema>, the schema of the release before<version>. - Move
hub.db,hub.db-walandhub.db-shmaside, then copy the backup tohub.db(owned by the hub user). - Pin lode to the older release (
[update].pin, orlode-cli rollback --version <old>) and start the hub again.
Writes made after the backup are lost by a restore; prefer rolling forward with a fixed release when possible.
- Stop the hub (
-
Releases.
.github/workflows/release.ymlruns when a GitHub Release is published (tagv*) and uploadsgim-hub-linux-x86_64.tar.gzandgim-hub-linux-aarch64.tar.gz(the archive root holdsgim-hub; built on Debian bookworm, glibc 2.36). With theLODE_SIGNING_KEYsecret set, each asset is signed withlode-cli sign --app gim-hub --version <tag without v>(lode-cli from the pinned, checksum-verified bkhq/lode v0.1.0 release) and uploaded with its<asset>.sig; without it the assets are uploaded unsigned, whichrequire_signature = "enforce"rejects. -
Smoke test.
scripts/lode-smoke.shbuilds the admin UI, the hub and the image, serves a signed native manifest from a sibling container and checks a real install (with a pre-migration backup) serving the embedded admin UI on its own port with an environment-only configuration (reported as sourceenvbygim-hub config), an update, a rollback from a release that fails at startup and a gracefuldocker stop.
Shutdown
Section titled “Shutdown”The hub stops accepting connections on SIGINT or SIGTERM and finishes in-flight requests before exiting.