documentation
everything it does
A calm reference for your own source forge. Find what you need, take your time — you don't have to read it all to get started.
getting started
what is kepr?
Kepr is a self-hosted source forge for your Koh repositories — a web home, a release pipeline, and a full snapshot history for every project, running on a machine you own.
It runs as a small daemon on a homelab server, a VPS, or a spare laptop. You offer a save from Koh and it's stored; you steal it back into a working repo from anywhere. Public repos are there to share — browse, clone, download releases; private repos are for everything else, invisible to anyone without access. Both live on one instance, and visibility is a per-repo choice you flip any time.
Kepr is built in Zig. A single binary, PostgreSQL underneath, your data in plain JSON manifests on disk. Nothing is ever lost: lose the database and one command rebuilds it from the manifests alone.
Kepr never reaches outward. No telemetry, no external services, no third-party runners. The instance you run is the whole system — what it does is what you can read in the source.
installation
Two ways to install the Kepr daemon.
curl (simplest)
$ curl -fsSL kepr.uk/install.sh | sh
Downloads the binary, installs to ~/.local/bin, prints next steps.
nix
On NixOS, use the module instead — see the NixOS section. No kepr init needed there; the module handles setup declaratively.
Kepr needs PostgreSQL available on the host. kepr init sets up the connection for you and most operators never touch it — but if you're on a minimal box, install Postgres first.
first run
Three commands from installed to running, then connect Koh.
kepr init
$ kepr init
Guided setup. Prompts for your domain, data directory, ports (HTTP defaults to 7701, SSH to 2222), and your first operator SSH key. Writes kepr.toml. Generates a recovery phrase — shown once, store it somewhere safe. Writes the supervisor integration files for your init system.
kepr start
$ kepr start :: kepr · kepr.local · running on :7701
Starts the daemon. Visit your domain to confirm it's live.
connect Koh
$ koh login kepr.local :: registering this machine with kepr.local ✓ key enrolled · you're [email protected]
Registers this machine's SSH key with your account. Run it once per machine — one account holds many keys.
offer your first save
$ koh offer ✓ offered [1f958bc] to kepr.local/myproject · 247 objects sent
Your snapshot is stored. Lose your machine, steal it back from anywhere.
New machine later? koh login adds its key too. Lost every machine? koh login --recover uses your recovery phrase plus an emailed code to enroll a fresh one. Your work continues.
core concepts
Five ideas, and you understand the whole system.
faces
A face is a Koh project tracked by Kepr — its save history, its rolling window, its web page. Offering or stealing a project makes it a face on that instance.
offers & steals
An offer pushes a save from Koh to Kepr; a steal pulls one back into a working repo. Offers are two-phase: Koh first asks which objects Kepr is missing, then sends only those. Nothing is transferred twice.
objects
Every file is a zlib-compressed blob keyed by its BLAKE3 hash. Identical files across saves are stored once — deduplication is automatic and exact. Files under 512 bytes are inlined in the manifest.
manifests
Every save is a JSON manifest on disk — the source of truth. The database is a derived index. If it's ever lost, kepr recover rebuilds it from the manifests alone. No save is lost unless its manifest is gone too.
the rolling window
Each face keeps a rolling window of saves per lane (main and dev, independently). When the window fills, the oldest save is pruned; shared objects survive until the last save referencing them is gone. Set max_snapshots = 0 to keep everything forever.
using kepr
offer & steal
The two verbs you'll use most. Both run from Koh, against any Kepr remote.
koh offer
koh offer [<url>] [--private] [--public] [--builds] [--local]
Push the current save to a remote. With no URL, offers to the repo's default remote, using the folder name as the repo name. --local writes a portable .face bundle to disk instead of pushing.
$ koh offer ✓ offered [a3f9c2] to kepr.uk/myproject · 12 objects sent $ koh offer --private # offer and make the repo private $ koh offer kepr.uk/other # offer to a specific remote $ koh offer --local # write myproject-a3f9c2.face
koh steal
koh steal <url>[@<tag|id>]
Initialize a local repo from a Kepr snapshot. Records the source as the origin remote in .koh/remotes, so later offers push back there.
$ koh steal kepr.uk/myproject $ koh steal kepr.uk/[email protected] # a tagged save $ koh steal kepr.uk/myproject@a3f9c2 # a specific save
koh apply
koh apply <id | file.face>
Apply a save by ID, or from a portable .face file someone shared with you.
public & private
Each repo is public or private. Public repos are browseable and cloneable by anyone. Private ones are invisible unless you're signed in.
koh view
koh view <public | private>
Change a repo's visibility. Going public asks for confirmation.
$ koh view private ✓ kepr.uk/myproject is now private $ koh offer --private # or flip it as part of an offer
A private repo isn't just locked — it's invisible. It won't appear in the browse index, won't confirm it exists to anyone without read access, and its traffic stays owner-only. Public and private repos coexist on one instance with no leakage between them.
accounts & login
Identity on Kepr is account-based. One account holds many machine keys and, optionally, an email for recovery and notifications.
koh login
koh login [--recover] <host>
Enroll this machine's key against your account. Run once per machine. --recover restores access after losing every key — recovery phrase plus an emailed confirmation code enrolls a fresh one.
koh key · koh whoami
$ koh key list # keys enrolled on this account $ koh key remove <fp> # revoke a key $ koh whoami # which account this machine is
Operators sign in to the admin panel with a password and authenticator code. Set operator.username in kepr.toml to your handle, then set a password from the admin panel. Recovery phrases are stored Argon2id-hashed; the recovery flow is rate-limited and always notifies the account.
builds & releases
Kepr serves release binaries — but it never compiles your code. Builds run on your machine: Koh cross-compiles locally and uploads the finished artifacts, and the instance stores and serves them.
the kepr.build file
Drop a kepr.build file at your repo root to opt in. An empty file is enough — Koh detects your language and infers a sensible command, output path, and platform set. Override only the lines you want to change.
# kepr.build trigger = on-offer platforms = linux-aarch64, linux-x86_64, macos-aarch64, macos-x86_64 command = zig build -Doptimize=ReleaseSafe output = zig-out/bin/mytool
| key | meaning | default |
|---|---|---|
| trigger | on-offer, on-promote, on-tag, or manual | on-promote |
| platforms | comma-separated targets | per-language |
| command | build command (argv only, no shell) | detected |
| output | artifact path, relative to repo root | detected |
koh offer --builds
$ echo "1.2.3" > VERSION $ koh save "bump to 1.2.3" $ koh offer kepr.uk/myapp --builds ✓ built 4 platforms · uploaded to kepr.uk/myapp/releases/1.2.3
Each platform builds with KEPR_TARGET set to the cross-compile triple, so one zig build or go build emits Linux, macOS, and Windows from a single host. Artifacts are served at stable URLs:
GET /api/<repo>/releases/latest # version, notes, platform map GET /api/<repo>/releases/<version>/<platform> # binary GET /<repo>/releases/<version>/checksums.txt # checksums
The one-line curl … | sh install experience is your own install.sh, served raw from /<repo>/install.sh. It reads /api/<repo>/releases/latest, picks the platform binary, and drops it in place. Kepr serves the binaries; you own the install script.
contributions
With contributions.accept_offers = true, other accounts can offer saves to your repos. Incoming offers land in quarantine pending your review — nothing merges until you accept.
$ kepr offers pending $ kepr offer show a3f9c2 $ kepr offer accept a3f9c2 $ kepr offer decline a3f9c2 --reason "out of scope"
Or from the admin panel at /admin/review. Accepting moves objects from quarantine to the main store and writes the manifest; declining discards them without a trace. Every pending offer carries an automated trust assessment.
Optionally, Kepr can pre-review contributions with a local language model — set review.agent_enabled and point review.inference_url at any local chat-completions endpoint. Your code never leaves your network, and the agent only advises; you always decide, and accepting requires viewing every changed file.
operating
lifecycle
kepr start kepr stop [--timeout <s>] kepr restart
start launches the daemon (via systemd when available, else foreground). stop drains in-flight requests first — default 30s. restart drains, stops, starts. Kepr handles its own signals: SIGHUP reloads live config, SIGTERM/SIGINT drain and exit, SIGUSR2 triggers storage GC.
health & status
kepr status
$ kepr status kepr · kepr.local · v1.56.196 domain kepr.local uptime 4 days 3 hours repos 14 storage 2.1 GB / 50 GB (4%) kepr ● running last offer 2 hours ago
kepr ping
$ kepr ping kepr · kepr.local · ok (12ms)
Liveness check. Exits 0 if healthy, non-zero if not — safe for monitoring scripts.
kepr doctor
$ kepr doctor :: diagnosing kepr.local data directory ok database ok on-disk layout ok operator keys ok (1) offer tokens ok :: all good
Read-only diagnostic over the data directory. Checks the directory opens, the database connects, the layout (manifests/, objects/, quarantine/) is intact, at least one operator key exists, and no offer tokens are stuck. Exits non-zero on any failure.
kepr logs
$ kepr logs --since 1h 14:23:01 offer myproject a3f9c2 june@library 14:18:44 steal myproject a3f9c2 anonymous
repos & storage
kepr repos · kepr repo
kepr repos # list all kepr repo show <name> kepr repo rename <name> <new> # atomic kepr repo remove <name> # irreversible, confirms by name
kepr prune
kepr prune <name> | --all | --dry-run
Trim a repo to its rolling window. Runs automatically after every offer when retention.prune_on_offer is true (the default), so manual pruning is rarely needed.
kepr storage
kepr storage status # usage breakdown kepr storage gc # remove orphaned objects kepr storage verify [--face] # rehash every object kepr storage trend # 30-day usage projection
backup & recover
kepr backup · kepr restore
kepr backup [--output <path>] kepr restore <backup.tar.gz>
restore validates integrity first, refuses to run while serving unless --force, confirms by typing the domain, and takes a pre-restore backup it auto-rolls-back to if anything fails.
kepr recover
kepr recover [--verify] [--dry-run]
Rebuilds the database entirely from the manifest files. Every repo, save, and object reference reconstructed. This is the safety net under everything — the manifests are the truth, the database is just an index.
kepr image
$ kepr image wrote kepr-instance-2026-06-24.tar.gz (4.1 GB) checksum: blake3:a3f9c2… 14 repos · 247 saves · 12,847 objects
A complete portable snapshot — data, config, and the exact binary that produced it. The checksum is BLAKE3 over every manifest and object, verifiable independently. The bundled README.txt has plain-text restore steps, readable on any machine in any future. Schedule with the [images] config block.
Three layers of safety, by design: manifests on disk survive a database loss; backups survive a disk loss; instance images survive a machine loss. Nothing about Kepr asks you to trust that data won't disappear — it's structured so it can't.
admin panel
The admin interface lives at /admin, a native section of the site. Operators sign in with their password; team members reach the shared sections through their own accounts.
| section | what's there |
|---|---|
| dashboard | instance vitals, activity, per-repo traffic |
| repos | management & visibility |
| issues / tickets | public issue triage & the internal tracker |
| review | the contribution queue |
| storage / keys / config | operator controls |
| theme | brand colours — the whole UI re-themes live |
| email / log / recover | delivery, activity, recovery |
Traffic is anonymous by design — per-repo counts of steals and downloads, no per-visitor tracking, no IP logging. You see how your work travels without watching anyone.
reference
configuration
Kepr is configured via kepr.toml, written by kepr init. kepr config set <key> <value> validates before writing and tells you whether the change is live or needs a restart. Most apply live; server.*, storage.data_dir, and instance.public require a restart.
| key | default | what it does |
|---|---|---|
| server.domain | — | public domain |
| server.http_port | 7701 | HTTP listen port |
| server.ssh_port | 2222 | SSH listen port |
| storage.data_dir | /var/lib/kepr | where everything lives |
| instance.public | false | discovery posture — browse index & registration open to the world |
| accounts.registration_open | true | whether /join accepts new accounts |
| retention.max_snapshots | 10 | saves kept per lane (0 = keep all) |
| retention.prune_on_offer | true | prune automatically after each offer |
| contributions.accept_offers | false | accept contributions to your repos |
| review.agent_enabled | false | pre-review contributions with a local LLM |
| review.inference_url | — | local chat-completions endpoint |
| images.schedule | "never" | auto-image cadence (e.g. monthly) |
| images.keep | 3 | how many images to retain |
| operator.username | — | your handle for admin sign-in |
| smtp.* | — | email for recovery, notifications, issue replies |
| rate_limit.* | varies | per-IP / per-key request ceilings |
| postgres.* / object_storage.* | — | database & optional S3 object storage |
email (SMTP)
Kepr sends mail for account recovery, notifications, and replies to issue reporters. Configure one [smtp] block; leave it out and those degrade gracefully (recovery needs email, so accounts without it can't recover by phrase).
[smtp] enabled = true host = "smtp.example.com" port = 587 # 587 = STARTTLS, 465 = TLS tls = "starttls" username = "[email protected]" password = "your-app-password" # an app password, never your login from = "Kepr <[email protected]>"
Use an app password, or point password_file at a file to keep it out of the config. Test from the admin Config page; failures print the exact SMTP error in kepr logs.
data directory
data_dir/
manifests/ SOURCE OF TRUTH — one JSON per save
{repo}/{id}.json
objects/ content-addressed, BLAKE3-keyed, zlib-compressed
{hh}/{rest}
.tmp/ in-flight writes
quarantine/ pending contribution objects
releases/ built release artifacts
images/ instance images
logs/kepr.log structured JSON log
manifests/ is the truth. Repo metadata and indexes live in PostgreSQL, fully rebuildable with kepr recover. Object content can live on local disk or in S3-compatible storage when [object_storage] is configured.
nixos module
Add the Kepr flake input, import the module, configure with Nix options. No kepr init, no imperative setup — the module manages the service, user, data directory, firewall, and Caddy virtual host.
services.kepr = {
enable = true;
domain = "kepr.local";
public = false; # discovery posture
maxSnapshots = 10;
authorizedKeys = [ "ssh-ed25519 AAAA… june@library" ];
};
services.kepr.images = { schedule = "monthly"; keep = 3; };
authorizedKeys is the source of truth for operator keys — add one and nixos-rebuild switch applies it immediately. Account keys registered through the web are stored separately and untouched by rebuilds.
recipes
A few patterns that make daily life with your forge smoother.
a private home for your work
# offer everything private by default $ koh offer --private # steal it back on any machine $ koh steal kepr.uk/myproject
Your own rolling backup of every project, on infrastructure you own, invisible to the world.
ship a release
$ echo "1.4.0" > VERSION $ koh save "release 1.4.0" $ koh offer kepr.uk/mytool --builds # users install with: $ curl -fsSL kepr.uk/mytool/install.sh | sh
recover after losing a machine
$ koh login --recover kepr.uk # enter recovery phrase, then the emailed code $ koh steal kepr.uk/myproject # you're back, working tree and all
accept contributions safely
$ kepr config set contributions.accept_offers true # offers land in quarantine; review before anything merges $ kepr offers pending $ kepr offer accept <id>
a monthly safety net
$ kepr config set images.schedule monthly # a full portable snapshot, kept automatically — # survives a database loss, a disk loss, a machine loss
When something goes wrong, the manifests are always there. The database can be rebuilt, a backup restored, an image unpacked on a fresh machine. Kepr is built so the answer to "what if I lose this?" is always "you don't."