# Knot

> What a knot does, what you can do with it, and how it talks to the rest of Tangled.

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.

```sh
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
```

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

## 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 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`).

> **Caution**
>
> The knot’s key is the sole rotation key for every repo DID it hosts. If you lose the master key or the sealed store, git data stays readable but you lose control of every repo DID. Keep the master key off the server.

## 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

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

| 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

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

```sh
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.

> **Note**
>
> New knots default to `[git] object_format = "sha256"`. SHA-256 repos can’t push to or fetch from SHA-1 repos, and the setting only affects newly created repos. Knots migrated with `knot-migrate` keep `sha1`.

[Run a knot](/docs/self-hosting/knot/) covers installation, [Knot configuration](/docs/reference/knot-config/) lists every setting, and [Golang to Rust knot migration guide](/docs/upgrading/knot-2/) covers moving from the Go knot.