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.

v1.56.196 Zig 0.16 MIT

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

$ nix profile add 'https://kepr.uk/kepr/archive/HEAD.tar.gz#kepr'

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
keymeaningdefault
triggeron-offer, on-promote, on-tag, or manualon-promote
platformscomma-separated targetsper-language
commandbuild command (argv only, no shell)detected
outputartifact path, relative to repo rootdetected

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.

sectionwhat's there
dashboardinstance vitals, activity, per-repo traffic
reposmanagement & visibility
issues / ticketspublic issue triage & the internal tracker
reviewthe contribution queue
storage / keys / configoperator controls
themebrand colours — the whole UI re-themes live
email / log / recoverdelivery, 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.

keydefaultwhat it does
server.domainpublic domain
server.http_port7701HTTP listen port
server.ssh_port2222SSH listen port
storage.data_dir/var/lib/keprwhere everything lives
instance.publicfalsediscovery posture — browse index & registration open to the world
accounts.registration_opentruewhether /join accepts new accounts
retention.max_snapshots10saves kept per lane (0 = keep all)
retention.prune_on_offertrueprune automatically after each offer
contributions.accept_offersfalseaccept contributions to your repos
review.agent_enabledfalsepre-review contributions with a local LLM
review.inference_urllocal chat-completions endpoint
images.schedule"never"auto-image cadence (e.g. monthly)
images.keep3how many images to retain
operator.usernameyour handle for admin sign-in
smtp.*email for recovery, notifications, issue replies
rate_limit.*variesper-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."