Smoke Signal Blog

ATProtocol Record Hydration: Building Privacy-Aware Views

Published by @smokesignal.events on 2025-08-01 18:00 UTC.

In my previous posts, I explored ATProtocol Record References and Off-Protocol Data. Now, I want to dive deeper into a pattern that I believe is worth exploring: on-demand record hydration for privacy-sensitive data.

The Challenge: Location Privacy in Event Management

Smoke Signal is an event and RSVP management platform built on ATProtocol. One of our core challenges involves location data—the fundamental "where and when" of human gatherings. Events happen at:

Each type demands different privacy considerations. How do we share location details with attendees while keeping them confidential from the broader network?

Current Approaches and Their Limitations

The Naive Approach: Full Disclosure

The simplest solution embeds complete location data in event records:

{
    "$type": "community.lexicon.calendar.event",
    "name": "Annual Cookout",
    "locations": [{
        "$type": "community.lexicon.location.address",
        "name": "Orchardly Park",
        "street": "2599 Delaine Ave",
        "locality": "Oakwood",
        "region": "Ohio",
        "postalCode": "45419",
        "country": "US"
    }]
}

This works for public events but fails for semi-confidential gatherings where organizers want to control who sees exact locations.

The Manual Approach: Partial Records

We can embed partial location records:

{
    "locations": [{
        "$type": "community.lexicon.location.address",
        "country": "US",
        "locality": "Oakwood",
        "region": "Ohio"
    }]
}

Organizers then share full details through side channels—DMs, emails, or private forum posts. This works for one-off events but doesn't scale. It creates friction, increases the chance of information getting lost, and makes it harder to update locations dynamically.

A Better Way: Confidential Record Hydration

What if we could serve different views of the same record based on who's asking? Enter source-based hydration.

The Pattern

Here's how a hydrateable location record looks:

{
    "locations": [{
        "$type": "community.lexicon.location.address",
        "country": "US",
        "locality": "Oakwood",
        "region": "Ohio",
        "source": {
            "vary": "authenticated",
            "subject": {
                "uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/community.lexicon.location.address/01k1kg8n491p421zgqrst02wtf",
                "cid": "6ca46e6724.."
            },
            "service": "did:web:locations.dayton-pokemon.club#ClubPlacesProvider"
        }
    }]
}

The source field signals that this record can be expanded by calling an external service. Key components:

How It Works

  1. Service Discovery: The AppView resolves the DID and finds the service endpoint:
{
    "id": "#ClubPlacesProvider",
    "serviceEndpoint": "https://locations.dayton-pokemon.club",
    "type": "AtprotoRecordProvider"
}
  1. Authenticated Request: The AppView makes an inter-service call with JWT authentication:
GET https://locations.dayton-pokemon.club/xrpc/com.atproto.repo.getRecord
    ?repo=did:plc:cbkjy5n7bk3ax2wplmtjofq2
    &collection=community.lexicon.location.address
    &rkey=01k1kg8n491p421zgqrst02wtf
    &cid=6ca46e6724..

Authorization: Bearer [inter-service JWT]
  1. Authorization Decision: The source service examines:

    • Identity (iss field) - Who's requesting?
    • Application (aud field) - Which AppView is asking?
    • Context - Is this person attending the event? (This could be determined by querying the event's RSVP records or checking an internal attendee database—the authorization logic is entirely up to the source service implementation)
  2. Selective Response:

    • Unauthorized users receive the same partial record
    • Authorized attendees get the full location:
{
    "$type": "community.lexicon.location.address",
    "country": "US",
    "locality": "Oakwood",
    "name": "Orchardly Park",
    "postalCode": "45419",
    "region": "Ohio",
    "street": "2599 Delaine Ave",
    "source": { /* ... */ }
}

Implementation Considerations

Runtime Complexity

This pattern does introduce some interesting challenges that are worth thinking through.

The vary: "authenticated" directive means AppViews need to be smart about caching. You can't just cache one version and serve it to everyone—that would leak confidential info! Instead, AppViews either need to cache separately for each user or skip caching these records entirely. They'll also need to respect when sources say "hey, this data changed" and make absolutely sure that one user's view doesn't accidentally leak to another.

Then there's the fact that external sources become real dependencies in your system. What happens when a source goes down? You'll want good fallback strategies so your app doesn't break. Think about performance too—how long should you wait for a source to respond before giving up? And don't forget about rate limiting to keep things running smoothly and prevent abuse.

One quirk of this approach is that the same record might look different depending on where you're viewing it. Maybe one AppView has cached an older version, or a source is temporarily unavailable, or the authorization rules have changed. This isn't necessarily bad—it's just something to be aware of when building your app.

Security Model

The inter-service JWT provides a robust authorization framework:

{
    "iss": "did:plc:user123",        // Requesting identity
    "aud": "did:web:appview.social"  // Requesting AppView
}

Sources can implement whatever authorization logic makes sense for their use case. Maybe you want to allowlist trusted AppViews while keeping others out, or check whether someone's actually attending an event before showing them the details. You could even get fancy with time-based restrictions—like revealing venue details only 24 hours before the event starts. And of course, rate limiting keeps things running smoothly whether you're limiting requests per user or per AppView.

Extending to Blobs

The same pattern works for binary data:

{
    "$type": "blob",
    "mimeType": "image/png",
    "size": 8312,
    "ref": {
        "$link": "bafkreiecd.."
    },
    "source": {
        "service": "did:web:locations.dayton-pokemon.club#LocationProvider",
        "subject": {
            "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2"
        },
        "vary": "authenticated"
    }
}

This opens up some really cool possibilities! Imagine sharing high-resolution venue photos that only attendees can see—perfect for those hard-to-find entrances or special event setups. You could distribute PDF tickets or passes that stay secure until someone actually needs them. Or handle confidential documents that should only be accessible to specific people. The blob pattern gives us the same privacy controls we get with structured data, but for any kind of file.

Design Principles

Building on this pattern, several principles emerge:

  1. Progressive Enhancement: Records should be usable without hydration, providing basic functionality to all users

  2. Explicit Boundaries: The source field clearly marks which data requires external hydration

  3. Strong References: Using URI + CID ensures integrity even when serving dynamic content

  4. Service Autonomy: Sources control their authorization logic without protocol changes

  5. Graceful Degradation: AppViews should handle source failures without breaking the user experience

This pattern also fundamentally impacts how we think about content identifiers. In traditional ATProtocol, a record has one CID that represents its complete, canonical form. But with hydration, a single logical record could have multiple valid CIDs—each representing a different view with its own permissions scope. The partial location record has one CID, while the fully hydrated version has another. Both are "correct" representations of the same underlying data, just with different levels of detail. This shifts our mental model from "one record, one CID" to "one record, multiple authorized views, multiple CIDs."

Future Directions

The beauty of this hydration pattern is how it opens up the design space for building more sophisticated privacy-aware applications. Once you start thinking about records as having different views for different audiences, all sorts of interesting possibilities emerge. Let me share a few ideas that have me excited about where this could go.

Dynamic Content Assembly

This pattern really shines when you start combining multiple sources. Picture an event that pulls venue details from one location service, checks ticket availability from another service, and even grabs weather updates from a weather API. Each piece comes together to create a rich, dynamic view that's always up-to-date and tailored to who's looking at it.

Authorization and Delegation

The inter-service JWT approach is just the beginning. We could extend this with richer authorization frameworks like UCANs (User Controlled Authorization Networks) or OAuth RAR (Rich Authorization Requests). Imagine if the PDS could include additional claims in the JWT that describe specific capabilities or permissions. A JWT might say "this user can view confidential locations for events they're attending, but only within 48 hours of the event start time." The source service could then make fine-grained decisions based on these delegated capabilities rather than having to implement all the logic itself. This opens up possibilities for standardized permission models across different source implementations—your PDS becomes not just where your data lives, but also where your permissions originate from.

Standardized Sources

We might see common patterns emerge as more people build with this approach. Imagine sources that only reveal content to people who've RSVP'd to an event, or ones that unlock information at a specific time—perfect for surprise announcements! You could even build geographic fence sources that share different details based on where someone's viewing from.

Conclusion

Confidential record hydration solves a real problem in decentralized social applications: how to share confidential information selectively without compromising the open nature of the protocol. By introducing a source abstraction, we can build privacy-aware applications while maintaining the benefits of content-addressed, user-controlled data.

The pattern respects ATProtocol's principles—records remain portable and verifiable—while adding a runtime layer for access control. As we build more sophisticated social applications on ATProtocol, patterns like these will be crucial for handling the messy realities of human social dynamics.


Have thoughts on this approach? Find me on Bluesky (@ngerakines) or join the discussion in the Smoke Signal Discourse.

This post has had 1 interaction.