One of the best features of the existing Atmosphere is how transparently AT URIs express the underlying data model. This is already true for public records, + we should strive for equally clear URIs for records which are only accessible via space credentials.

To do so, we need the right mental model for what spaces are.

URIs for public records

In atproto, any lexicon reference to public data has clear semantics. Collections of records belong to users, + the URI layout unambiguously expresses that:

at://RECORD, where
RECORD = {authority}/{collection}/{rkey}
or colloquially {who}/{what}/{which}

Every record in a user's collection has the same type + belongs to the same authority (a DID or handle). A complete URI also specifies an rkey, to indicate which individual record you're referring to.

The entire collection is canonically stored together in that user's public repository on whatever Personal Data Server (PDS) they choose to use. From there, users choose which applications they authorize to maintain each collection (via OAuth).

Public views aggregate records

The cool thing about Atmospheric apps is how easy it is to assemble views which aggregate collections of records from multiple authors. There's a broad spectrum of ways to create such views:

  • (server-side) building your own custom AppView

  • (hosted) leveraging a platform like

  • (client-side) ad hoc queries to a generic index like

Regardless of how you do so, these views all share the same tacit data model -- it's an aggregation of all records for a given collection type from anyone publishing to the Atmosphere, each of which has a unique URI following the same pattern:

at://VIEW, where 
VIEW = */{collection}/*

Copies of each record accessible via a such a view can be identified by URI. The same is true for all other AT URIs mentioned within the data for those records.

How do spaces change this?

As protocol designer Daniel Holmgren described in his most recent leaflet, the goal is for spaces to feel as Atmospheric as possible:

Our next goal is to maintain existing atproto intuition. Users publish records, those records are canonically stored in the user’s PDS, authority rests in the author’s DID, and applications crawl PDSes and sync data to build their own views.

However, instead of crawling the entire public Atmosphere for records to include in those views, spaces explicitly delineate a much smaller subset of records to include:

Spaces are meant for establishing a shared social context. Records are in that context, and everyone with access to that context has the exact same view over it. It’s a shared common reality, and this shared sense of space allows for truly social applications.

In both cases, you're building a view by aggregating specific collections of records stored on users' PDSes. The only difference is where you look:

  • public = everywhere

  • space = just "there" (ie, on "those" PDSes)

To decide where "there" is requires a space host, who defines the scope of the space + hands out credentials allowing space members to access relevant records from "those" PDSes.

Where do spaces live?

For a space to exist, it needs to be defined, hosted, accessed, + assembled into a view. The definition, naturally, is itself a URI:

at://SPACE, where 
SPACE = {spaceDid}/space/{spaceType}/{skey}

The type = "space" record found at that URI is a space type declaration describing which kinds of collections can be accessed via this space:

{
  "lexicon": 1,
  "id": "com.atmoboards.forum",
  "defs": {
    "main": {
      "type": "space",
      "description": "A discussion forum",
      "key": "any",
      "name": "AtmoBoards Forum",
      "name:lang": { "es": "Foro AtmoBoards", "ja": "AtmoBoards 掲示板" },
      "collections": ["com.atmoboards.thread", "com.atmoboards.reply", "org.example.reaction"]
    }
  }
}

That space is owned by a space authority which lives at the spaceDid from that URI. That authority in turn delegates the job of identifying who belongs to that space -- both readers + writers -- to a spaceHost service listed in the DID document for that authority. Because a single authority may want to manage multiple spaces of this spaceType, a complete space URI also includes an skey to indicate which one you're talking about at any given time.

Thus, one URI encapsulates everything needed to define + host a unique space.

Where do space records live?

This is the tricky one. People access these collections via a space-constrained view, but individual records still live on the PDSes of whoever owns them.

For a record to be visible in a space requires the cooperation of two authorities, not one:

  • the record owner (author) +

  • the space host

Anyone can write records "for" a space to their own PDS, but nobody else can sync those records out of there into a consensus view for that space without:

  • a credential minted by the space host

  • being accepted by the owner's PDS

  • per a prior OAuth consent choice made by the owner.

That's a long sentence, but read it again carefully, because it's a subtle detail that's often missed by people studying the sync protocol for spaces. In the same way that existing appviews can decide which records or PDSes they want to ignore (usually for moderation reasons), space hosts MUST be able to definitively decide which authors' content are sync-able into views for the spaces they manage (which isn't necessarily all members who can read that space).

So the space that matters to everyone else is NOT the set of all PDSes who've ever written content "for" it, it's the aggregate view managed by the space host on behalf of whichever space members currently can read it.

In short, spaces are a view, not a container.

Implications for space record URIs

Sadly, URI layouts in the current alpha implementation for spaces imply that records belong to spaces:

at://SPACE/RECORD, or
at://{spaceDid}/space/{spaceType}/{skey}/{authorDid}/{collection}/{rkey}

But that's misleading. Spaces are views, not containers. Records belong to users, + are included in spaces, so the URI layout should reflect that:

at://RECORD?space=SPACE, where
RECORD={authorDid}/{collection}/{rkey}, and
SPACE={spaceDid}{spaceType}/{skey}

Given this updated syntax for accessing space-constrained records, what would view aggregation look like?

at://VIEW, where 
VIEW = */{collection}/*?space=SPACE and 
collection ⊂ SPACE.spaceType.collections

Note that this proposed change is all about making the URI resolution process more transparent + expressive. Think of it as syntactic sugar, which in no way changes how current alpha PDS implementations store or negotiate access to local repositories of space-constrained records.

Specifically, notice how well resolving URIs in this format rhymes with what's happening at the protocol level. What's the protocol asking the PDS to do with each of these URIs?

  • public sync = access at://RECORD

  • permissioned sync = access at://RECORD?space=SPACE, using credentials for that space

The reason you're using different sync mechanisms -- to build a global public view vs. a space-constrained view -- is right there in the URI format. It's not buried as an exception at some arbitrary token depth.

Record variants

To be clear. I'm absolutely not proposing anything like "Circles" here.

The space-centric features of the current design -- locality, constraining "modality" to the set of collections identified in the space type declaration, even the implementation detail of storing each space repo separately within a given PDS -- should all stay intact. This isn't an end run to sneak record-level permissioning back into the design, a la circles. That way lies madness.

Instead, the goal here is just to tidy up the URI format -- now, before leaving alpha -- to better express the "views, not containers" aspect of the existing design. (Getting dual sync protocols through the IETF ATP process down the road will be hard enough without billions more ugly URIs out there littering the world. We don't need a repeat of the existing :// syntax mess.)

However, it's absolutely worth asking the question of what the semantics might be if any/all of the following do happen to coexist within the same PDS:

at://RECORD
at://RECORD?space=SPACE1
at://RECORD?space=SPACE2, where
RECORD is identical, but SPACE1 != SPACE2

If we continue to use the current implementations, where those three variants of the "same" record are each stored separately -- one in the author's public repo, the others in their SPACE1 + SPACE2 repos -- how significant is the similarity of their URIs?

If I write a record elsewhere with a Ref to that at://RECORD, might it ever be valid for an application to construct views which connect it to any of those other record variants? Does the protocol forbid or discourage that? Why? Can applications allow it, especially with more knowledge about the semantics of the collection within RECORD, +/or the respective spaceType(s) within SPACE1 vs. SPACE2?

At some point in the future, might we want to extend the PDS spec to make it easier for a user to migrate their record collections between spaces? Why or why not? And if so, which URI format would make it more natural to do?

Potential use cases

Without going into detail, I can imagine all sorts of scenarios where an app designer might want to pair up -- or migrate -- variants of the "same" record across different contexts:

  • Drafts (space=DRAFT <--> public)

  • Time-delay publishing (space=PAID --> public)

  • Bonus features in sidecars (public && space=EXTRAS)

  • Toggling account privacy (public <--> space=FRIENDS)

  • Going dark for a single app (public --> space=ME)

  • Forking a space (space=HIJACKED --> space=RESCUED)

  • etc.

As always, note that these features are all deliberate context shifts, expressed by changing the URI to reference a variant of that record in a different space.

Why URI design matters

So much of #AtmosphericUX design is lexicon design. I can't overstate the importance of thinking through your data models + future-proofing your URIs to make it easier for everyone else to do cool things down the road with the data you're storing now. When designing for use cases like these, it all boils down to one question:

What do you want the URIs to look like when writing references into PDSes everywhere?