Providers

Tangled

Use Tangled, a forge built on atproto: issues, pulls, comments, live events and webhooks.

The Tangled provider covers Tangled, a forge built on atproto. It reads and writes issues, pulls and comments. It also streams live events from Jetstream and verifies incoming push webhooks. Tangled has no reviews, checks, releases or repository contents API, so those capabilities are false.

DetailValue
Importforges/tangled
Factoriestangled(), tangledLite()
Authanonymous, app_password, oauth

Authentication

Reads are public and need no credentials. Writes need an atproto session, from an app password or from an OAuth client:

import { tangled } from 'forges/tangled'

const reader = tangled({}).create()

const writer = tangled({
  auth: {
    type: 'app_password',
    identifier: 'alice.example.com',
    password: process.env.TANGLED_APP_PASSWORD!,
  },
}).create()

With app_password, the provider creates a session on the account's PDS (personal data server). It finds the PDS in this order:

  1. The pds option, if you pass it.
  2. The DID document, if identifier is a DID.
  3. https://bsky.social.

With oauth, pass the did, the pds and a fetch from an atproto OAuth client that signs requests:

tangled({
  auth: {
    type: 'oauth',
    did: session.did,
    pds: session.pds,
    fetch: session.fetch,
  },
})

Instance and services

baseUrl defaults to https://tangled.org. The provider uses it for web links and as the instance.

Tangled records are stored on many servers, so the provider reads from several services. You can change each one:

OptionReadsDefault
plcUrlDID documents, to find each author's PDShttps://plc.directory
backlinksUrlRecords that point at a thread, such as commentshttps://constellation.microcosm.blue
recordsUrlRecords, through a Slingshot-compatible cacheEach author's PDS
jetstreamUrlThe live event streamA public Bluesky Jetstream instance
apiUrlrequest() and listSource: 'index'https://api.tangled.org

Refs and numbers

Issues and pulls are atproto records, identified by AT-URI. A ThreadRef has the AT-URI as both number and externalId. The repo's owner is the owner's DID, and RepoRef.externalId is the repo DID.

The numbers in the Tangled UI and in webhooks come from the appview, and records don't store them. The provider reports them as displayNumber.

A ref from a webhook has no number until it's matched to a record. To fill number during webhooks.ingest(), pass a resolveDisplayNumber(repo, kind, displayNumber) function that returns the AT-URI.

Thread state

A thread's state is the latest state record written by the thread's author, the repo owner or a collaborator. The provider ignores state records from other accounts. If another account calls close() or reopen(), the call throws InsufficientScopeError and writes no record.

Listing threads

By default, threads.list() reads every record and computes each thread's state, which takes several requests per thread. Set listSource: 'index' to list from the index at apiUrl. The index takes one request per page, but its states can lag behind the records. Every listing from the index has an index_possibly_stale warning.

Notifications

Notifications use the org.tangled.temp.notification.* lexicons. These lexicons are in Tangled's temporary namespace and can change. To enable notifications, pass auth and set notificationsUrl to the base URL of the notification service. The notification capabilities are then 'experimental'.

Stars and follows have no thread. They appear with kind: 'other', and typeRaw contains the notification type.

Merging

The knot that hosts a repository merges its pulls, with no review step. merge() and approveAndMerge() are unavailable.

Live events

sources.subscribe() streams events from Jetstream. Each item has a cursor. Save it, and pass it back to resume the stream:

for await (const { event, cursor } of reader.sources.subscribe({
  cursor: saved,
  signal,
})) {
  console.log(event.kind, event.summary)
  saved = cursor
}

To close the connection, abort the signal or break out of the loop. If the connection drops, the iterator throws SubscriptionClosedError with the last cursor. The provider doesn't reconnect, so call subscribe() again with that cursor.

Webhooks

Set webhookSecret to the secret configured on the hook:

const forge = tangled({
  webhookSecret: process.env.TANGLED_WEBHOOK_SECRET,
}).create()

const events = await forge.webhooks.ingest({
  headers: request.headers,
  body: await request.text(),
})

Tangled sends these headers with each delivery:

HeaderContent
X-Tangled-Signature-256sha256= followed by the hex HMAC-SHA256 of the body
X-Tangled-EventThe event name
X-Tangled-DeliveryThe delivery ID

A push delivery becomes a push or ref event. Tangled has no API to register hooks, so the webhooks management methods are unavailable.

Recording fixtures

Recording needs no credentials:

pnpm record-fixtures tangled

To record other threads, set FIXTURE_TANGLED_ISSUE and FIXTURE_TANGLED_PULL to AT-URIs. To record through a Slingshot cache, set TANGLED_RECORDS_URL.

Capabilities

Generated by pnpm capability-matrix; do not edit by hand.

CapabilitySupport
experimental❌
sources.poll❌
sources.webhook✅
sources.subscribe✅
repos.get✅
users.get❌
repos.listexperimental
repos.labels❌
repos.createLabel❌
repos.milestones❌
repos.collaborators❌
repos.permissionFor❌
repos.addCollaborator❌
repos.assignableUsers❌
repos.reviewerCandidates❌
threads.getissue, PR
threads.listissue (experimental), PR
threads.getMany✅
comments.listissue, PR
comments.editissue (experimental), PR (experimental)
comments.deleteissue (experimental), PR (experimental)
reactions.list❌
notifications.listexperimental
notifications.markReadexperimental
notifications.markDone❌
notifications.unsubscribe❌
notifications.markAllReadexperimental
notifications.markAllDone❌
notifications.unreadCountexperimental
writes.commentissue (experimental), PR
writes.upsertCommentissue (emulated), PR (emulated)
writes.closeissue (experimental), PR
writes.reopenissue (experimental), PR
writes.createissue (experimental)
writes.updateissue (experimental), PR (experimental)
writes.setLabels❌
writes.addLabels❌
writes.removeLabels❌
writes.setMilestone❌
writes.react❌
writes.setAssignees❌
writes.requestReview❌
writes.merge❌
writes.approveAndMerge❌
writes.transfer❌
writes.markDuplicate❌
subscriptions.getissue (experimental), PR (experimental)
subscriptions.setissue (experimental), PR (experimental)
installations❌
checks.thread❌
checks.list❌
checks.report❌
checks.rerun❌
ci.runs❌
ci.run❌
ci.jobs❌
ci.log❌
contents.file❌
contents.tree❌
contents.branches❌
contents.tags❌
contents.resolveRef❌
contents.commits❌
contents.commit❌
contents.compare❌
contents.threadFiles❌
contents.threadCommits❌
reviews.list❌
reviews.create❌
reviews.submit❌
reviews.approve❌
reviews.resolveThread❌
releases.list❌
releases.get❌
releases.latest❌
releases.getByTag❌
releases.downloadAsset❌
webhooks.list❌
webhooks.create❌
webhooks.update❌
webhooks.delete❌
webhooks.rotateSecret❌
webhooks.deliveries❌
webhooks.redeliver❌
search.threads❌
search.repos❌
search.commits❌
securityAlerts❌
eventKindsnative
authanonymous, app_password, oauth
limitsunknown
Copyright © 2026