backend / lorx

Lorx forge server

A single Rust process that hosts git repositories over smart HTTP and SSH and serves the REST API for change requests, review, merge and auto-merge, on SQLite or Postgres.

2026 · live · private repository

git → caddy: git smart HTTP / SSHweb → caddy: HTTPS JSON + relay keycaddy → server: reverse proxy :4040server → repos: git http-backend, merge-tree, update-ref CASrepos → hooks: pre/post-receivehooks → db: ref updates + outbox in one txndb → bus: outbox rows by seqbus → automerge: push / CI finished eventsbus → web: SSE, Last-Event-ID resumeserver → db: SeaORMserver → github: OAuth / RESTserver → smtp: lettregit / bit CLIgit / bit CLI (client) stock git, bit supersetNext.js web appNext.js web app (client) apps/site on VercelEvent bus + SSEEvent bus + SSE (queue) outbox tail, durable cursorsreceive hooksreceive hooks (worker) lorx hook over a Unix socketCaddyCaddy (edge) TLS, relay-key check, one X-Forwarded-ForMerge gate /auto-mergeMerge gate / auto-merge (worker) branch rules, CI, review enginelorx serve (axum)lorx serve (axum) (service) REST API, git smart HTTP, russh SSHGitHubGitHub (third party) OAuth, PR importSMTPSMTP (third party) verification, reset mailBare git reposBare git repos (store) local disk, refs/lorx/historySQLite / PostgresSQLite / Postgres (store) SeaORM, 24 migrationsgit smart HTTP / SSHHTTPS JSON + relay keyreverse proxy :4040git http-backend, mer…pre/post-receiveref updates + outbox …outbox rows by seqpush / CI finished ev…SSE, Last-Event-ID re…SeaORMOAuth / RESTlettre git → caddy: git smart HTTP / SSHweb → caddy: HTTPS JSON + relay keycaddy → server: reverse proxy :4040server → repos: git http-backend, merge-tree, update-ref CASrepos → hooks: pre/post-receivehooks → db: ref updates + outbox in one txndb → bus: outbox rows by seqbus → automerge: push / CI finished eventsbus → web: SSE, Last-Event-ID resumeserver → db: SeaORMserver → github: OAuth / RESTserver → smtp: lettregit / bit CLIgit / bit CLI (client) stock git, bit supersetNext.js web appNext.js web app (client) apps/site on VercelEvent bus + SSEEvent bus + SSE (queue) outbox tail, durable cursorsreceive hooksreceive hooks (worker) lorx hook over a Unix socketCaddyCaddy (edge) TLS, relay-key check, one X-Forwarded-ForMerge gate /auto-mergeMerge gate / auto-merge (worker) branch rules, CI, review enginelorx serve (axum)lorx serve (axum) (service) REST API, git smart HTTP, russh SSHGitHubGitHub (third party) OAuth, PR importSMTPSMTP (third party) verification, reset mailBare git reposBare git repos (store) local disk, refs/lorx/historySQLite / PostgresSQLite / Postgres (store) SeaORM, 24 migrationsgit smart HTTP / SSHHTTPS JSON + relay keyreverse proxy :4040git http-backend, mer…pre/post-receiveref updates + outbox …outbox rows by seqpush / CI finished ev…SSE, Last-Event-ID re…SeaORMOAuth / RESTlettre
clientedgeserviceworkerqueuestorethird partyasync
  • [*]LanguagesRust · JavaScript · Shell
  • [*]Frameworksaxum · tower-http · SeaORM · utoipa · russh · gix · tokio
  • [*]StoresSQLite · Postgres · git bare repositories on local disk
  • [*]AsyncTransactional outbox with durable consumer cursors · Server-Sent Events · Periodic sweeps (orphaned git processes, push reconciliation)
  • [*]InfraGCP Compute Engine · Caddy · systemd · Cloudflare Workers (relay) · Cloudflare KV · Vercel (web app) · GitHub Actions
  • [*]Third partiesGitHub OAuth and REST (sign-in, pull request import) · SMTP (lettre) · OpenTelemetry OTLP

lorx serve is an axum server that hosts ordinary bare git repositories. Clone, fetch and push work over smart HTTP (v0 and v2, streamed through git http-backend) and over an embedded russh SSH server that can only run git. The same process serves 108 REST operations across 92 paths under /api/v1, all described in a committed OpenAPI document that CI checks against the handlers and the generated TypeScript types.

Every push runs git's receive hooks, which call back into the server over a Unix socket. pre-receive runs while the pushed objects are still in git's quarantine and accepts or refuses the whole push. post-receive records what was applied and writes the events into a database outbox in the same transaction. Before a force-push or deletion moves a head, the old head is kept under refs/lorx/history/ so garbage collection cannot drop it.

Change requests merge with no working tree. git merge-tree writes into a throwaway object directory, and the base ref only moves through a compare-and-swap update-ref, so a push that lands at the same moment wins. A merge gate built from branch rules (CI passed on the head, the in-house review engine finding nothing at or above a set severity) blocks the merge endpoint. Auto-merge is armed per request and fires on the server, as the person who armed it, with their access checked at the moment it fires.

The workspace has 350 commits from 2026-09-22 to 2026-10-07 and 90,551 lines of Rust across 8 crates. Of those, 48,596 are in lorx-server and 12,724 in lorx-db (24 SeaORM migrations, 35 entities). It runs on one e2-small GCP VM behind Caddy, on SQLite. The live health endpoint reports backend sqlite.

How it holds up

Auth
Argon2 passwords with web sessions and CSRF tokens, scoped access tokens for git and the API (Basic or Bearer, never cookies on git routes), an OAuth-style device flow for the bit CLI, GitHub OAuth linking, step-up re-authentication for sensitive actions, signed known-device cookies that exempt a browser from account lockout, and a central permit() check that answers 401 or 404 so a private repository's existence is never revealed.
Data
35 SeaORM entities over 24 migrations that run on both SQLite and Postgres: users, identities, sessions, tokens, device authorizations, repos, branch rules, ref updates, change requests with revisions, reviews, comments and events, a commit index and code cache, the events outbox with event_consumers cursors, and audit events.
Consistency
Events go into the outbox in the same transaction as the change they report, and outbox writers are serialised so seq commits in order. On SQLite every write transaction takes the writer lock with its first statement, so WAL snapshot conflicts wait out the busy timeout instead of failing. Merges move refs only by compare-and-swap. Auto-merge holds a per-request mutex from reading the arming to the end of the merge, and coalesces results that arrive while an attempt is running.
Failure
Every git child asks the kernel for SIGKILL when the server dies, and a sweep every minute kills descendants whose owning server (identified by pid, start time and pid namespace) is gone. Ref updates that post-receive never reported are reconciled against the repository's actual refs. API routes have a request timeout and a panic catcher that returns a JSON error. Auto-merge waits on conditions that can clear by themselves and disarms, with the reason on the timeline, on ones that cannot.
Testing
33 integration test files in crates/lorx-server/tests (28 for the forge itself, 5 for CI) drive real git over HTTP and SSH, SMTP and the device flow. There are 624 #[test]/#[tokio::test] cases across the workspace, 74 of them in lorx-review with a flag/pass corpus. A migrations test proves every down migration works. GitHub Actions runs the whole suite on both SQLite and Postgres.

Techniques

  1. Transactional outbox with durable, resumable deliveryEvents are written in the same transaction as the change they describe, so a crash can neither lose one nor invent one. Each consumer keeps a durable cursor and acknowledges by seq. SSE clients resume from Last-Event-ID out of the same outbox, so they miss nothing and get nothing twice.crates/lorx-server/src/events.rs:3 crates/lorx-server/src/events.rs:51 crates/lorx-db/src/store.rs:161 crates/lorx-server/src/live.rs:3
  2. All-or-nothing push policy inside git's quarantinepre-receive judges the pushed objects before they enter the repository and accepts or refuses the whole push, giving a reason for each refused ref. post-receive applies the result. Heads displaced by a force-push or deletion are kept under refs/lorx/history/ so they can be restored, and pushes that were never reported are reconciled against the real refs.crates/lorx-server/src/hooks.rs:10 crates/lorx-server/src/hooks.rs:54 crates/lorx-server/src/hooks.rs:595
  3. Worktree-free merges with a compare-and-swap ref moveMergeability and conflict checks run git merge-tree against a temporary object directory that uses the repository's objects as alternates, so the check leaves nothing behind. The base ref moves only through update-ref with the expected old value, so a push that races the merge wins.crates/lorx-server/src/git.rs:168 crates/lorx-server/src/git.rs:312 crates/lorx-server/src/api/cr_merge.rs:182
  4. Merge gate and server-side auto-mergeBranch rules are combined into the strictest requirement: CI passed on the head, and the review engine finding nothing at or above a set severity. An armed change request merges itself on the server as the person who armed it, with their access checked when it fires. A per-request lock makes a disarm either land before the merge starts or find it already merged.crates/lorx-server/src/merge_gate.rs:1 crates/lorx-server/src/merge_gate.rs:46 crates/lorx-server/src/auto_merge.rs:1 crates/lorx-server/src/auto_merge.rs:37
  5. Reaping orphaned git processesEvery git child sets PR_SET_PDEATHSIG, which is the only unsafe block in the workspace. Each child also carries an owner marker (pid, start time, pid namespace) that its descendants inherit. A sweep every minute kills processes whose owner is gone and never touches another user's or another namespace's processes.crates/lorx-server/src/git.rs:46 crates/lorx-server/src/reaper.rs:1
  6. Rate limiting that a flood cannot evictFixed-window limits are kept in memory, one capped map per namespace. When a namespace fills, it evicts expired windows first and then the lowest-count ones, and it never evicts a window that has reached half its limit. That keeps a pending lockout alive through a key-flooding attack. Sign-in failures for unknown users verify against a dummy Argon2 hash to equalise timing, and known-device cookies stop an attacker from locking the real owner out.crates/lorx-server/src/auth/ratelimit.rs:12 crates/lorx-server/src/auth/ratelimit.rs:157 crates/lorx-server/src/auth/password.rs:41 crates/lorx-server/src/auth/device.rs:1

Counted

  • 350Commits
    git rev-list --count HEAD
  • 90,551Rust LOC (workspace)
    git ls-files '*.rs' | xargs wc -l | tail -1
  • 48,596lorx-server LOC
    git ls-files crates/lorx-server | grep .rs$ | xargs cat | wc -l
  • 12,724lorx-db LOC
    same, crates/lorx-db
  • 108 / 92REST operations / paths
    crates/lorx-server/openapi.json, counted with python json
  • 24Migrations
    ls crates/lorx-db/src/migration/m*.rs
  • 35Entities (tables)
    ls crates/lorx-db/src/entity | grep -v mod.rs | wc -l
  • 33Server integration test files
    git ls-files crates/lorx-server/tests | grep .rs$
  • 624Test cases (workspace)
    grep -cE '#\[(tokio::)?test' over git ls-files '*.rs', corpus excluded
  • 11Active days
    git log --format=%ad --date=short | sort -u | wc -l