Self-hosting

The whole product is one static binary: amadan-hub. Front door, web UI, and per-repo instance servers, all self-contained, assets embedded, CGO_ENABLED=0, and no external requests from any page.

Run it on your own box, under your own domain, pointed at a Keymail deployment for sign-in.

Try it locally first#

hack/local-deploy-demo.sh

Builds every binary and drives a real end-to-end journey — sign-in, key registration, an encrypted push and clone, a plaintext push and clone with a real token — as separate OS processes under a scratch directory that cleans itself up. No credentials needed, no mocks, nothing left behind.

Set AMADAN_DEMO_KEEP_RUNNING=1 to leave the stack up and poke at it in a browser afterwards.

Configuration#

The hub is configured entirely by environment variables.

Required#

  • AMADAN_KEYMAIL_ISSUER — the real Keymail deployment's OAuth issuer. Never point this at fakeymail in production.
  • AMADAN_OAUTH_CLIENT_ID — the hub's IndieAuth-style client_id, which is simply its own origin (e.g. https://amadan.example.com), normally identical to AMADAN_ORIGIN. Keymail has no client registration and no client secret: the origin is the registration, and PKCE plus a same-origin redirect are what secure the exchange. So there is nothing to register with Keymail beforehand.
  • AMADAN_DATA_DIR — where the hub's own SQLite database, every repo instance's SQLite file, and the materialized git state live. Defaults to ./amadan-data, which is not a production-safe default.
  • AMADAN_INTERNAL_KEY — read the section below before deploying. This is the one people miss.
  • AMADAN_ORIGIN — the hub's own public URL. Used to build the OAuth redirect default, to decide whether session cookies get the Secure flag, and to construct the Go module paths for public repos. Defaults to http://127.0.0.1:8080.

Optional#

  • AMADAN_OAUTH_REDIRECT — overrides the default <AMADAN_ORIGIN>/auth/callback. Must stay same-origin with AMADAN_OAUTH_CLIENT_ID; Keymail rejects a cross-origin redirect.
  • AMADAN_GIT_RL_RPS — requests per second per client IP for unauthenticated public-tier git reads. Token bucket, burst around 10. Defaults to 5. An unset or unparseable value falls back to the default rather than disabling the limiter.
  • AMADAN_MINTERlocal (the default) or platform. Use local. See the known gap below.
  • AMADAN_REPO_BIN — path to a standalone amadan-repo binary for the local minter to spawn. Leave it unset in production: the hub then self-execs its own binary as amadan-hub repo …, so one shipped artifact is both the front door and the repo server, and nothing has to be co-located on the box.

Set for you, unless you run instances yourself#

AMADAN_REPO_PATH tells a repo instance which repo it is, as the {ns}/{repo} people address it by. Every signed request to the encrypted tier is verified under a context bound to that name, so an instance that does not know it authenticates nobody.

The hub sets it when it spawns an instance, so there is nothing to do here. It only becomes yours if you run amadan-repo by hand — a standalone instance with no AMADAN_REPO_PATH will serve public reads and refuse every signature.

AMADAN_INTERNAL_KEY — read this before deploying#

This key authenticates the hub to its own repo instances. Set it explicitly, to 32 bytes as hex (64 characters) or base64:

openssl rand -hex 32

If you don't set it, the hub generates one and persists it to a file under AMADAN_DATA_DIR — or, if that is also unset, under the system temp directory.

That fallback exists so a first run works, and it is not safe to rely on in production. On ephemeral storage the key vanishes on restart and the hub can no longer talk to the instances it created. Set it explicitly before going live.

Known gap: AMADAN_MINTER=platform#

Only AMADAN_MINTER=local is implemented. platform is a documented stub that fails the hub at startup, by design, rather than half-working.

With local, the hub is one long-lived process that spawns a repo instance per repo on its own machine. That is the recommended — and currently the only — configuration.

The consequence worth knowing: you cannot yet keep your ciphertext on hardware you own while somebody else's hub handles names and membership. The architecture leans that way (every repo is its own hibernating instance server the hub reaches by URL), but today a hub only mints instances locally.

Your hub hosts Go modules#

Any public-tier repo on your deployment is a Go module host, with no configuration. The hub answers Go's ?go-get=1 probe with a go-import meta tag built from AMADAN_ORIGIN:

go install your-hub.example.com/<ns>/<repo>/cmd/<tool>@latest

Only public repos. Private and encrypted repos fall through untouched — the tag would otherwise both leak that a repo exists and offer closed source as a module.

  • docs/deploy.md in the repository is the full operator's guide, including production checklists and the CARLOS deployment shape.
  • Tiers and encryption — what your server will and won't be able to read.
  • CI — runners are member machines, so there is no CI infrastructure for you to operate.