Hearthforge 

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
.patchfiles 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
- Bun — runtime and package manager
- ElysiaJS — HTTP framework
- SQLite — single-file database via Kysely
- @kitajs/html — server-side JSX (no client-side framework)
- Shiki — syntax highlighting
Running
bun install
bun run db:init # creates database and admin account
bun run start # 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:
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 db: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) |
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 |
HIGHLIGHT_WORKERS |
4 |
Syntax highlighting worker threads * |
COMMITTER_NAME |
$OWNER_DISPLAY_NAME |
Git committer name for merges and UI edits |
COMMITTER_EMAIL |
$OWNER_DISPLAY_NAME@<hostname> |
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 |
CI_DOCKER_SOCKET |
(auto-detected) | Path to Docker/Podman socket |
CI_MAX_HISTORY |
50 |
Max pipeline runs to keep per repo |
CI_DEFAULT_TIMEOUT |
3600 |
Default step timeout in seconds |
CI_MAX_CONCURRENT |
2 |
Advisory max concurrent runs |
* Each highlighting worker loads its own copy of the language grammars and uses ~200 MB of memory. Increase with care.
** 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
OriginagainstBASE_URL(full origin, scheme + host + port), not theHostheader.
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.
Development
bun run dev # watch mode with auto-restart
bun run lint # lint with Biome
bun run format # format with Biome
bun run test # E2E and unit tests (uses Playwright; don't call bun test directly)
Roadmap
- Use git-bug for issue tracking instead of custom implementation
- Remove test retry logic once Bun no longer randomly stalls