A code host you sign into with a Bitcoin Cash wallet. Repositories on your own VPS, cold blobs on the Sia network. No email. No password reset. No middleman.
A friction-free GitHub alternative for people who want to hold their own identity. Under the hood it's Forgejo (a soft-fork of Gitea) with a wallet-authentication layer bolted on and cold storage moved from disk to Sia.
Generate or import a BCH wallet in your browser. Sign a one-time challenge to prove control. That's your account — nothing to remember except your recovery phrase.
Every LFS object, release archive, package registry blob and issue attachment lives on the Sia network via s3.silentmode.st:8600. Live git repos + Postgres stay on the VPS SSD for speed.
Nothing weird. git clone, git push, PRs, issues, releases, container registry, the works. Bring your existing muscle memory.
bch_<first-20-of-your-cashaddr>. You land in the dashboard.Why no email? Nothing to leak, nothing to phish, no password-reset flow to hijack. The trade: if you lose your recovery phrase there is no support form to send you back in. Back the phrase up. Twice. On paper.
If you're driving the sign-in from a CLI, CI job, or any scripted client without a real browser, read this first. It heads off two misdiagnoses we've already hit.
It's not a stub — it's a deliberately small bootstrap page that renders the wallet UI client-side via a JS module (standard SPA pattern). Size ≠ brokenness. The full UI (mnemonic generation, passphrase encryption, sign flow) is inside wallet.js, not the shell. Test by requesting wallet.js instead:
curl -sI https://code.silentmode.st/auth/static/wallet.js # expect: HTTP 200, ~10 KB, Content-Type: application/javascript
It only 404s if you drop the /auth/ prefix. Caddy path-multiplexes: /auth/* goes to the auth-proxy, everything else goes to Forgejo. Forgejo has no /static/wallet.js, so that path is expected-404. The real paths are:
| URL | Served by | Purpose |
|---|---|---|
| /auth/authorize | auth-proxy | Login shell HTML (SPA bootstrap) |
| /auth/static/wallet.js | auth-proxy | Wallet UI + signing logic (~10 KB) |
| /auth/static/login.css | auth-proxy | Styles for the login card |
| /auth/challenge | auth-proxy | POST cashaddr → get nonce + signed message |
| /auth/verify | auth-proxy | POST nonce + signature → get OAuth code |
| /auth/.well-known/openid-configuration | auth-proxy | OIDC discovery |
| /user/oauth2/hephaestus-wallet | Forgejo | Kicks off the OAuth handshake (307 → /auth/authorize) |
| /user/oauth2/hephaestus-wallet/callback | Forgejo | Receives the OAuth code, mints session |
| /api/v1/version | Forgejo | Public: returns Forgejo version |
| /api/v1/user | Forgejo | Requires an API token (not a session cookie) — returning 401 in a browser is not a bug |
The wallet signing is easiest to reproduce in Node/Deno or a headless browser — the crypto is BIP-137 "Bitcoin Signed Message" over BCH secp256k1. But the server-side pieces alone can be smoke-tested with curl:
ISSUER=https://code.silentmode.st/auth
# 1. discovery document lists all endpoints
curl -s "$ISSUER/.well-known/openid-configuration" | jq .
# 2. request a challenge for any cashaddr (server just holds it 5 min)
curl -s -X POST -H 'content-type: application/json' \
-d '{"cashaddr":"bitcoincash:qq00000000000000000000000000000000000000q"}' \
"$ISSUER/challenge"
# → returns {"nonce": "...", "message": "code.silentmode.st wants you to sign in..."}
# 3. any BCH wallet that speaks "Sign Message" can produce a valid signature
# for that exact message. Then POST it:
curl -s -X POST -H 'content-type: application/json' \
-d '{"nonce":"THE_NONCE","signature":"BASE64_SIG"}' \
"$ISSUER/verify"
# → returns {"redirect": "https://code.silentmode.st/user/oauth2/hephaestus-wallet/callback?code=..."}
For the JS side (which is what the browser does), inspect auth-proxy/public/wallet.js in the repo — it's ~250 lines and does exactly this: generateBip39Mnemonic → deriveHdPath("m/44'/145'/0'/0/0") → sign(magicHash(message)). Nothing exotic.
If your CLI drives a real browser (Playwright, Puppeteer, curl-with-cookies) and you want to know if you're actually signed in:
// The URL bar isn't reliable when Forgejo shows Security or Settings pages —
// they render for logged-in users but don't have "Dashboard" in the title.
// Best check: does the profile menu render, and is there no "Sign in" link?
const signedIn = !document.querySelector('a[href*="/user/login"]')
&& !!document.querySelector('[aria-label*="Profile"], .user-menu, .avatar');
// The username, if you're on the Dashboard:
document.title.match(/^(bch_\w+)/)?.[1];
Do not test with fetch('/api/v1/user') unless you have an API token. That endpoint requires token auth even when you're signed into the web UI. Session cookies get you into Forgejo pages; they don't get you into the token-scoped API. A 401 there means "no token" — it does not mean "not signed in."
Two paths, either works. Pick whichever fits your setup.
Log in, open Settings → Applications → Generate New Token, give it write:repository scope. Then:
git clone https://your_bch_username:YOUR_TOKEN@code.silentmode.st/your_bch_username/your-repo.git cd your-repo echo "Hello, forge" > README.md git add README.md && git commit -m "hello" git push
Add your public key to Settings → SSH / GPG Keys → Add Key. Then use port 2222:
git clone ssh://git@code.silentmode.st:2222/your_bch_username/your-repo.git
Your key is only trusted for git-over-SSH; there's no shell access.
you (browser)
│
https://code.silentmode.st/
│
┌──────────┴──────────┐
│ Caddy │ ← Let's Encrypt cert, HTTP/3
│ (path-based mux) │
└──┬───────────────┬──┘
│ │
│/auth/* │/
▼ ▼
auth-proxy Forgejo ── Postgres (live git repos + DB
(OIDC provider) ───┬─── on local VPS SSD)
Node/Fastify │
libauth for │ storage backend
BCH signature ▼
verification s3.silentmode.st:8600 ← s3d gateway
│
▼
Sia network hosts
(LFS, attachments,
packages, archives)
| Component | Role |
|---|---|
| Forgejo 10 | Web UI, PRs, issues, releases, container/npm registry, git-over-HTTPS + SSH |
| Postgres 16 | User records, issues, PRs, permissions, session state |
| Caddy 2 | Reverse proxy + automatic Let's Encrypt TLS + HTTP/3 |
| auth-proxy | OIDC provider (Node/Fastify + libauth). Issues signed challenges, verifies BCH signatures, mints OAuth codes. |
| s3d → Sia | Cold storage. Files are encrypted client-side, sharded via Reed-Solomon, scattered across independent hosts. |
| restic | Nightly encrypted snapshots (Postgres + /data/git) to a separate Sia bucket. Systemd timer at 03:15 UTC. |
Any BCH wallet that speaks Bitcoin Signed Message can produce a signature Hephaestus will accept. There is no Hephaestus-specific signing format.
Message the wallet signs: ───────────────────────────── code.silentmode.st wants you to sign in with your Bitcoin Cash account: bitcoincash:qzkc695pm4r3p7kcq36sh0t3fdu5e6ua7u2ar36vea By signing, you prove you control this address. This request will not trigger a blockchain transaction or cost any fees. Domain: code.silentmode.st Nonce: e65d80b51d3f1baa2efb8d952d827104 Issued At: 2026-08-29T17:50:12.655Z
Signature is standard BIP-137-style recoverable ECDSA over the SHA256(SHA256(varint(magic) || magic || varint(msg) || msg)) digest, base64-encoded. The server recovers the pubkey, derives the cashaddr, and matches it against the address you claimed. Nonces expire after 5 minutes and are single-use.
OIDC id_token claims: sub = full cashaddr, preferred_username = "bch_" + first-20-of-address, plus a custom cashaddr claim carrying the full address for downstream tools. Signed with EdDSA (Ed25519).
Hephaestus is a docker-compose stack. Everything you need is in the silentmode monorepo under Hephaestus/. To stand up your own instance:
git clone ssh://git@code.silentmode.st:2222/silentmode/silentmode.git cd silentmode/Hephaestus cp .env.example .env # fill in real values — see .env.example for what each field means
docker compose up -dFirst run pulls ~500 MB of images and builds the auth-proxy. Give it a few minutes.
/ with your admin creds (see PROTOCOL.md in the repo for the exact field list — Forgejo's --config path prevents the CLI installer working, so it has to be the HTTP path).docker compose exec forgejo forgejo \
--config /data/gitea/conf/app.ini admin auth add-oauth \
--provider openidConnect \
--name hephaestus-wallet \
--key "$AUTH_PROXY_CLIENT_ID" --secret "$AUTH_PROXY_CLIENT_SECRET" \
--auto-discover-url "$AUTH_PROXY_ISSUER/.well-known/openid-configuration" \
--scopes "openid profile"app.ini from env vars on every start — sed edits inside the container get clobbered. Everything must come from FORGEJO__section__KEY env vars in docker-compose.yml.DISABLE_REGISTRATION=true blocks OIDC auto-provisioning too, not just the local form. Use DISABLE_REGISTRATION=false + ALLOW_ONLY_EXTERNAL_REGISTRATION=true + SHOW_REGISTRATION_BUTTON=false.[oauth2_client] ENABLE_AUTO_REGISTRATION=true AND USERNAME=preferred_username AND an email claim on the id_token (auth-proxy synthesizes one).[a-zA-Z0-9-_.]+, max 40 chars. Raw cashaddrs (54 chars, colon) don't fit — auth-proxy sends bch_<first-20> as the username while keeping the full cashaddr in sub./auth/* path prefix behind a reverse proxy, all client-side asset paths and fetch URLs must be path-relative (no leading slash), otherwise the browser resolves them to origin root and hits the wrong service.| Live at | code.silentmode.st |
|---|---|
| Signup | Open (wallet-only; local registration disabled) |
| Storage | Sia network for cold blobs, VPS SSD for hot path |
| Backups | Nightly restic → separate Sia bucket, encrypted client-side |
| Source | silentmode/silentmode (private — request access from an admin), or the local checkout under Hephaestus/ |
| License | Forgejo is GPL-3.0. Auth-proxy + Caddyfile + docker-compose scaffolding is part of the Silent Mode monorepo. |
| Chain | Chipnet (Bitcoin Cash test network) alpha |
| BCNR name | hephaestus.x — minted on chipnet, points at https://code.silentmode.st/. Resolve via navigate.st gateway or any BCNR-aware browser (Theseus, Ariadne). |
Report an issue. Open one at silentmode/silentmode/issues once you have access, or ping in the usual Silent Mode channels.