ATFISL — Content Discovery with the AT Protocol

date2026-10-01
statushot new stuff
editorsDaniel Norman <daniel@norman.life>
issueslist, new
abstract

ATFISL is a content discovery system for CIDs built with the AT Protocol. Publishers announce download locations for CIDs by publishing records to their AT Protocol repositories. Indexers aggregate these records into a lookup table from CIDs to live locations. ATFISL is complemented by FISL which defines an HTTP query API for ATFISL records.

Introduction

Content-addressed resources are self-certifying: a client that knows a CID can verify the bytes it receives, no matter who serves them ([ipfs-principles]). This leaves one problem open: given a CID, where do you fetch it from?

RASL ([rasl]) answers this with inline hints, but hints are static: they are baked into the URL at the time of writing and go stale. ATFISL makes discovery dynamic. A publisher announces "this CID is retrievable at these URLs, until this time" and can update or retract the announcement at any moment.

ATFISL uses the AT Protocol ([at]) as its publication layer rather than defining its own. This is a deliberate trade:

An indexer can be either scoped or global. You run one for your own data consortium, mirror network, or application, and configure it with the DIDs or collections it should track.

Location Records

A location record announces that one CID is retrievable at one or more URLs. Records use the dev.fisl.at collection in the publisher's repository, keyed by the announced CID.

{
  "$type": "dev.fisl.at",
  "addrs": [
    { "url": "https://berjon.com/.well-known/rasl/bafkreifn5yxi7nkftsn46b6x26grda57ict7md2xuvfbsgkiahe2e7vnq4" },
    { "url": "https://mirror.example.com/kitten.jpg", "expires": "2026-09-01T00:00:00Z" }
  ],
  "size": 94201,
  "expires": "2026-09-18T00:00:00Z"
}
      

The fields are:

Each addr object has the fields:

The record's expires is a ceiling: the effective expiry of an addr is the earliest of its own expires and the record's, whichever of the two are present. An absent expires states no expiry; it does not mean that the addr or the record is expired. An addr past its effective expiry is treated as absent. This lets a record mix locations with different lifetimes — a stable mirror next to a short-lived one — without re-publishing the record on the shortest lifetime's cadence. Expiry exists because retrieval locations rot: domains lapse, certificates expire, servers move.

Records and addr objects may carry additional fields, for example to signal capabilities of an endpoint that the scheme alone cannot express. Indexers must preserve fields they do not recognize and clients must ignore them.

The record key is the string form of the announced CID. It must be a valid DASL CID ([cid]) or BDASL CID ([bdasl]); no other CID formats are supported. The record carries no CID field: the key alone identifies the content. This gives each repository at most one live record per CID, which fixes the meaning of every repository operation: a put creates or replaces the announcement, and a delete retracts it. There is no separate update or revocation mechanism, and no way for a repository to hold two records that disagree about the same CID. A DASL CID is a conformant record key with no escaping or transformation ([record-key]): its string form is 59 characters of lowercase base32 including a b prefix, so it falls inside the permitted character set, well under the 512 character limit, and free of the case-sensitivity hazard that record keys otherwise carry. A BDASL CID has the same string form, because BDASL changes only the hash type byte ([bdasl]).

The key also makes a publisher's PDS a discovery endpoint on its own: getRecord with the CID as the record key answers "does this DID serve this CID?" in one request, with no indexer ([repository]).

Publishers pay for this in two ways. To add or remove one addr, you must read the record, edit addrs, and put it back. Use the swapRecord parameter of putRecord to make the put conditional on the record you read, and retry if it conflicts. And two independent writers cannot announce the same CID side by side — each rewrite replaces the other's addrs. Treat one CID in one repository as owned by one writer. Publishers that refresh many records at once should batch them into one commit with applyWrites.

Lexicon

The Lexicon definition ([lexicon]) for location records is:

{
  "lexicon": 1,
  "id": "dev.fisl.at",
  "defs": {
    "main": {
      "type": "record",
      "description": "An announcement that the content identified by the DASL or BDASL CID in the record key is retrievable at one or more URLs, until an expiry time.",
      "key": "any",
      "record": {
        "type": "object",
        "required": ["addrs"],
        "properties": {
          "addrs": {
            "type": "array",
            "description": "Locations that serve the content.",
            "items": { "type": "ref", "ref": "#addr" },
            "minLength": 1,
            "maxLength": 32
          },
          "size": {
            "type": "integer",
            "description": "Size of the content in bytes.",
            "minimum": 0
          },
          "expires": {
            "type": "string",
            "format": "datetime",
            "description": "Time after which this record is no longer valid. Ceiling for the expiry of every addr."
          }
        }
      }
    },
    "addr": {
      "type": "object",
      "description": "One location that serves the content.",
      "required": ["url"],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Absolute URL for the content. The scheme identifies the retrieval method.",
          "maxLength": 2048
        },
        "expires": {
          "type": "string",
          "format": "datetime",
          "description": "Time after which this addr is no longer valid. Defaults to, and is capped by, the record's expires."
        }
      }
    }
  }
}
        

The key type is any because Lexicon has no pattern-based key type: of tid, nsid, literal:<value>, and any, only any admits a CID ([record-key]). The constraint that the key is a DASL or BDASL CID can therefore only be stated in prose, as above, and checked by indexers. Likewise, format: uri is looser than the normative rules above; the prose governs.

Indexing

An indexer builds a lookup table from CIDs to live location records. Use the following steps to index a location record:

  1. Accept a record from a repository, obtained by subscribing to a firehose or relay for the dev.fisl.at collection, by backfilling repositories, or both ([at]). Configuration of which DIDs or collections to track is up to the indexer.
  2. Verify the record's inclusion in a signed commit of the publisher's repository. Discard the record if verification fails.
  3. Parse the record key per the steps to parse a string-encoded CID, accepting the BDASL hash type extension ([bdasl]). Discard the record if parsing fails.
  4. Discard the record if its expires is malformed, or in the past. Discard any entry of addrs whose url does not parse as a URL ([url]), whose expires is malformed, or whose effective expiry is in the past. Do not discard an addr whose scheme the indexer does not recognize. Discard the record if no entries remain.
  5. Store the record, indexed by that CID, together with the publisher's DID, which FISL serves as the record's publisher ([fisl]). Replace any record previously stored for the same DID and CID.
  6. On a delete event for a stored record, remove it.

Because a repository holds at most one live record per CID, an indexer needs no more state than a single row per (did, cid) pair, and no reconciliation logic: a put is an upsert on that key, a delete is a delete, and expiry of the row is a comparison against the record's expires. An indexer that loses its state can rebuild it by replaying repositories from scratch.

Where two events concern the same (did, cid), the one from the later repository commit wins. Commit revisions (rev) are TID-format logical clocks that increase monotonically within a repository and sort lexicographically, so the comparison is a string comparison ([repository]). An indexer that processes each repository's events in order needs nothing further; one that backfills and follows a firehose concurrently should record the rev alongside each row and ignore any event whose rev is not greater, so that a late-arriving put cannot resurrect a retracted record.

Indexers store what publishers said, not what is true: they do not fetch the URLs, verify that the content is there, or interpret URL schemes. Verification belongs to the client at retrieval time; that is the point of content addressing. FISL ([fisl]) specifies how clients query the index and verify what they retrieve.

Security Considerations

Open questions

References

[at]
AT Protocol. URL: https://atproto.com/
[bdasl]
Robin Berjon, Brendan O'Brien, & Juan Caballero. Big DASL (BDASL). 2026-10-01. URL: https://dasl.ing/bdasl.html
[cid]
Robin Berjon & Juan Caballero. Content IDs (CIDs). 2026-10-01. URL: https://dasl.ing/cid.html
[fisl]
Daniel Norman. FISL — Finding Internet Structures & Links. 2026-10-01. URL: https://dasl.ing/fisl.html
[ipfs-principles]
Robin Berjon. IPFS Principles. march 2023. URL: https://specs.ipfs.tech/architecture/principles/
[lexicon]
AT Protocol: Lexicon.
[rasl]
Robin Berjon & Juan Caballero. RASL — Retrieval of Arbitrary Structures & Links. 2026-10-01. URL: https://dasl.ing/rasl.html
[record-key]
AT Protocol: Record Key. URL: https://atproto.com/specs/record-key
[repository]
AT Protocol: Repository. URL: https://atproto.com/specs/repository
[url]
WHATWG. URL. Living Standard. URL: https://url.spec.whatwg.org/