ATFISL — Content Discovery with the AT Protocol
| date | 2026-10-01 |
|---|---|
| status | hot new stuff |
| editors | Daniel Norman <daniel@norman.life> |
| issues | list, 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:
- Every location record is signed as part of a repository commit, so announcements are attributable to a DID without any new identity or signature scheme.
- Records live in the publisher's own PDS. Publishers keep credible exit: they can move hosts and their announcements move with them.
- Indexers remain decoupled with no write API, they observe the network and index what they see.
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:
-
addrs(required): an array of one or more addr objects, each describing one location for the content. -
size(optional): the size of the content in bytes, as an integer. This is a hint for clients to plan retrieval; it is not verified by indexers. -
expires(optional): the time after which this record is no longer valid, as an AT Protocol Lexicon datetime string, which should meet the intersecting requirements of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards ([lexicon]). Publishers that want to keep content discoverable re-publish records before they expire.
Each addr object has the fields:
-
url(required): an absolute URL ([url]) from which the content can be retrieved. The scheme identifies the retrieval method, and with it how a client turns the URL and the CID into a request; there is no separate protocol field. Schemes may be introduced without a registry, and clients must skip URLs whose schemes they do not support. FISL ([fisl]) specifies retrieval. -
expires(optional): the time after which this addr is no longer valid, as a datetime string.
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:
-
Accept a record from a repository, obtained by subscribing to a firehose or relay for the
dev.fisl.atcollection, by backfilling repositories, or both ([at]). Configuration of which DIDs or collections to track is up to the indexer. - Verify the record's inclusion in a signed commit of the publisher's repository. Discard the record if verification fails.
- 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.
-
Discard the record if its
expiresis malformed, or in the past. Discard any entry ofaddrswhoseurldoes not parse as a URL ([url]), whoseexpiresis 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. -
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. - 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
- Location records are claims, not proofs. A publisher can announce URLs it does not control, or that serve wrong bytes. Client-side CID verification makes this a denial-of-service vector, not an integrity failure.
-
sizeis unverified. Clients must not allocate resources based on it without bounds.
Open questions
- This spec is pretty vague about more complex transfer protocols like iroh-blobs or a BAO based HTTP, or even UnixFS with IPFS trustless gateways. One additional protocol, e.g. iroh-blobs, should be explored to ensure protocol flexibility and to avoid overskewing the design for one protocol.
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/