Hephaestus docs
A code forge you sign in to with a Bitcoin Cash wallet. Repos on a fast local disk, cold blobs on Sia, container registry baked in. Open source, self-hostable. This page is the map โ every section is a one-shot recipe.
1. What Hephaestus is
Hephaestus is a Forgejo instance with a Silent Mode auth-proxy in front: instead of email + password, sign-in uses a Bitcoin Cash wallet signature (BIP-137 "Bitcoin Signed Message"). Everything else โ git, PRs, issues, releases, container/npm registries, the API โ is stock Forgejo. Nothing custom for you to learn if you have used Gitea or Forgejo before.
- runs on
- Docker Compose ยท Caddy + Postgres + Forgejo 10 + auth-proxy
- sign-in
- BCH wallet signature โ OIDC callback โ Forgejo session
- usernames
- Auto:
bch_<first-20-of-your-cashaddr>. Renameable to anything else. - storage
- Live git + Postgres on SSD; LFS/releases/packages spill to Sia via s3.silentmode.st
- source
- silentmode/hephaestus โ MIT, open
2. Two hostnames, one instance
The same Forgejo is reachable at two addresses. Same account, same repos, same session cookie โ differs only in how you got there:
| Host | How to reach it | Cert |
|---|---|---|
| hephaestus.x | A BCNR-aware browser (Theseus, Ariadne) resolves it directly, or through the public gateway at navigate.st/bns/hephaestus.x/ | Silent Mode Argonautica CA |
| code.silentmode.st | Regular DNS + Let's Encrypt. Works in any browser, no resolver setup | Let's Encrypt (public) |
code.silentmode.st. It works everywhere. Use hephaestus.x when the whole path is BCNR-aware.3. Sign in with a wallet
The sign-in button gives you three routes:
- Sign up โ creates a fresh BIP-39 wallet in your browser, shows you the recovery phrase, and encrypts the wallet in
localStoragewith a passphrase you set. easiest - Sign in โ you paste a 12- or 24-word recovery phrase you already have. Same wallet as anywhere else that speaks BIP-39.
- WizardConnect โ sign with an external wallet (Cashonize / Paytaca) via an encrypted Nostr relay. Keys never leave the wallet. most private
What happens on the wire
- Forgejo redirects you to the auth-proxy's
/auth/authorize. - Your browser POSTs
{cashaddr, state}to/auth/challengeand gets back a nonce + message. - The wallet signs the message. Standard "Bitcoin Signed Message" (varint-prefixed magic + double-SHA256 + secp256k1 recoverable-compact + base64).
- Browser POSTs
{nonce, signature, state}to/auth/verify. The auth-proxy recovers the pubkey, re-hashes to a cashaddr, matches, mints an Ed25519-signedid_token, and hands back a Forgejo callback URL. - Following the callback lands you in the dashboard with a session cookie.
If you use the flow programmatically (no browser)
Every step is a JSON POST. The wallet-signing bit is the only crypto: sign the exact message string the server returned โ no trailing newline. See auth-proxy/src/verify.ts for the byte-perfect reference. A working sample lives in PROTOCOL.md.
4. Rename your username
The auto-generated bch_<addr-prefix> is a placeholder. Rename yourself to anything else โ BitcoinCash, alice, whatever's not taken.
From the UI
Settings โ Profile โ Username. Change the field, save. All your repo URLs move to /<new-name>/โฆ and the old URLs 307-redirect for a while so bookmarks survive.
From the admin API (if you're the operator)
# The endpoint expects FORM-encoded, not JSON. If you send JSON it silently returns 422 "NewName required".
curl -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "new_name=BitcoinCash" \
https://code.silentmode.st/api/v1/admin/users/<old-name>/rename
5. Create + push a repo
Standard Forgejo. Nothing custom.
Create
- UI: + menu top-right โ New Repository
- API:
POST /api/v1/user/reposwith a token
Clone + push
# HTTPS โ always works, no key setup git clone https://code.silentmode.st/<you>/<repo>.git # SSH โ needs your key uploaded under Settings โ SSH keys. Port is 2222, not 22. git clone ssh://git@code.silentmode.st:2222/<you>/<repo>.git
6. Personal access tokens
Settings โ Applications โ Generate New Token. Pick the scopes you actually need โ the token that pushes commits does not need write:organization.
| Scope | What it does |
|---|---|
read:repository, write:repository | Clone / push git over HTTPS, use the repo API |
read:package, write:package | Docker login + push/pull on the container registry |
write:organization | Create repos under an org you belong to |
read:user, write:user | List and revoke your own tokens via API |
7. Container registry (OCI)
Forgejo speaks the standard OCI Distribution API at code.silentmode.st/v2/. Docker + skopeo + buildah + Kubernetes all work as-is.
Log in
docker login code.silentmode.st -u <your-username>
# password: a Personal Access Token with write:package scope
Push
docker build -t code.silentmode.st/<owner>/<image>:<tag> . docker push code.silentmode.st/<owner>/<image>:<tag>
Owner is a username or an org you belong to. Owner-org pushes need you to be an Owners team member; add via the admin API or the org settings page.
Pull
docker pull code.silentmode.st/<owner>/<image>:<tag>
Anonymous pull works for public repos. The /v2/ endpoint returns 401 to guide clients through the Bearer-challenge flow, but the token endpoint issues an anonymous token for public content. This means external systems like Flux can pull from Hephaestus without any credential setup.
List packages
curl https://code.silentmode.st/api/v1/packages/<owner>?type=container
Worked example: publish an app's backend as an image
Any backend that runs in a container publishes the same way. Node example โ Dockerfile at the repo root:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
Build + push in one pass:
docker login code.silentmode.st -u <you>
# password: PAT with write:package scope (see ยง6)
docker build -t code.silentmode.st/<owner>/<app>:latest .
docker push code.silentmode.st/<owner>/<app>:latest
The image is now pullable at code.silentmode.st/<owner>/<app>:latest. If the repo is public, Flux + friends can pull it anonymously (ยง7 above). Verify it in /-/packages or via the list-packages API.
8. API basics
Full Swagger at code.silentmode.st/api/swagger. Auth is a token in a header:
curl -H "Authorization: token $TOKEN" https://code.silentmode.st/api/v1/user
# or a Bearer if you prefer:
curl -H "Authorization: Bearer $TOKEN" https://code.silentmode.st/api/v1/user
A few endpoints you'll want early:
| Endpoint | What |
|---|---|
GET /api/v1/user | Whoami โ verify token works |
POST /api/v1/user/repos | Create a repo under yourself |
POST /api/v1/orgs/<org>/repos | Create a repo under an org |
PATCH /api/v1/repos/<o>/<r> | Change description / website / private flag |
DELETE /api/v1/repos/<o>/<r> | Delete a repo (destructive, no undo) |
POST /api/v1/repos/<o>/<r>/branches | Copy a branch (rename via copy + delete) |
Content-Type: application/json; charset=utf-8 AND a properly UTF-8-encoded body. Shell interpolation on Windows Git Bash silently CP1252-encodes and the char lands as ๏ฟฝ. If that happens, PATCH again with a Python urllib caller or plain curl --data-binary @body.json.9. Where files live
- git repos
- VPS SSD at
/data/git/repositories/<owner>/<repo>.gitinside the Forgejo container - Postgres
- SSD at
/var/lib/postgresql/dataโ issues, PRs, users, sessions, tokens - LFS objects
- Sia via S3 at
s3.silentmode.st:8600, buckethephaestus-lfs - Release attachments, packages
- Same Sia bucket, different key prefixes
- Container images
- Same Sia bucket, prefix
packages/container/ - Nightly backup
resticencrypts the whole tree + Postgres dump to a separate Sia bucket, retained 30 days- DR mirror
- Static landing (this page) also mirrored to Sia bucket
bns/hephaestus.x/, updated on redeploy
10. Self-hosting
You can run your own Hephaestus on any Linux box with Docker. Nothing about it depends on Silent Mode's infrastructure โ the operator's role is just to own the domain and hold the wallet secrets.
# 1. clone git clone https://code.silentmode.st/silentmode/hephaestus.git cd hephaestus # 2. configure โ copy .env.example, fill in your domain, Sia S3 creds, OIDC secret cp .env.example .env $EDITOR .env # 3. run docker compose up -d # 4. first wallet signs in and takes the admin seat open https://your-domain.example/user/oauth2/hephaestus-wallet
Full deploy notes: README, and PROTOCOL.md for the wire format.
11. Known gotchas
- Push over HTTPS asks for a password. That password is a Personal Access Token, not your wallet passphrase. Wallet passphrases decrypt the wallet in your browser's
localStorage; they have nothing to do with Forgejo's git auth. See ยง5 for the git-remote URL and ยง6 for how to generate the PAT. - Descriptions show as
๏ฟฝwhen shell โ curl โ API re-encodes UTF-8 as CP1252 (Windows Git Bash is the usual culprit). Use a PythonurllibPATCH orcurl --data-binary @file. Full recipe with the correctContent-Typeheader is in ยง8 โ "Encoding gotcha". - Signature mismatch on
/auth/verifyalmost always means your message string got a trailing newline (Pythonprint, Bash heredoc). Write the message to a file withnewline=''and hash the file bytes directly. - OIDC callback URL uses port 3000 if Forgejo's
ROOT_URLis unset. Setting[server].ROOT_URL = https://code.silentmode.st/inapp.inifixes it; browsers can't reach internal port 3000. - Cross-origin session cookies. Starting OAuth on hephaestus.x and finishing on code.silentmode.st loses the session cookie mid-flow. The landing dropdown always kicks off on code.silentmode.st to avoid this.
- Renaming a user via admin API takes
new_nameas a form field, not JSON. JSON returns 422 "NewName required". Full command in ยง4 โ "From the admin API". - SSH port is 2222, not 22, so the compose stack doesn't fight the host's sshd. Your git remote URL must include
:2222. - Docker Hub rate limits can bite
docker compose buildif you're unauthenticated. Log in once withdocker loginagainst Docker Hub before running the stack build.
12. Help + source
- Source: silentmode/hephaestus (MIT)
- Wire-format spec: PROTOCOL.md
- Auth-proxy verify reference: auth-proxy/src/verify.ts
- Silent Mode: silentmode.st
- Get a
.xname for your fork: sirius.x