Knot
A knot is the server that hosts your git repositories. It is a single
Rust binary, knot-server, that serves git over SSH and HTTPS and also
acts as the atproto PDS for every repository it hosts. You don’t need to
run a knot to use Tangled: host with Tangled on the knots we run, or
self-host your own.
git clone knot.example.com:alice.bsky.social/proj # SSH, by owner and namegit clone https://knot.example.com/did:plc:abc123 # HTTPS, by repo DIDWhat a knot does
Section titled “What a knot does”- Git hosting. Clone, fetch and push over SSH (
git-upload-pack,git-receive-pack,git-upload-archive), smart HTTP, and Git LFS. The SSH server is built in: there is nosshd,AuthorizedKeysCommandorgitunix user to configure. - A PDS for repo accounts. Every repository has its own
did:plcrepo DID. The knot holds that DID’s rotation and signing key, and its DID document points#atproto_pdsand#tangled_knotat the knot. Issues, pulls, comments, reactions, label definitions and patches are records in the repo DID’s account on the knot, not in the author’s PDS. - Access control. Admins, members, repo owners, collaborators and a blocklist decide who can create repos, push and write records.
- Events. A
com.atproto.sync.subscribeReposfirehose that the indexer, mirrors and spindles follow.
Git as the database
Section titled “Git as the database”The knot has no SQL database. All state lives under repo.scan_path:
- Each repo is a bare git repository, sharded by DID
(
<scan_path>/<method>/<first 2 chars>/<rest>). - ACLs and records are signed collaborative objects (COBs): change
graphs stored under
refs/cobs/<nsid>/<id>, with snapshots underrefs/cob-checkpoints/*. The repo DID’s atproto commit chain is derived from them and stored atrefs/atproto/commit. - A meta-repo, sharded under the knot’s own
did:web, holds the member list, blocklist, repo registry (owner and name to repo DID, policies), the queue of pending PLC operations and the firehose sequence number. At boot the knot rebuilds its in-memory index from the meta-repo and answers “warming” until it is ready.
The only other state is a sealed key store, unlocked by the key in the
env var named by secrets.master_key_env (usually KNOT_MASTER_KEY).
Roles and access
Section titled “Roles and access”| Role | Where it’s stored | What it allows |
|---|---|---|
| Admin | server.admins in config (the first entry is the knot owner) | Manage members, ban accounts, moderate any repo’s text |
| Member | Members COB in the meta-repo | Create repos when admission is closed |
| Repo owner | Repo registry | Push, manage collaborators, set contribution policy, rename, delete |
| Collaborator | Collaborators COB in the repo | Push, merge pulls, moderate the repo |
| Blocked | Blocklist in the meta-repo | Nothing; overrides every other role |
Admission ([acl] admission) is closed by default: only members
can create repos. Set it to open to let any account create repos.
Contribution policy decides who may write records (issues, comments,
pulls) into a repo, separately from who may push. Values are anyone,
members and collaborators; the default comes from
[repo] contribution_policy (collaborators). Owners change it per
repo through the manifest’s contributionPolicy, with
com.atproto.repo.putRecord on org.tangled.repo.manifest. Pushing
always requires being the owner or a collaborator, and not being
blocked.
Membership and collaborators are records the knot keeps itself, written
with com.atproto.repo.createRecord and removed with deleteRecord.
Admins add members as org.tangled.knot.member in the knot’s own
account (knot-server admin member add does this), and owners add
collaborators as org.tangled.graph.collaborator in the repo’s
account; the record key is the subject’s DID. The web UI manages
collaborators. Members can also join with an invite code: codes are single
use, expire after two days, and live in memory, so a restart discards
them. The landing URL is https://<knot>/invite/<code>.
Pushing
Section titled “Pushing”SSH push identifies you by matching the offered key against the
org.tangled.key.sshKey records in your PDS whose usages include
auth (or omit usages). The knot caches keys for an
hour ([keyfill] ttl_secs). If your agent offers many keys, add
IdentitiesOnly yes for the knot host in ~/.ssh/config.
HTTPS push takes an atproto service-auth token bound to
org.tangled.git.receivePack, as a Bearer token or as the Basic-auth password,
usually from a git credential helper.
After a push, git prints push messages: an acknowledgement, an “Open
pull request” link for new branches, workflow compile diagnostics, and a
whatever the repository’s spindle answered about the pipeline it scheduled,
such as how to follow its logs over SSH. The text comes from
[messages.*] templates. Pass -o skip-ci to skip CI for a push.
A Knot won’t allow any pushes to the following reserved refs: refs/cobs/*,
refs/cob-checkpoints/*, refs/hidden/* and refs/atproto/*. On the flip-side, refs/cobs/* will be fetchable read-only when using an explicit ref-prefix. There
are no server-side hooks.
How it talks to the rest
Section titled “How it talks to the rest”| Peer | Direction | What |
|---|---|---|
| Web | in | org.tangled.repo.createRepo, ACL procedures, com.atproto.repo.* record writes (aud = repo DID) |
| Hydrant | out | Follows subscribeRepos for #commit, #identity and #account frames |
| Spindle | out | Follows subscribeRepos for org.tangled.git.refs record ops; clones over HTTPS |
| Knotmirror | in/out | Fetches packs; pushes merges and branch deletes with your forwarded token |
| PLC directory | out | Submits genesis and update ops for repo DIDs, with retries |
| User PDSes | out | Reads identities and SSH keys |
After every push, the knot rewrites the single org.tangled.git.refs
record (rkey self) with a snapshot of every ref and the default
branch, then emits a knot-signed org.tangled.git.pushEvent frame
(repo, pusher, ref edits, push options) on the firehose.
org.tangled.knot.describeKnot returns the knot’s DID, version and
capabilities: tangled1.0, knot-acl, repo-did-input,
atproto-firehose and knot-social-writes.
Operations
Section titled “Operations”Maintenance runs in the background: commit-graph, multi-pack index,
bitmaps and a geometric repack every six hours, with prune and reflog
grace periods ([maintenance]).
Health: curl https://<knot>/xrpc/_health returns the version, or
503 if the LFS store is unwritable. /xrpc/org.tangled.knot.describeKnot
describes the knot, and /.well-known/did.json the knot’s DID document.
Admin CLI. knot-server admin posts to the knot’s own
https://<server.hostname> with Basic auth. It only works when
[acl] legacy_admin_secret_env names an env var holding a shared
secret.
knot-server admin create-invite-codeknot-server admin member add did:plc:aliceknot-server admin member remove did:plc:aliceknot-server admin user ban did:plc:spammerknot-server admin user unban did:plc:spammerknot-server config-template prints a full config, and
knot-server validate checks one.
Run a knot covers installation, Knot configuration lists every setting, and Golang to Rust knot migration guide covers moving from the Go knot.