Skip to content

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 name
git clone https://knot.example.com/did:plc:abc123 # HTTPS, by repo DID
  • 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 no sshd, AuthorizedKeysCommand or git unix user to configure.
  • A PDS for repo accounts. Every repository has its own did:plc repo DID. The knot holds that DID’s rotation and signing key, and its DID document points #atproto_pds and #tangled_knot at 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.subscribeRepos firehose that the indexer, mirrors and spindles follow.

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 under refs/cob-checkpoints/*. The repo DID’s atproto commit chain is derived from them and stored at refs/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).

RoleWhere it’s storedWhat it allows
Adminserver.admins in config (the first entry is the knot owner)Manage members, ban accounts, moderate any repo’s text
MemberMembers COB in the meta-repoCreate repos when admission is closed
Repo ownerRepo registryPush, manage collaborators, set contribution policy, rename, delete
CollaboratorCollaborators COB in the repoPush, merge pulls, moderate the repo
BlockedBlocklist in the meta-repoNothing; 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>.

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.

PeerDirectionWhat
Webinorg.tangled.repo.createRepo, ACL procedures, com.atproto.repo.* record writes (aud = repo DID)
HydrantoutFollows subscribeRepos for #commit, #identity and #account frames
SpindleoutFollows subscribeRepos for org.tangled.git.refs record ops; clones over HTTPS
Knotmirrorin/outFetches packs; pushes merges and branch deletes with your forwarded token
PLC directoryoutSubmits genesis and update ops for repo DIDs, with retries
User PDSesoutReads 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.

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-code
knot-server admin member add did:plc:alice
knot-server admin member remove did:plc:alice
knot-server admin user ban did:plc:spammer
knot-server admin user unban did:plc:spammer

knot-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.