Two pictures of how this goes — what I'm building now, and where I'd like to push it.
0
forego-knot-gist.md edited
63 lines 3.2 kB view raw view code

Today — Forgejo private, public repos mirrored to knot1.tangled.sh#

Forgejo holds canonical state. A small bridge translates Forgejo webhook events into AT Proto XRPC calls and signed git pushes against the hosted knot. Forgejo image untouched.

flowchart LR
    user(["author"]) -->|git push| forgejo

    subgraph private["my forge — private"]
        forgejo["Forgejo<br/>(authoritative)"]
        webhook[/"webhook config<br/>per repo or org"/]
        forgejo --- webhook
    end

    webhook -->|push / create / delete<br/>HMAC-signed| bridge

    subgraph bridgeBox["forgejo-knot-bridge — small Go service"]
        bridge["event handler<br/>user → DID mapping"]
        keystore[("signing keys<br/>per bridged DID")]
        statedb[("bridge.db<br/>repo→DID,<br/>last-mirrored ref")]
        bridge --- keystore
        bridge --- statedb
    end

    bridge -->|sh.tangled.repo.create<br/>Service-Auth JWT| knot
    bridge -->|git push --mirror<br/>Service-Auth Bearer| knot

    subgraph hosted["knot1.tangled.sh — hosted, third-party"]
        knot["knotserver"]
        appview["tangled.sh appview<br/>(consumes firehose)"]
        knot -.->|sh.tangled.knot.subscribeRepos<br/>ws firehose| appview
    end

    plc[("plc.directory")] -.->|DID document<br/>verifies signing key| knot

The bridge needs three things from the knot side: knot URL + DID, server:member role for the DIDs being published as, and signing keys for those DIDs. From Forgejo it needs a webhook subscription, public-repo read access, and a Forgejo-user → DID mapping.


Tomorrow — Forgejo is the knot#

Instead of bridging Forgejo into a separate knotserver, teach Forgejo to be a knot. Same Forgejo binary, same codeberg.org hostname, with a module that exposes the knot lexicon surface backed by Forgejo's existing repo storage.

flowchart LR
    subgraph today["Today (above)"]
        f1["Forgejo"] -->|webhook| b1["bridge<br/>(separate service)"]
        b1 -->|XRPC + git push| k1["knotserver<br/>(separate process)"]
    end

    subgraph proposed["Forgejo as a knot"]
        forgejo["Forgejo<br/>+ knot-frontend module<br/>(or external sidecar)"]
        users(["users<br/>(unchanged web/SSH/REST)"]) --> forgejo
        forgejo -.->|"/xrpc/sh.tangled.*<br/>/events firehose<br/>Service-Auth on writes"| world["AT Protocol clients<br/>(tangled.sh appview,<br/>knotmirror, ...)"]
    end

    today -.->|same protocol surface,<br/>different deployment| proposed

The mapping is mostly mechanical — sh.tangled.repo.{create,delete,tree,log,branches,diff,...} lands on Forgejo's existing repo APIs in XRPC envelopes; sh.tangled.knot.listKeys reuses Forgejo's user SSH keys; identity reconciles via a did field on Forgejo's user table.

No-trivial part is sh.tangled.knot.subscribeRepos — an atproto-style sequenced WebSocket subscription with cursor resume and ConsumerTooSlow semantics. Forgejo's existing internal event hooks (the same ones driving its ActivityPub outbound delivery) are the natural source; you translate ref-update events into the lexicon's wire format and replay from a tangled_firehose_events table on reconnect.