In this article, I will propose a new architecture to make Tangled fully decentralized, mainly about collaborative objects (COBs). I will explain what we are lacking today, how can we solve it, and what are the limitations of my own proposal.
DISCLAIMER: All future directions introduced in this article are just my personal proposal and not an official plan. We haven't fully settled down our plan yet and there is high possibility for us to choose completely different approach. We just thought it is interesting to share publicly to gather more opinions from users, so keep in mind that I'm not representing the Tangled in any sense from this article.
NOTE: This article is pretty long explaining all the backgrounds, what we've tried and how that fails, and all the big and small reasonings behind my choice. If you are ready to face the new architecture and don't have time to read all this, go straight to the summary.
How Tangled handles COBs today
Currently, author of the issue owns the issue record. The record content is stored in author's PDS. Maintainers cannot control anything about those issue records. To allow maintainers collaboratively modify the issue state, Tangled had to make issue/PR states as dedicated records like sh.tangled.repo.issue.state and sh.tangled.repo.pull.status where last ingested one overrides the old state.
For more complicated collaborative objects like issue/PR labels... we basically implemented a CRUD system on top of existing atproto CRUD system. We have sh.tangled.label.op record which represents add/update/delete operations. These records represent a modification instead of specific state and appview is expected to ingest everything in correct order, then amend everything to the final state. Here, we just assume label operation records to not be deleted or modified and always ingested in correct order and nothing will ever be deleted.
Even when initial label adding label:duplicate is deleted from the network, appview just remembers it to preserve current state.
This current architecture becomes way more complicated when we consider collaborators can also be removed.
- 1.
Alice adds Bob as collaborator.
- 2.
Bob close or label some issues. These new states are stored in bob's PDS.
- 3.
Bob is removed from the collaborator.
- 4.
How can new appview restore final state of issue states or labels? What if bob remove
issue.stateorlabel.oprecords for the repository bob isn't belonged to?
Our current solution is to just... not allow removing a collaborator.
Even that doesn't fully work. What if Bob nukes his account? Suddenly all bob's work to that repo will disappear from entire network. This should not happen and obviously not an expected behavior in collaboration platform. For example, we don't expect old contributor to be able to nuke all their codes and commits from the project without permission just because they want to. Current Tangled appview is working fine because we use our own DB as a source of truth collecting all events in real-time since the very beginning, validating them just as we received them.
Because appview is collecting distributed issues for each repositories, the issue/PR ids are appview-local and doesn't work universally.
This situation is not good both for feature-wise and architecture-wise. We want full decentralization while acting as a working contribution platform. Maintainers should be able to restore the collaborated state from scratch without depending on centralized service holding all the cache.
Recap about did-for-repo situation
Before going further, let me do a recap of the did-for-repo situation for people who haven’t been following recently.
I heavily recommend reading the repo lifecycle post by Nelind because it can give you really good insights about "repository having a DID" concept.
Tangled recently switched the git repository identifier from sh.tangled.repo record AT-URI to plain DID. This is to allow repository transfer to different owner without loosing all the related data like stars, issues and PRs. We are still in progress of migrating existing records using the AT-URI as a reference, but eventually DID will be the preferred way to reference the git repository.
How repo renaming works today
While DID of the git repository represents the existence, owner still has sh.tangled.repo record to claim the ownership of that repo. Basically we do two-way verification for the repository ownership:
A repo claim its current owner and name through
{knot}/xrpc/sh.tangled.repo.describeRepo.A user claim the repository ownership by having
sh.tangled.repo/<repo-slug>record in their PDS.
When there are more than one sh.tangled.repo records pointing to same git repository DID, we treat invalid ones as "old repository name" and redirect to current name.
For example, when alice renames repo-a to repo-b,
- 1.
appview sees
/alice.com/repo-a - 2.
appview resolves the repoDid pointer from
at://alice.com/sh.tangled.repo/repo-arecord - 3.
from resolved repo DID, appview resolves the valid owner & repository slug:
/alice.com/repo-b
This way, we can easily rename repo and even have redirections for old names. Same architecture can be used for future repo owner transfer too.
Few things I'd like to change further
- 1.
As
sh.tangled.repocollection is not about git repository itself anymore and more about the ownership, change the collection NSID fromsh.tangled.repotoorg.tangled.repo.ownershipfollowing official lexicon NSID guideline. - 2.
For same reason, all metadata like
description,websiteandspindlewon't belong to this record.org.tangled.repo.ownershiprecord will only contain the git repository DID and nothing else. - 3.
All those metadata should be owned by the git repository identity. I'll discuss about this later in this article.
Atproto Repository vs Tangled Git Repository
Now "atproto repository" and "Tangled git repository" are pretty similar:
| | atproto repository | tangled git repository |
|----------------|--------------------|------------------------|
| identity | did:plc / did:web | did:plc / did:web |
| host service | PDS (#atproto_pds) | Knot (#tngl_knot) |
| data structure | MST | MST (git) |
| data format | CBOR | raw git blobs |
| event stream | firehose | knotstream |
| fetch/sync | xrpc | git |Git-repository-owned data
Due to the limitations of our current model and the context where repos can now exist as standalone identity without belonging under immutable owner, it's obvious that we should move some of the data under the git repository itself to achieve full decentralization. Since repo owner can also change, any data owned by the git repository must be editable by any arbitrary user with correct permissions. The owner is the first collaborator of the repository. Therefore, we refer to all repository-owned data as collaborative objects (COBs).
Here is a list of data that should be owned by the git repository.
identity
current owner and repository name
metadata
settings (description, website, spindle, etc)
collaborator list including permissions
contributions
codes and commits
issues and PRs (including their state,label,title and body)
Decision to store issues and PRs under the git-repository's identity will bring up lots of questions which I will answer later in this article. For now, let's focus on a fact that "Git repo needs to own some amount of structured data" because identity and metadata parts are pretty obvious ones.
Things like stars and comments won't be included because we don't expect git repository to verify those. If maintainers value comments and want to own those, they can just import those to issues.
Attempt 1: Can we merge Knot and PDS?
My first thought was to assign a PDS to the git repository. But then users need to host two separate services just to self-host their own git repos. The syncing can also be a problem. We currently use git to sync the git objects and it's unlikely for us to change that just to use more atproto infra. Which means if we assign both PDS and Knot to a git repository, we will basically have two separate MST synced by two separate protocols.
It's unrealistic to believe official PDS spec to include the Knot spec.
It's unrealistic to believe Knot to be PDS-compatible. (We either reinvent git or have two separate MST synced by two separate protocols)
Even if we decide to make Knot become PDS-compatible while serving git in parallel, we should blindly guess that #atproto_pds service in specific DID to be Knot-compatible. Which, can technically work by fetching Knot-specific xrpc methods but requiring a fetch just to ensure the service type is not ideal. Especially when we can define clear service type in DID document.
The Knot needs its own spec and that indicates having Tangled-specific service namespace. Having application-specific service isn't crazy, Bluesky also has its own dedicated services like #bsky_chat and #bsky_feedgen.
Attempt 2: How about T-log service?
One proposed solution is to have a general t-log service that cryptographically guarantees the timing/ordering of events. But it has several problems:
As mentioned above, records or entire account can be deleted. Even we know the correct order of events, we cannot restore the exact state without their content.
Maintainers need a control over their issue list. (This will be covered in details below)
Example scenario - missing records
- 1.
Alice adds Bob as collaborator with limited permissions. Bob can edit codes but cannot invite other collaborators
- 2.
Bob tries to invite new collaborator Charlie and get rejected from Knot ACL
- 3.
Bob leaves the team
- 4.
New stateless appview tries to backfill the data. It cannot know if old collaborator bob had permission to invite other collaborators because that collaborator record is now deleted.
T-log service is about the verification and not about data syncing. "When this event happened" is a thing that is missing from current ATproto spec and doubtlessly a good-to-have feature. But it doesn't solve the real problem we are suffering with. For T-log service to work, it should store all event history alongside with their contents and that's essentially what I'm going to suggest.
We are already storing structured data in git
If you think about it...
.gitattributedefining linguist/diff information.tangled/workflows/*.ymldefining CI pipelinesother meta files like
FUNDING.ymlor even.git/description(yeah, only one service is using it but spec is spec)
All these informations can be stored outside of git but included in git for convenience and simplicity. It's not that crazy to store structured data in git. Imagine a world where you can fetch all public data related to the repository via single command: git clone --mirror.
How Radicle handles same problem
Radicle exactly does this. It uses git to store structured data. They store every records under the git repository in special refs like refs/rad/id and refs/cobs/<nsid>/* and sign entire repository state using refs/rad/sigrefs. source
Proposed Architecture
Just store COBs in git!
Make Knot more PDS-shaped with ability to store records (but not actual #atproto_pds in any sense.)
We already have MST based data structure in Knot and already syncing them pretty similar to syncing PDS data, so why not just use that? We can follow the same architecture from Radicle. We can either use refs/tngl/sigrefs or just reuse refs/rad/sigrefs.
$ git cat-file -t refs/tngl/sigrefs
blob
$ git cat-file -p refs/tngl/sigrefs
9767b485c2aad1e23097d2b5165287ba84cfa452 refs/heads/master
088bdecc8d3b64f1d69638e75daeadcaca387b60 refs/cobs/org.tangled.repo.identity/self
f3eaa7454e3a4714885905ae99f616fc7895b5fa refs/cobs/org.tangled.repo.collaborator/3mlof4wfodj22
0590b78ee42b39087983e4de04164065e5aa11bc refs/cobs/org.tangled.repo.collaborator/3mhgbkz4ruw22
...
$ git cat-file -p refs/cobs/org.tangled.repo.collaborator/3mhgbkz4ruw22 | jq
{
"subject": "did:plc:3fwecdnvtcscjnrx2p4n7alz",
"createdAt": "2026-03-19T17:04:18+02:00",
"permissions": [...]
}I'm not sure if cobs should be JSON or CBOR. JSON makes sense considering ease of debugging, while CBOR can be slightly more efficient.
Extra benefits of doing this
Few extra benefits that I didn't covered from above sections:
ACL computing becomes way easier since git repository has only one data storage service. We don't have to sync permission sets across multiple services.
For example, branch protection and permission set over COBs are basically same thing.
We get version control system for free (might need some arrangement on how we structure the data, but full blown VCS is there to be used)
PR rounds can benefit a lot from this.
Dual-signing COBs
We should still allow users to sign their own issues and PRs to verify they actually made that operations. But in same time, it is git-repository's bug tracker so they should also be able to sign and own those issues. Both repo & author should agree with the existence of the collaborative objects. To achieve this, we can create a detached signature by creating a record in their PDS including the hard reference (I mean the strongRef) of the original record. Users can have similar effect of signing by creating sh.tangled.cob.signature records.
I said strongRef but we will be using our custom schema because it isn't pointing to the atproto repository.This way, we can allow both users and git-repository to verify the issue while issue content preserved under the git-repo's identity.
Example scenarios
Case 1: Opening an issue
Bob opens an issue in Alice's repo.
- 1.
Bob creates an issue by calling
{knot}/xrpc/org.tangled.cob.createRecord - 2.
Knot checks the permission. Any users can create
org.tangled.issuerecord in alice's repo, so it accepts that operation, creates a record and return it with the git object oid (similar to CID of atproto records) - 3.
Issue creation is completed at this step, but it's not yet verified by Bob.
- 4.
Bob verifies the operation by creating
/org.tangled.cob.signaturerecord in their own PDS, referencing the record with received oid. - 5.
Now the issue is verified. Appview links to the Bob's identity in UI.
Case 2: Editing an issue
Charlie who is collaborator of Alice's repo found Bob's issue is duplicate of old one so close it as duplicate.
- 1.
Charlie calls
{knot}/xrpc/org.tangled.cob.putRecordto update issue label and its state. - 2.
Knot checks the permission. Charlie has a permission to edit the issue labels and states. So it accepts that operation, update the record and return it with new oid.
- 3.
Issue is closed at this step, but it's not yet verified by charlie.
- 4.
Charlie verifies the operation by creating
/org.tangled.cob.signaturerecord in their own PDS, referencing the record with received oid. - 5.
Now the issue is fully verified. Appview links to Charlie's identity in UI as "Charlie closed issue #123 as duplicate".
Mind-shift of issue ownership & lifecycle
With this proposal, I'm changing the issue ownership model from atproto-style to self-hosted-git-forge-style, where maintainers have full control over submitted issues. They can rephrase the title, change states or even delete the issues from their own repository.
Because issues and PRs are now stored in Knots, writing something in your PDS doesn't guarantee you the record creation. All your operations, creating, updating, and deleting issue should pass the Knot and be verified by the git repository. If maintainer doesn't allow authors to delete their own issue, you cannot delete it. You can un-verify it by removing the signature stored in your PDS, but you don't have full control over the lifecycle.
Someone used to atproto-style "only you own your stuffs" might be frustrated in this decision.
Why issue should be owned by the git repository? Can't we only put things like issue state or labels in Knot while leaving issue title and body stored in users PDS? It feels like issue lifecycle should be controlled by users, in their own PDSs. If user deleted the issue, the repository should not be able to backfill it.
The reason why I think this model is ok and should work like this is because I see issues as contributions. Just like commits, your issues are contributions since their initial creation. The repository's state is now built on top of your contributions. If you want to remove it, just like git commits, you usually just amend it passing the verification step from the git repository again. You can completely delete it by rebasing the history, but that also needs the verification from repository maintainers. Once you contribute, the issue is not just yours. You've already contributed to the repository and you cannot undo that operation.
For example, something like this should not happen:
- 1.
Bob create "amazing useful issue" in Alice's repo.
- 2.
Maintainers are happy, so they decide to leave it.
- 3.
Bob edit the issue to "this repository sucks!" without permission.
Or in case of deletion, this should be avoided:
- 1.
Bob create "amazing useful issue" in Alice's repo.
- 2.
Maintainers are happy, they mark it as good-first-issue.
- 3.
Bob nukes his entire atproto account deleting several good-first-issues in Alice's repo.
To be clear, you still have ability to verify and un-verify your operation without the confirmation from the repository. If you un-verify your work, your work will be still there but you can still claim that you didn't do it.
FAQ
How about spindle?
Simple, Spindle is basically a Bluesky feedgen in Tangled. Both services serve automated, dynamic data for subscribed identities. The data isn't synced but fetched on-demand.
| | Bluesky Feedgen | Tangled Spindle |
|-------|---------------------------------|--------------------------------------------------------|
| data | dynamic, unsigned | dynamic, unsigned |
| sync | on-demand fetch | on-demand fetch |
| fetch | `app.bsky.feed.getFeedSkeleton` | `/logs` (can be `sh.tangled.ci.subscribeLogs` instead) |NOTE: Of course we can sign and sync the completed pipeline logs, but fundamental model is same as feedgen.
So basically it is a solved problem. We can follow the exactly same model of Bluesky Feeds. Git repos won't sign or own the pipelines because they don't explicitly trigger those. Instead, git repos can have full control over which spindle to use just like you have a control on which feed to use.
This leaves a fact that we might allow assigning multiple spindles to a git repository as several users requested, but that needs more discussion which I won't cover in this article.
Isn't that public write access to the Knot? DDoS?
Yes, and that's the whole point of being open-source. You accept contributions from open network. It doesn't matter cause you have full control on your own issue list. You can close them, delete them or even disable ANY submissions and just treat the entire repository as source-available. DDoS is a problem of being open-source. It's up to you to deal with them.
How will appview treat unverified COBs?
Just like unverified git commits. The appview will show them, but with "unverified" mark. Or we just don't render the author's profile or and don't link to their account at all when the issue is not verified by that user. We will be super clear when the record isn't verified, but we will still show them without connecting to your identity.
Summary
What this proposal is changing in high level:
To achieve decentralization with full backfillability, we are going to move collaborative objects (COBs) to Knot, owned by the git-repository's own identity.
This includes contributions like issues and PRs which are currently stored in contributor's PDS (things like comments and reactions are not included.)
Tangled will be completely decentralized!
For users:
When creating issues or PRs, you call xrpc methods to git repository's Knot, thus, you need git repository's permission for those operations.
You are still able to verify or un-verify your issues and PRs without the git repository's permission.
For maintainers:
You will be able to fully control your own issue list. You can rephrase the title or even delete it completely.
You can completely disable the issues and PRs.
You can keep issues and PRs even when original author wants to delete them (they can unverify their work though.)
For Knot hosters:
Knot won't listen to atproto firehose or jetstream anymore.
Contributors to the git repository will directly create records against the Knot.
Misc:
issue ids and PR ids will work universally.
we can easily have version history of issues and PRs. When they were closed, reopened, labeled, edited.
Basically I am giving more power to the maintainers in this proposal.
Again, this is yet unconfirmed proposal and I'm not representing the entire Tangled team's opinion. We haven't fully decided our direction yet, so we want to hear what you think. Would these changes be ok? Do you have better ideas? Please share your thoughts if you have any :)