Skip to content

Running knotless-serve

knotless works without any server. knotless-serve makes reads faster, lists pulls and issues, lets collaborators push, and draws the web pages. It is one binary, and each of its jobs is optional. Its state is a directory of bare mirrors and one SQLite file, so everything fits on one small VPS.

knotless-serve explains how each part works, and deploy/README.md shows how knotless.dev runs it. The command reference lists every flag.

JobTurned on byWhat it does
Read cachealwaysgit smart HTTP (protocol v0 and v2, partial and shallow clones) and an object API, read only, from cached mirrors
Appview--indexdev.knotless.listRepos, getForge, getFeed and listChecks from a SQLite index of the feed, and getProfile from the account’s PDS
Gateway--public-urldev.knotless.pushRefs, createCommit, putDelegation and getDelegation, for collaborators
Moderation service--index and --public-urlcom.atproto.moderation.createReport, takedowns, and labels on subscribeLabels
MCP over HTTP--public-url/mcp for remote agents
Web pages--webthe SvelteKit app in web/
Statusalways/_status
Usage counts--usagerequests, page views and events for the operator, as dev.knotless.getUsage

A server with all of them:

knotless-serve --listen 0.0.0.0:2590 \
--jetstream wss://jetstream.us-east.bsky.network \
--index /var/lib/knotless/index.db \
--backfill https://bsky.network \
--public-url https://git.example.com \
--labeler did:plc:… \
--web http://127.0.0.1:2591

Here --jetstream keeps mirrors warm and feeds the index, and --backfill indexes what was there before the server started. --public-url makes the server a gateway, as did:web:git.example.com. --labeler honours another labeler’s takedowns.

With --public-url, the server is open to every atproto account. Anyone can log in to its pages, push through its gateway to a repository whose owner delegated to it, and connect an assistant to /mcp. There is no list of accounts to keep, and no flag that turns one of these off on its own.

A takedown is what turns an account away, and takedowns need --index. The server’s operators, the accounts KNOTLESS_ADMIN_DIDS names, take an account down with knotless admin takedown <handle>, and a labeler named with --labeler can too. A taken-down account cannot log in to the pages or connect an assistant to /mcp, and the cookie and tokens it already has stop working at their next use. Its pushes through the gateway are not refused. Without --index, the server keeps no takedowns and refuses no one: run it that way only where you trust everyone who can reach it.

The server learns of changes from a Jetstream v2 host (--jetstream), or from a relay or PDS directly (--firehose wss://…). Jetstream is the cheaper of the two. It sends only dev.knotless.* commits, plus identity and account changes, as small JSON frames that carry each record. The server then neither decodes every commit on the network nor fetches each record from its author’s PDS.

Jetstream’s frames are unsigned, so the index trusts the Jetstream operator for what a record says. Mirrors still sync from the PDS, and --backfill rebuilds the index.

Clone over HTTP, or read single objects through the API:

git clone --filter=blob:none http://localhost:2590/alice.test/proj
curl 'localhost:2590/api/alice.test/proj/blob?rev=main&path=src/lib.rs'
curl 'localhost:2590/api/alice.test/proj/tree?rev=main&path=src'
curl 'localhost:2590/api/alice.test/proj/log?rev=main&path=src&limit=20'
curl 'localhost:2590/api/alice.test/proj/diff?base=main&head=feature'
curl 'localhost:2590/api/alice.test/proj/object/<sha>' # immutable, cacheable

The pages are read only. They show what knotless is and how to connect, what happened lately, each account’s profile and public repositories, and each repository’s trees, files, history, commits, pulls, issues and checks. A repository’s page is at its clone URL. Commits show each file’s diff with old and new line numbers, syntax highlighting, and the changed part of each edited line. Markdown is rendered, Mermaid diagrams included. Every page’s header searches repositories by name or description, and their owners by handle: / or ⌘K opens a palette over the page, showing matches as they are typed, and /search?q= lists them all, or every repository, newest first, without q. Logged in, the private repositories they may read come first, marked so. The front page, repositories, pulls and issues unfurl into social cards that the server draws itself.

The pages are a SvelteKit app in web/, run with Node. --web names where it listens: the server hands it every request it does not answer itself, and the app draws each page from the server’s XRPC queries. Without --web, the server has no pages.

just web # build web/build
just pages # serve it on :2591
just serve --index index.db --web http://127.0.0.1:2591
just web-dev # or redraw as the source changes

/_status answers 200 while the server’s feeds bring events, its index answers and, with ?repo=<handle>/<name>, that repository’s mirror reads. Otherwise it answers 503 and says what failed. knotless status reads it:

knotless status https://knotless.dev --repo chadtmiller.com/knotless

deploy/README.md says what each part means.

With --usage <file>, the server counts its requests by surface, route, status, time taken and kind of client, and the pages drawn in a browser, each visitor once a day by a salted hash it forgets once the day is over, and the events of signed-in accounts (logins, MCP, the gateway), keeping who did each for 30 days and only counts after. knotless admin stats shows those beside what the index says the network did, to the accounts KNOTLESS_ADMIN_DIDS names on the server:

KNOTLESS_ADMIN_DIDS=did:plc:… knotless-serve --usage usage.db … # on the server
knotless admin stats --days 30 # signed in as one of them

deploy/README.md says what is counted and what is kept.