# Hearthforge ![](web/static/assets/favicon.svg) ![](preview.png) A self-hosted git forge designed to host your own repositories while allowing others to interact with them via issues and patch proposals. The frontend works without JavaScript — JS is only required for WebAuthn, with a graceful fallback to password-only auth. ## Features - **Repository browser** — integrated file tree, single-file editing, blob view, commit log, markdown rendering, media previews - **Issues & patches** — create, comment, react with emoji; submit git `.patch` files for review and merge them directly from the UI - **Labels** — per-repo labels with custom colors; optionally allow users to label their own issues and patches - **Templates** — issue and patch templates per repository - **Releases** — create releases with source archives (zip/tar.gz), uploaded assets, and optional tag creation - **SSH push/pull** — built-in SSH server, no external git daemon needed - **Auth** — password login or passkeys (WebAuthn/FIDO2) - **Commit signing** — patches merged and files edited through the UI are automatically signed; verification badges shown in the commit log - **Registration control** — open registration, disabled, or queue mode where the admin manually approves new accounts ## Stack - [Go](https://go.dev) — single static binary, no runtime dependencies besides `git` and `ssh-keygen` - [chi](https://github.com/go-chi/chi) — HTTP router - [SQLite](https://www.sqlite.org) — single-file database via [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) (pure Go, no cgo) - [gomponents](https://www.gomponents.com) — server-side HTML in Go (no client-side framework) - [chroma](https://github.com/alecthomas/chroma) — syntax highlighting, in-process - [goldmark](https://github.com/yuin/goldmark) + [bluemonday](https://github.com/microcosm-cc/bluemonday) — Markdown rendering and sanitizing - [gliderlabs/ssh](https://github.com/gliderlabs/ssh) — embedded SSH server - [go-webauthn](https://github.com/go-webauthn/webauthn) — passkeys ## Running ```bash go build -o hearthforge ./cmd/hearthforge ./hearthforge init # creates database and admin account ./hearthforge # http://localhost:3000, SSH on port 2222 ``` Default admin credentials: `admin` / `changeme` Set the `ADMIN_PASSWORD` environment variable **before** running `db:init` to choose your own. ### Docker / Podman A `Containerfile` and `compose.yml` are provided: ```bash docker compose up # or podman compose up ``` The container stores all persistent data (repos, database, avatars, releases, SSH host key) under `/data` — mount a volume there to keep it across restarts. ### Manual repository import Existing repositories can be copied into the `data/repos` directory. Non-bare repos are automatically converted to bare repos on startup (uncommitted changes and worktrees are discarded). Pushing directly to the on-disk repositories (bypassing the bundled HTTP/SSH endpoints) generally works as well. ### SSH access Add your public key under **Settings → SSH keys**, then: ``` git clone ssh://git@localhost:2222/REPO_NAME ``` Pushing is supported for the admin. ### Configuration All settings are environment variables: | Variable | Default | Description | |------------------------------|----------------------------------|-------------------------------------------------------------| | `PORT` | `3000` | HTTP port | | `SSH_PORT` | `2222` | SSH port | | `DATA_DIR` | `./data` | Repos, database, uploads | | `ADMIN_PASSWORD` | `changeme` | Initial admin password (only used by `init`) | | `OWNER_DISPLAY_NAME` | `Admin` | Display name for the owner | | `BASE_URL` | `http://localhost:$PORT` | Used in clone URLs and links | | `REGISTRATION_TYPE` | `enabled` | `enabled`, `disabled`, or `queue` (admin approval) | | `REGISTER_QUESTION` | _(empty)_ | Question shown on the registration form in `queue` mode | | `MAX_UPLOAD_BYTES` | `10485760` | Max request body size (10 MB), not applied to `git push` | | `MAX_USER_UPLOAD_BYTES` | `2097152` | Max upload size for non-admin users (2 MB) | | `INLINE_MAX_BYTES` | `524288` | Max file size rendered inline in the code view (512 KB) | | `SSH_DISABLED` | `0` | Set to `1` to disable the embedded SSH server | | `SCANNED_REPO_PRIVATE` | `1` | Set to `0` to make auto-scanned repos public by default | | `TRUSTED_PROXY` | `0` | Trust `X-Forwarded-For` headers \* | | `RATE_LIMIT_DISABLED` | `0` | Set to `1` to disable rate limiting | | `COMMITTER_NAME` | `$OWNER_DISPLAY_NAME` | Git committer name for merges and UI edits | | `COMMITTER_EMAIL` | `$OWNER_DISPLAY_NAME@` | Git committer email for merges and UI edits | | `SSH_HOST_KEY_PATH` | `$DATA_DIR/ssh_host_key` | Path to the SSH host key (auto-generated if missing) | | `EXTRA_ALLOWED_SIGNERS_PATH` | _(empty)_ | Additional git allowed-signers file for commit verification | | `MAX_TITLE_BYTES` | `500` | Max length for titles (issues, patches, releases) | | `MAX_TEXT_BODY_BYTES` | `100000` | Max length for text bodies (descriptions, comments, notes) | | `MAX_USERNAME_BYTES` | `64` | Max username length at registration | | `MAX_PASSWORD_BYTES` | `1024` | Max password length | | `MAX_RENDER_BYTES` | `10485760` | Max size for any inline render (blob, diff, markdown) | | `MAX_RAW_DOWNLOAD_BYTES` | `0` | Max streamed `/raw` size, `0` = unlimited | | `MAX_CONCURRENT_ARCHIVE_JOBS`| `2` | Parallel source-archive jobs | | `CI_DOCKER_SOCKET` | _(auto-detected)_ | Path to Docker/Podman socket | | `CI_MAX_HISTORY` | `50` | Max pipeline runs to keep per repo | | `CI_MAX_ARTIFACT_BYTES` | `536870912` | Max size of one published CI artifact (512 MiB) | | `CI_DEFAULT_TIMEOUT` | `3600` | Default step timeout in seconds | | `CI_MAX_CONCURRENT` | `2` | Advisory max concurrent runs | | `CI_ENGINE_SOCKET` | `0` | Allow CI steps with `engine_socket = true` to use the engine socket | | `CI_NETWORK` | _(engine default)_ | Engine network for CI containers, e.g. one created with IPv6 | | `REGISTRY_PULL` | `admin` | Who may pull container images: `admin`, `users`, or `public` | \* Set `TRUSTED_PROXY=1` only when Hearthforge is behind a reverse proxy that strips any incoming `X-Forwarded-For` from clients. Caddy and Traefik do this by default; nginx requires `proxy_set_header X-Forwarded-For $remote_addr;` (rather than the common `$proxy_add_x_forwarded_for`, which appends to a client-supplied value). Setting `TRUSTED_PROXY=1` in front of a proxy that does not strip means rate limits and any audit logging are spoofable per request. ### Reverse proxy deployment Hearthforge does not terminate TLS itself. For any production deployment, run it behind an HTTPS-terminating reverse proxy (Caddy, nginx, Traefik, …) and set: ``` BASE_URL=https://your-forge.example.com ``` A `BASE_URL` with the `https://` scheme is what activates HTTPS hardening: - Session and preference cookies are emitted with `Secure`, so the browser will only send them over HTTPS. - Every response includes `Strict-Transport-Security: max-age=31536000; includeSubDomains`. - The CSRF middleware compares the request `Origin` against `BASE_URL` (full origin, scheme + host + port), not the `Host` header. ## CI/CD Pipelines Hearthforge includes a built-in CI/CD system that runs pipelines in Docker or Podman containers, configured via a `.hearthforge-ci.toml` file at the root of your repository. The Pipelines tab contains a small tutorial and an example file. Setup, the run model, caches, and known pitfalls are documented in [CI.md](CI.md). ## Container registry Hearthforge serves an OCI container registry under `/v2/`. Image names map to repositories: `your-forge.example.com/REPO_NAME:tag` or `your-forge.example.com/REPO_NAME/sub-image:tag`. The first segment must be an existing repository. - Only the admin may push, with the admin password as HTTP Basic auth. `docker login your-forge.example.com` or `podman login` stores it. - `REGISTRY_PULL` decides who may pull from public repositories: `admin`, any signed-in user (`users`), or anyone (`public`). Images of private repositories are always admin-only. - Blobs are stored once by digest under `DATA_DIR/registry/`. Deleting a repository removes its image index. Blob files shared with other images stay. - `BASE_URL` must be HTTPS for Docker and Podman to talk to the registry without an insecure-registry exception. CI steps can build and push images through the engine socket, see [CI.md](CI.md#building-container-images). ## Development ```bash go build -o hearthforge ./cmd/hearthforge go test ./... # unit and end-to-end tests ``` The end-to-end suite lives in `internal/web/e2e/`. It starts the real server in-process on a throwaway data directory and drives it over HTTP. It needs `git` and `ssh-keygen`. The browser tests in that package (`TestBrowser*`) need a Chromium binary. They look at `HEARTHFORGE_BROWSER`, then `PATH`, then a Playwright download, and skip when none is found. Formatting and linting: ```bash go install mvdan.cc/gofumpt@latest go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest go install golang.org/x/vuln/cmd/govulncheck@latest gofumpt -l -extra . # formatter, stricter than gofmt golangci-lint run ./... # errcheck, staticcheck, gosec, bodyclose, noctx; config in .golangci.yml govulncheck ./... # known vulnerabilities in the toolchain and dependencies go test -race ./... ``` `.hearthforge-ci.toml` runs the same checks plus the E2E suite when the repository is hosted on a Hearthforge instance. ## Roadmap - Use [git-bug](https://github.com/git-bug/git-bug) for issue tracking instead of custom implementation