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 an AI assistant, CI job, or anyone debugging the sign-in 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 you're in a browser (or a browser-driving tool) and 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.