hotlane

module
v0.7.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT

README

hotlane - a flame with a lane through it

hotlane

Validation-first deployment.
Push a change, get a verified running fork of your app in about a second.
Roll back by pointer. Images build in the background.

Release npm PyPI MIT license

Website · Docs · CI integration · Benchmark · Roadmap

Why

Classical CI/CD treats every change as a cold-start artifact production problem. Push a one-line fix and the pipeline rebuilds the world: cold clone, cold caches, full image build, registry push, registry pull, scheduler round-trip. The change was 40 bytes; the pipeline moved gigabytes - for eight minutes. Rollback usually means running that same pipeline again.

hotlane inverts the model:

  1. Validation + serving is a delta operation on a warm, running system.
  2. Artifact production (reproducible image, audit trail) still happens - asynchronously, after the change is already verified and live.

The deploy unit is a verified running fork, not an image.

How it works

push (delta)                          ~0.1s
└─ fork warm instance                 ~0.2s
└─ apply patch + incremental build    ~2-15s   (hot caches; near-zero for interpreted langs)
└─ verify in isolation                ~2-10s   (health + smoke hooks, no traffic exposure)
└─ promote: router flips to fork      ~0.05s   (previous version stays parked in a ring)
                                      ────────
                        total         ~5-30s   (~1s on small apps - see the benchmark)
└─ (background) clean image build + registry push + drift check

A fork that fails verification is destroyed - the pusher gets the failing hook and the fork's last logs; unverified code never receives a byte of traffic. Rollback flips the router to any kept version: sub-second, no builds, works when the builder is down.

Quickstart

curl -fsSL https://hotlane.dev/install.sh | sh
# or: brew install StefanIancu/hotlane/hotlane  |  npm install -g hotlane  |  pip install hotlane

cd your-app
hotlane init      # detects Node / Python / Go, writes hotlane.yml
hotlane serve     # boots the warm pool, proxies traffic

# make a change, then:
hotlane push
#   ok   http: /health == 200 (13ms)
# PROMOTED v2 live in 978ms

hotlane rollback  # flip back, sub-second

Requirements: a Linux or macOS host with Docker and git. One Go binary is both the daemon (serve) and the CLI.

Common questions answered honestly in the docs: isn't this mutable infrastructure? · what if the box dies? · why not ECS / Cloud Run / Kubernetes? · is the API safe to expose?

Measured, not promised

push to verified live (median) rollback
hotlane - TypeScript/Express (tsc build) 1.72s 0.64s
hotlane - FastAPI (pip) 1.18s 0.65s
for reference: a real GitHub Actions deploy pipeline 493s (8m13s), 15-run median re-run the pipeline

Read that reference row carefully - it is not a like-for-like comparison. The 493s is a real production workflow deploying a bigger system (two images, registry, SSH deploy); putting these small bench apps through a fresh pipeline would be faster than that. The honest claim is structural, not a ratio: a pipeline pays runner queue + checkout + toolchain setup (typically 60-120s) before it builds anything, then an image build, a registry round-trip, and a scheduler deploy. hotlane skips all of it by mutating a warm machine that is already running your app - so the floor is your incremental build plus a boot, and that is what the top two rows measure. Full method, per-run data, and caveats: docs/benchmark.md.

Everything here is reproducible: bench/bench.sh with the workloads in bench/.

Config

Everything is one file:

# hotlane.yml
app: api
image: node:22-alpine         # base image for the warm baseline
build: npm run build          # incremental command, reruns against warm caches
run: node dist/server.js
port: 3000
verify:
  - http: /health == 200
    timeout: 5s               # optional; defaults: 15s for http, 60s for run
  - run: ./smoke.sh
ring: 5                       # versions kept for instant rollback
archive: ghcr.io/acme/api     # registry ref for the archivist's clean images
notify: ${HOTLANE_NOTIFY_URL} # drift detected/healed, push rejected (Slack/Discord)

notify and archive interpolate ${VAR} from the daemon's environment, so webhook URLs and registry refs never have to live in a committed file (an unset variable fails the load loudly). Build/run/verify scripts are left untouched - their ${VAR}s belong to the shell inside the container. For running the daemon as a service, a systemd unit ships in packaging/systemd/ (guide); without $HOME, state lands in /var/lib/hotlane.

Several apps, one box: hotlane serve -apps /etc/hotlane/apps/ serves every *.yml in the directory. Each config adds src: (the app's checkout) and domain:; traffic routes by Host header on the shared listeners (-tls provisions a Let's Encrypt cert per domain), and every app keeps its own ring, archivist, held forks, and verify gate - a rejected push on one app cannot touch another. Client commands name their app via ./hotlane.yml, HOTLANE_APP, or -app; hotlane status -all shows the whole box. Design notes: docs/multi-app.md.

CLI

hotlane init         # detect the app, write a starter hotlane.yml
hotlane serve        # run the daemon (-token / -tls-domain to expose safely;
                     #   -apps DIR serves every config in a directory, -tls certs them all)
hotlane push         # git delta -> verified running fork -> traffic flip (~1-2s)
hotlane test         # like push, but HOLD the verified fork: poke it via the
                     #   X-Hotlane-Fork header (tokenized), then promote or discard it
hotlane promote <n>  # flip traffic to a held fork - byte-identical to what you tested
hotlane discard <n>  # destroy a held fork; live traffic never knew
hotlane rollback [n] # flip to the previous (or a specific) kept version
hotlane status       # live version, ring, drift verdict, timings (-all: every app)
hotlane logs [-n N]  # tail the live version's output
hotlane drift        # cold-boot the clean image, diff behavior vs live; exit 1 on drift
hotlane mcp          # serve hotlane as MCP tools over stdio - agents get push/test/
                     #   promote/rollback natively, no shell parsing

Every state-touching command accepts -json for machine-readable output. The daemon API is self-describing (GET /-/v1), and agents can read the complete operating contract in one fetch: hotlane.dev/llms-full.txt.

Client commands read HOTLANE_DAEMON and send HOTLANE_TOKEN as a bearer token. With serve -tls-domain yourapp.example.com, the daemon does its own HTTPS via Let's Encrypt and shares :443 the way humans expect: your app is served at https://yourapp.example.com/ with TLS included, the API tucks under the reserved /-/ prefix, and port 80 redirects. CI deploys with two secrets and one command (full guide).

Shadow testing, built in

Verify hooks are guesses about what matters; your live traffic is the truth. With a replay: block, the daemon records a rolling in-memory slice of real requests - and the responses live actually served - and replays it against every fork before promotion, diffing the answers:

replay:
  last: 200        # newest buffered requests to replay against each fork
  mode: report     # annotate the push; "gate" rejects a mismatch like a failing hook
$ hotlane push
  ok   http: /health == 200 (13ms)
  replay 199/200 matched (2 dynamic, status-only) (312ms, 512 buffered)
  MISMATCH GET /api/items: live answered 200, fork answers 500
push REJECTED after 1430ms: fork destroyed, live version untouched

The comparison reuses the drift normalizer - timestamps, UUIDs, request ids masked, and any path that varies between two live requests compares status only - so dynamic content doesn't cry wolf. Reads-only by default (GET/HEAD): a fork's state is isolated, but its external side effects (your Stripe, your SMTP) are not. The buffer is memory-only and never touches disk. hotlane test attaches the same report to held forks, so an agent reads the diff before deciding to promote. This is tap-compare / traffic shadowing without the service mesh.

The archivist

The warm fork chain is a cache; the archivist is its validation. After every promote it rebuilds your app from source, from scratch, in the background - the image classical CI would have made, minus the waiting - pushes it to your registry, and periodically cold-boots it to diff behavior against live:

$ hotlane drift
DRIFTED: behavior differs on /: clean build serves "hello", live serves "TAMPERED"
next push will rebuild from hotlane-api:clean

The comparison is behavior-based and tolerant of dynamic content: timestamps, UUIDs, request ids and the like are masked, and anything that differs between two requests to the same instance is excluded as evidence (status codes always compare) - so a /health that reports uptime doesn't cry wolf. With replay: enabled, drift checks also replay the recorded traffic slice against the cold boot - divergence is caught on any endpoint users recently exercised, not just the paths named in hooks. Divergence pings your webhook (Slack/Discord native), and the next ordinary push rebuilds from the clean image - the chain heals itself. The clean image also keeps the fast lane sustainable: fork chains auto-rebase onto it every ~40 pushes, so agent-speed pushing never hits Docker's layer-depth limit. Fast lane and audit trail, both real.

Built for agent loops

An agent can't wait eight minutes to learn it was wrong. hotlane makes each push-observe-fix turn cost about a second, over one HTTP endpoint (POST a raw git diff, get JSON back: timings, hook verdicts, promoted or rejected with logs), with the verify gate as the guardrail - a bad agent push dies in isolation while production keeps serving. And with hotlane test, the agent's own task-specific checks join the gate: fork, poke the running result through the X-Hotlane-Fork header while users stay on live, then promote the exact instance it validated - no rebuild between tested and live. Agents can learn the whole tool from hotlane.dev/llms.txt.

Where it runs (and doesn't)

On a machine you own that runs Docker: a VPS, bare metal, an EC2 instance, a homelab box. Not on ECS/Fargate/Cloud Run/Kubernetes - hotlane commands the host's Docker daemon and is an alternative to that layer, not a passenger on it. Teams keeping a managed platform for prod can run hotlane on a cheap box for the fast inner loop and feed the archivist's images to the existing pipeline. Details.

Honest tradeoffs

  • State does not fork. Forks share the real database. Run migrations expansively; treat schema changes with respect.
  • Drift checks cover hook paths. An endpoint without a hook can drift undetected - add hooks for what matters.
  • Single host. One daemon, one box. That covers a huge share of real apps; multi-host is on the roadmap.
  • Container-grade isolation. Fine for your own code and trusted teams; not a sandbox for hostile code.

License

MIT

Directories

Path Synopsis
cmd
hotlane command
hotlane: validation-first deployment.
hotlane: validation-first deployment.
internal
archive
Package archive is the trust layer: the warm fork chain is a cache, and the archivist is its validation.
Package archive is the trust layer: the warm fork chain is a cache, and the archivist is its validation.
config
Package config loads and validates hotlane.yml, the single per-app configuration surface.
Package config loads and validates hotlane.yml, the single per-app configuration surface.
detect
Package detect guesses a working hotlane.yml from what's in a repo.
Package detect guesses a working hotlane.yml from what's in a repo.
docker
Package docker is a thin wrapper over the docker CLI.
Package docker is a thin wrapper over the docker CLI.
notify
Package notify delivers events to a user-configured webhook.
Package notify delivers events to a user-configured webhook.
pool
Package pool manages the warm pool: the live, running instance of an app that forks are taken from and traffic is routed to.
Package pool manages the warm pool: the live, running instance of an app that forks are taken from and traffic is routed to.
proxy
Package proxy is the traffic front: a reverse proxy whose backend target can be swapped atomically.
Package proxy is the traffic front: a reverse proxy whose backend target can be swapped atomically.
replay
Package replay is shadow testing built into the deploy: it records a rolling slice of live traffic - including the response live actually served - and replays that slice against a fork before promotion, diffing the fork's answers against the recorded ones.
Package replay is shadow testing built into the deploy: it records a rolling slice of live traffic - including the response live actually served - and replays that slice against a fork before promotion, diffing the fork's answers against the recorded ones.
respdiff
Package respdiff normalizes HTTP response bodies so that behavioral comparison - drift checks, traffic replay - never reads legitimate volatility as divergence.
Package respdiff normalizes HTTP response bodies so that behavioral comparison - drift checks, traffic replay - never reads legitimate volatility as divergence.
verify
Package verify runs the configured checks against a fork before any traffic can reach it.
Package verify runs the configured checks against a fork before any traffic can reach it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL