levelrail

module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: Apache-2.0

README

Levelrail

CI Go Report Card Go Version GitHub stars Last commit PRs welcome Discussions

Levelrail is a self-hosted deployment platform whose agent talks to Docker's own Engine API directly instead of SSHing into your servers and shelling out docker commands, with metrics and log storage built into the core instead of a separately-installed extra. Point it at one or more Linux boxes and it turns them into a private cloud: push to a git repo, get a running app with TLS, logs, metrics, and rollback.

If this solves a problem you have, a star helps other people building the same thing find it.

Quickstart

curl -fsSL https://raw.githubusercontent.com/glincker/levelrail/main/install.sh | sudo sh

Installs the binary, installs Docker if it's missing, sets up a systemd unit, and waits for the control plane to report healthy before declaring success. Safe to re-run later as an upgrade. See docs/getting-started.md to build from source instead, and docs/comparison.md for how this differs from Coolify, Dokploy, CapRover, Dokku, and Kamal, including what Levelrail doesn't do yet.

Already running everything else as containers? ghcr.io/glincker/levelrail and ghcr.io/glincker/levelrail-agent images are published on every tagged release; see docs/docker.md for a docker run and docker-compose.yml example.

Features

  • Zero-downtime deploys. Rolling, recreate, or blue-green strategy, gated on real readiness/liveness probes, with rollback to pinned prior images always available, not something to reconstruct by hand.
  • Observability built in. Node-local metrics at 15s resolution and full-text log search, no separate Grafana/Loki install. Deploy markers are overlaid directly on metric charts, so "which deploy caused this" is a visual answer, not an investigation.
  • Eight managed database engines. Postgres, Redis, MySQL, MongoDB, MariaDB, KeyDB, Dragonfly, and ClickHouse, all through one engine registry: scheduled backups, restore, and automatic post-backup verification apply generically across every engine.
  • Multi-node from day one. WireGuard mesh, internal DNS across nodes, cordon/drain, and no inbound ports required on any managed server.
  • Git-native deploys. GitHub, GitLab, and Bitbucket webhooks, plus preview environments per pull request with automatic teardown.
  • IAM and audit. AWS-IAM-shaped Allow/Deny policies scoped to a specific resource, with a full audit log and CSV export.
  • Alerting across thirteen channels. Threshold, crashloop, certificate expiry, and five other rule kinds, delivered to Slack, Discord, email, Telegram, Pushover, PagerDuty, Microsoft Teams, Resend, Gotify, Ntfy, Mattermost, Lark, or a webhook.
  • AI-ready API. The same HTTP API the dashboard runs on backs an MCP server, so AI tools can list apps, read logs, and diagnose a crashloop directly.

Status

Early, active development. Single-node and multi-node both run today: agent enrollment, the WireGuard mesh, internal DNS, and node placement/cordon/drain are built. Beyond the core deploy path, an IAM-style policy engine, audit logging, feature flags, alerting across eight rule kinds and thirteen notification channels, an admin-created team invite flow, and eight managed database engines with backup/restore/verification are also shipped (see docs/roadmap.md for the full, current list). There is no stable release yet and the project is not ready for production workloads. APIs, the app spec format, and the on-disk data layout can all still change without notice.

From the team behind thesvg (6,400+ brand SVG icons) and theauth-go (OAuth 2.1 auth library for Go).

Why not Coolify or Dokploy

Most self-hosted platforms in this category drive remote servers by SSHing in and shelling out to the docker CLI, then parsing its text output. That's a common source of flakiness and it forces polling loops to detect state changes. Levelrail takes a different approach:

  • Agent-based control plane. A small agent runs on each node, talks to the local Docker Engine API directly, and streams container events up to the control plane, which never polls.
  • Observability built in, not bolted on. Metrics and log storage are first-class parts of the core, not something you're told to install separately.
  • Low idle footprint. A single static Go binary for the control plane, a single static Go binary for the agent, no separate database server, no message queue, no extra containers just to run the platform itself.

Levelrail is not a Kubernetes competitor. It targets teams running somewhere between 3 and 50 services across 1 to 10 machines who want a private cloud without taking on Kubernetes's operational surface.

Architecture at a glance

  • Control plane (cmd/levelrail): a single Go binary. Reconciles declarative resource records against observed Docker state, the same pattern Kubernetes controllers use, without the rest of the Kubernetes runtime.
  • Node agent (cmd/levelrail-agent): dials out to the control plane (no inbound ports on managed servers) and talks to the local Docker socket via the Engine API.
  • Ingress: Caddy embedded as a Go library (internal/ingress), driven in-process, for automatic TLS and domain routing.
  • Builds: BuildKit as a Go library (internal/build), not docker build, for remote cache and parallel stage execution.
  • State: embedded SQLite in WAL mode (internal/store), pure Go via modernc.org/sqlite, so cross-compiling the binary stays simple.
  • API: an HTTP API under internal/api, versioned at /api/v1.
  • Frontend (web/): React, Vite, TypeScript, Tailwind, TanStack Router and Query. Built as static assets and embedded into the control plane binary via embed.FS, so there's no separate Node process to run in production.

Everything ships as two binaries: levelrail (control plane, with the frontend embedded) and levelrail-agent (node agent). In single-node mode the agent's transport runs in-process instead of over the network, so the code path is the same whether you're running one node or ten.

How it compares

Positioning, not a ranking. All of these are worth using; the differences below are the ones that matter for choosing between them.

Project Node control Orchestration Observability Ingress
Levelrail Reverse-dialed gRPC agent, no CLI shelling Custom Go reconciler over Docker Engine API, level-triggered Node-local metrics and logs, federated query, all shipped Embedded Caddy, in-process
Coolify (v4) SSH plus CLI-shelled docker/docker compose Docker Compose per app, Traefik label discovery Optional bolt-on agent, opt-in Traefik, separate container
Dokploy SSH-tunneled Docker Engine API plus CLI-shelled lifecycle ops Docker Swarm services Separate Go binary, polling Traefik, Swarm service
CapRover Docker Swarm API, even single-node Docker Swarm services Optional sibling containers, not built in nginx, sibling Swarm service
Dokku Local dokku bash entrypoint, no daemon Custom Bash scheduler over plain docker Not stated in research nginx by default, pluggable
Kamal One-shot SSH CLI, no daemon or agent None: deploy script, not a control plane None built in kamal-proxy, standalone container

Full writeup with per-project detail: docs/comparison.md.

Screenshots

Levelrail apps list showing all services across nodes at a glance
Apps list
Levelrail deploy history view with one-click rollback
Deploy history and rollback
Levelrail live log viewer with full-text search
Live log viewer
Levelrail nodes list showing node health and placement
Nodes

Building and running locally

Requires Go 1.26+ and Docker.

# control plane
go build ./cmd/levelrail

# node agent
go build ./cmd/levelrail-agent

The frontend lives in web/ and is a separate Vite project:

cd web
npm install
npm run dev       # Vite dev server
npm run build      # production build, embedded into the control plane binary

See web/README.md for frontend-specific commands and conventions.

Contributing

See CONTRIBUTING.md for how to propose changes, commit conventions, and how to run tests and the linter locally.

Docs and community

  • docs/ -- getting started, architecture, app spec reference, roadmap, full index
  • GitHub Discussions -- questions, ideas, show and tell

Star history

Star History Chart

License

Apache 2.0, see LICENSE.

Directories

Path Synopsis
cmd
levelrail command
Command levelrail is the control plane binary.
Command levelrail is the control plane binary.
levelrail-agent command
Command levelrail-agent is TASKS.md 3.2's node agent binary (the repo layout names it, this is the first pass to actually build it).
Command levelrail-agent is TASKS.md 3.2's node agent binary (the repo layout names it, this is the first pass to actually build it).
levelrail-cli command
Command levelrail-cli is a thin, scriptable HTTP client for the control plane's versioned API (internal/api, mounted at /api/v1), the first command-line surface this project has (cmd/levelrail is the control plane server, cmd/levelrail-agent is the node agent, neither is meant to be typed at by an operator).
Command levelrail-cli is a thin, scriptable HTTP client for the control plane's versioned API (internal/api, mounted at /api/v1), the first command-line surface this project has (cmd/levelrail is the control plane server, cmd/levelrail-agent is the node agent, neither is meant to be typed at by an operator).
levelrail-mcp command
Command levelrail-mcp is an MCP (Model Context Protocol) server that lets an AI agent manage a control plane deployment through the same versioned REST API (internal/api, mounted at /api/v1) cmd/levelrail-cli already drives: see internal/apiclient's own doc comment, and internal/store/tokens.go's APIToken doc comment on why a bearer token was always meant to authenticate "a CLI, an MCP server, or a third-party integration" alike.
Command levelrail-mcp is an MCP (Model Context Protocol) server that lets an AI agent manage a control plane deployment through the same versioned REST API (internal/api, mounted at /api/v1) cmd/levelrail-cli already drives: see internal/apiclient's own doc comment, and internal/store/tokens.go's APIToken doc comment on why a bearer token was always meant to authenticate "a CLI, an MCP server, or a third-party integration" alike.
internal
agent
Package agent is TASKS.md 3.1's transport boundary: the interface ADR 003 describes ("the reconciler and everything above the transport boundary never knows whether it's talking to a local in-process agent or a remote one over mTLS") but that Phase 1 never actually built, confirmed directly against this repo before writing this package: every reconcile controller (internal/reconcile/application, internal/reconcile/database) takes a bare docker.Runtime, and cmd/levelrail/main.go's dynamicSource hands every controller the same single local docker.Runtime with no node concept anywhere.
Package agent is TASKS.md 3.1's transport boundary: the interface ADR 003 describes ("the reconciler and everything above the transport boundary never knows whether it's talking to a local in-process agent or a remote one over mTLS") but that Phase 1 never actually built, confirmed directly against this repo before writing this package: every reconcile controller (internal/reconcile/application, internal/reconcile/database) takes a bare docker.Runtime, and cmd/levelrail/main.go's dynamicSource hands every controller the same single local docker.Runtime with no node concept anywhere.
alerting
Package alerting implements TASKS.md 2.5/2.7: threshold rules over internal/telemetry's metrics and logs, and crashloop detection as a built-in rule kind sharing the same evaluate/notify path rather than a separate mechanism (see TASKS.md 2.5's own note on why).
Package alerting implements TASKS.md 2.5/2.7: threshold rules over internal/telemetry's metrics and logs, and crashloop detection as a built-in rule kind sharing the same evaluate/notify path rather than a separate mechanism (see TASKS.md 2.5's own note on why).
api
Package api implements TASKS.md 1.9: the HTTP API the web frontend (1.10) and, later, the MCP layer both build on.
Package api implements TASKS.md 1.9: the HTTP API the web frontend (1.10) and, later, the MCP layer both build on.
apiclient
Package apiclient is the one HTTP client implementation for the control plane's versioned API (internal/api, mounted at /api/v1).
Package apiclient is the one HTTP client implementation for the control plane's versioned API (internal/api, mounted at /api/v1).
backup
Package backup drives a database backup end to end: dump a managed database container's data, stream it straight to an S3-compatible bucket, and record the attempt in store.BackupHistory.
Package backup drives a database backup end to end: dump a managed database container's data, stream it straight to an S3-compatible bucket, and record the attempt in store.BackupHistory.
bitbucketapp
Package bitbucketapp implements OAuth2 and the small slice of Bitbucket Cloud's REST API this control plane needs once connected.
Package bitbucketapp implements OAuth2 and the small slice of Bitbucket Cloud's REST API this control plane needs once connected.
brand
Package brand provides the branding indirection layer this project requires: no product name is ever hardcoded in source.
Package brand provides the branding indirection layer this project requires: no product name is ever hardcoded in source.
build
Package build drives container image builds through BuildKit's Go client (github.com/moby/buildkit/client), never by shelling out to `docker build` or the `docker` CLI, matching the same "no CLI shelling" rule the node communication layer follows.
Package build drives container image builds through BuildKit's Go client (github.com/moby/buildkit/client), never by shelling out to `docker build` or the `docker` CLI, matching the same "no CLI shelling" rule the node communication layer follows.
catalog
Package catalog holds Levelrail's own curated set of one-click service templates (ADR 015).
Package catalog holds Levelrail's own curated set of one-click service templates (ADR 015).
compose
Package compose parses a Docker Compose file into Levelrail's own desired-state model, in two shapes depending on the caller.
Package compose parses a Docker Compose file into Levelrail's own desired-state model, in two shapes depending on the caller.
cronexpr
Package cronexpr parses and evaluates standard 5-field cron expressions ("minute hour day-of-month month day-of-week", e.g.
Package cronexpr parses and evaluates standard 5-field cron expressions ("minute hour day-of-month month day-of-week", e.g.
deploy
Package deploy is TASKS.md 1.4's build integration: it connects internal/spec's build declaration to internal/build's BuildKit client, and a successful build's output to internal/store's desired state, closing the loop the application controller (internal/reconcile/ application, TASKS.md 1.3) reads from on every reconcile.
Package deploy is TASKS.md 1.4's build integration: it connects internal/spec's build declaration to internal/build's BuildKit client, and a successful build's output to internal/store's desired state, closing the loop the application controller (internal/reconcile/ application, TASKS.md 1.3) reads from on every reconcile.
deploylog
Package deploylog is the glue between internal/build.ProgressEvent (the progress callback all three real deploy-attempt trigger paths already thread through internal/deploy.Pipeline.Deploy) and two real consumers: a persisted, replayable row in telemetry.db (internal/telemetry's deploy_logs table) and any currently-connected SSE viewer watching that same attempt live.
Package deploylog is the glue between internal/build.ProgressEvent (the progress callback all three real deploy-attempt trigger paths already thread through internal/deploy.Pipeline.Deploy) and two real consumers: a persisted, replayable row in telemetry.db (internal/telemetry's deploy_logs table) and any currently-connected SSE viewer watching that same attempt live.
diagnose
Package diagnose is a deterministic, pure-Go pattern matcher over diagnostic signals the platform already collects: deploy attempt status/error, reconcile condition reason strings, crashloop alert state, and recent log lines.
Package diagnose is a deterministic, pure-Go pattern matcher over diagnostic signals the platform already collects: deploy attempt status/error, reconcile condition reason strings, crashloop alert state, and recent log lines.
docker
Package docker wraps the Docker Engine API.
Package docker wraps the Docker Engine API.
dockertest
Package dockertest holds test helpers shared by every package whose _live_test.go files need a real Docker daemon (or other real, network-dependent infra) to run.
Package dockertest holds test helpers shared by every package whose _live_test.go files need a real Docker daemon (or other real, network-dependent infra) to run.
email
Package email is the platform's one email-sending capability: a narrow Sender interface plus SMTP and SES implementations, shared by internal/alerting and internal/api so neither imports the other.
Package email is the platform's one email-sending capability: a narrow Sender interface plus SMTP and SES implementations, shared by internal/alerting and internal/api so neither imports the other.
githubapp
Package githubapp implements the GitHub App manifest registration flow, App-level JWT signing (RS256, per GitHub's own App authentication spec), and the small slice of the GitHub REST API this control plane needs once an App is connected: minting installation access tokens, listing an installation's repositories, and listing a repository's branches.
Package githubapp implements the GitHub App manifest registration flow, App-level JWT signing (RS256, per GitHub's own App authentication spec), and the small slice of the GitHub REST API this control plane needs once an App is connected: minting installation access tokens, listing an installation's repositories, and listing a repository's branches.
gitlabapp
Package gitlabapp implements OAuth2 and the small slice of GitLab's REST API this control plane needs once connected.
Package gitlabapp implements OAuth2 and the small slice of GitLab's REST API this control plane needs once connected.
ingress
Package ingress is the Phase 0 spike for embedding Caddy as a Go library, driven entirely through its in-process admin API.
Package ingress is the Phase 0 spike for embedding Caddy as a Go library, driven entirely through its in-process admin API.
network
Package network is ADR 006's WireGuard mesh: the abstraction that ADR 006 says "has to be designed in Phase 1 as an interface, not a concrete WireGuard dependency" and that, until TASKS.md 3.4, did not exist at all (the directory the project's repo layout reserves for it was empty).
Package network is ADR 006's WireGuard mesh: the abstraction that ADR 006 says "has to be designed in Phase 1 as an interface, not a concrete WireGuard dependency" and that, until TASKS.md 3.4, did not exist at all (the directory the project's repo layout reserves for it was empty).
probe
Package probe implements HTTP readiness checking: the controller calls out to a container over HTTP, rather than the container self-reporting health via Docker's own HEALTHCHECK state machine.
Package probe implements HTTP readiness checking: the controller calls out to a container over HTTP, rather than the container self-reporting health via Docker's own HEALTHCHECK state machine.
prompb
Package prompb implements the small subset of Prometheus's remote-read wire protocol TASKS.md 2.6 needs (ReadRequest/ReadResponse and their nested messages), by hand, using google.golang.org/protobuf's low-level protowire primitives (already a transitive dependency, no new module).
Package prompb implements the small subset of Prometheus's remote-read wire protocol TASKS.md 2.6 needs (ReadRequest/ReadResponse and their nested messages), by hand, using google.golang.org/protobuf's low-level protowire primitives (already a transitive dependency, no new module).
reconcile
Package reconcile implements the core convergence loop: desired state in, observed state diffed against it, idempotent and level-triggered controllers converge the two.
Package reconcile implements the core convergence loop: desired state in, observed state diffed against it, idempotent and level-triggered controllers converge the two.
reconcile/application
Package application implements the declarative app spec's service contract and TASKS.md 1.3's application controller: the reconcile.Controller that converges a real, store-backed desired service to a running container, replacing nginxdemo's hardcoded desired state with the real thing.
Package application implements the declarative app spec's service contract and TASKS.md 1.3's application controller: the reconcile.Controller that converges a real, store-backed desired service to a running container, replacing nginxdemo's hardcoded desired state with the real thing.
reconcile/cloudflaretunnel
Package cloudflaretunnel implements the reconcile.Controller that converges a single, platform-wide desired state (store.
Package cloudflaretunnel implements the reconcile.Controller that converges a single, platform-wide desired state (store.
reconcile/database
Package database implements TASKS.md 1.8's managed database controller: the reconcile.Controller that converges a store-backed store.DesiredDatabase to a running, volume-backed container, the same architectural pattern internal/reconcile/application already establishes (level-triggered, deterministic naming, a narrow store interface for testability), applied to a database instead of a built application image.
Package database implements TASKS.md 1.8's managed database controller: the reconcile.Controller that converges a store-backed store.DesiredDatabase to a running, volume-backed container, the same architectural pattern internal/reconcile/application already establishes (level-triggered, deterministic naming, a narrow store interface for testability), applied to a database instead of a built application image.
reconcile/ingress
Package ingress implements TASKS.md 1.6's ingress controller: the reconcile.Controller that keeps Caddy's config (internal/ingress, ADR 005) in sync with every service that declares domains.
Package ingress implements TASKS.md 1.6's ingress controller: the reconcile.Controller that keeps Caddy's config (internal/ingress, ADR 005) in sync with every service that declares domains.
reconcile/mesh
Package mesh implements TASKS.md 3.4's mesh controller: the reconcile.Controller that keeps the WireGuard mesh and the internal DNS zone converged on whatever the store currently says the fleet and its placements look like.
Package mesh implements TASKS.md 3.4's mesh controller: the reconcile.Controller that keeps the WireGuard mesh and the internal DNS zone converged on whatever the store currently says the fleet and its placements look like.
reconcile/nginxdemo
Package nginxdemo is the Phase 0 exit criterion: one controller that keeps a single hardcoded nginx container running.
Package nginxdemo is the Phase 0 exit criterion: one controller that keeps a single hardcoded nginx container running.
reconcile/nodehealth
Package nodehealth implements TASKS.md 3.7's node health check: the reconcile.Controller that converges a node's observed heartbeat (internal/store's last_seen_at, kept fresh by internal/agent.Server's periodic touch loop while a node's gRPC session stays open) against its recorded Status, the same architectural pattern internal/reconcile/application and internal/reconcile/database already establish (level-triggered, a narrow store interface for testability, one controller instance per resource), applied to a node instead of a service or database.
Package nodehealth implements TASKS.md 3.7's node health check: the reconcile.Controller that converges a node's observed heartbeat (internal/store's last_seen_at, kept fresh by internal/agent.Server's periodic touch loop while a node's gRPC session stays open) against its recorded Status, the same architectural pattern internal/reconcile/application and internal/reconcile/database already establish (level-triggered, a narrow store interface for testability, one controller instance per resource), applied to a node instead of a service or database.
reconcile/registry
Package registry implements the reconcile.Controller that converges a single, platform-wide desired state (store.RegistrySettings plus a generated password in internal/secrets) to a running or absent registry:2 container: Levelrail's own built-in image registry, so a multi-node deployment gets a BuildKit cache/distribution backend (internal/build's WithCacheRegistry) without an operator first signing up for an external one.
Package registry implements the reconcile.Controller that converges a single, platform-wide desired state (store.RegistrySettings plus a generated password in internal/secrets) to a running or absent registry:2 container: Levelrail's own built-in image registry, so a multi-node deployment gets a BuildKit cache/distribution backend (internal/build's WithCacheRegistry) without an operator first signing up for an external one.
rightsizing
Package rightsizing is a deterministic, pure-Go engine that turns an app's historical CPU/memory usage into a resource-limit suggestion.
Package rightsizing is a deterministic, pure-Go engine that turns an app's historical CPU/memory usage into a resource-limit suggestion.
scheduledtask
Package scheduledtask execs an operator-defined command into a running app's container on a cron schedule (e.g.
Package scheduledtask execs an operator-defined command into a running app's container on a cron schedule (e.g.
secrets
Package secrets implements envelope encryption: per-app data encryption keys, wrapped by a master key held only by the control plane, using filippo.io/age for the crypto primitives.
Package secrets implements envelope encryption: per-app data encryption keys, wrapped by a master key held only by the control plane, using filippo.io/age for the crypto primitives.
spec
Package spec parses and validates the app.yaml file: the one declarative file a user writes in their repo, per the app spec design.
Package spec parses and validates the app.yaml file: the one declarative file a user writes in their repo, per the app spec design.
store
Package store is the embedded SQLite state layer: WAL mode, modernc.org/sqlite (pure Go, no cgo, keeps cross-compiling the control plane binary trivial), forward-only versioned migrations.
Package store is the embedded SQLite state layer: WAL mode, modernc.org/sqlite (pure Go, no cgo, keeps cross-compiling the control plane binary trivial), forward-only versioned migrations.
telemetry
Package telemetry is the node-local metrics store, decided in ADR 009: a dedicated SQLite database (telemetry.db, separate from internal/store's levelrail.db) holding container resource-usage samples, queried in place rather than shipped anywhere centrally.
Package telemetry is the node-local metrics store, decided in ADR 009: a dedicated SQLite database (telemetry.db, separate from internal/store's levelrail.db) holding container resource-usage samples, queried in place rather than shipped anywhere centrally.
totp
Package totp implements time-based one-time passwords (RFC 6238, the algorithm every mainstream authenticator app speaks) plus the small amount of supporting logic 2FA needs: secret generation, an otpauth:// provisioning URI, and single-use recovery codes.
Package totp implements time-based one-time passwords (RFC 6238, the algorithm every mainstream authenticator app speaks) plus the small amount of supporting logic 2FA needs: secret generation, an otpauth:// provisioning URI, and single-use recovery codes.
version
Package version holds the control plane's own build version, injected via -ldflags at release build time (see .github/workflows/release.yml).
Package version holds the control plane's own build version, injected via -ldflags at release build time (see .github/workflows/release.yml).
webhook
Package webhook is TASKS.md 1.5's git integration: a GitHub push-webhook receiver that verifies the request signature, extracts the pushed commit SHA, fetches the repository at that SHA to a local checkout, and hands the result to internal/deploy's Pipeline (TASKS.md 1.4), the same pipeline a manual deploy trigger will use once the HTTP API (1.9) exists.
Package webhook is TASKS.md 1.5's git integration: a GitHub push-webhook receiver that verifies the request signature, extracts the pushed commit SHA, fetches the repository at that SHA to a local checkout, and hands the result to internal/deploy's Pipeline (TASKS.md 1.4), the same pipeline a manual deploy trigger will use once the HTTP API (1.9) exists.
test
e2e
Package web embeds the built frontend via embed.FS, keeping the control plane a single static binary with no Node runtime required on the server, and serves it with client-side-routing fallback, so a hard refresh or direct link to a TanStack Router path like /apps/foo resolves correctly instead of 404ing at the server before React Router ever gets a chance to handle it.
Package web embeds the built frontend via embed.FS, keeping the control plane a single static binary with no Node runtime required on the server, and serves it with client-side-routing fallback, so a hard refresh or direct link to a TanStack Router path like /apps/foo resolves correctly instead of 404ing at the server before React Router ever gets a chance to handle it.

Jump to

Keyboard shortcuts

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