| public | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | 478 B | |
| biome.json | 843 B | |
| bun.lock | 50.3 KB | |
| compose.yml | 465 B | |
| Containerfile | 868 B | |
| package.json | 1.6 KB | |
| preview.png | 97.3 KB | |
| README.md | 7.7 KB | |
| tsconfig.json | 752 B |
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 |
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.
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