Automattic / Automattic/wordpress-atmosphere

Jetpack integration: deliver gated content as AT Protocol permissioned data

Open
#226 0 comments 1 reaction 1 assignee Claimed by @jeherve View on GitHub
[Feature] Integrations [Feature] OAuth enhancement
Dominant language
PHP
Stars
52
Forks
3
Avg merge
2d 1h
Merged PRs (30d)
22

Description

I've been reading through the permissioned data proposal over the past few weeks, and I keep coming back to the same thought: it lines up with what Jetpack already does almost suspiciously well. Writing it down while the shape of it is fresh.

> **Tracking issue, not something to pick up.** Permissioned data is an early draft; three load-bearing decisions reversed in July 2026 alone, and the reference implementation is still very much in progress. Nothing here should be built yet. This is here so we can move quickly once things settle.
>
> Upstream: [Proposal 0016]() / [proposal PR #94]() / [atproto implementation PR #5187]() / [forum discussion](), which is where the actual debate happens rather than the PR.

## What

Deliver Jetpack's gated content to AT Protocol as permissioned records, instead of either publishing it publicly or not publishing it at all. Newsletter subscriber posts, paid tiers, paywalled sections.

If you're not familiar with the proposal, the new primitive is the *space*: an authorization and sync boundary whose records live in members' own repos, readable only by authorized parties. A gated WordPress post would become an ordinary `site.standard.document` written to a space rather than to the public repo, with WordPress deciding who gets a credential.

The gating rules already exist in Jetpack, and the publishing pipeline already exists here. Most of this is about connecting the two.

## Why

Someone running a paid or subscriber-only newsletter on WordPress has two options today, and neither is good: publish to the network and lose the gate, or keep the gate and stay invisible. We don't offer a third one. The work people actually pay for is the work nobody on the network can see.

A few things they'd get out of this.

**Their gated archive gets an address without going public.** The post exists on the network, discoverable in structure, readable only by the people who've earned it. That's a kind of content that can't exist on AT Protocol at all right now.

**Subscribers read it wherever they already read.** Right now a Jetpack subscriber gets gated content by email, or by coming back to the site. With this it lands in whichever AT Protocol reader they like; Leaflet, a Bluesky client, whatever ships next year. The creator maintains none of those and gets all of them.

**Maybe most importantly, the subscriber relationship stays theirs.** On Substack or Patreon, the platform holds the list and the reading surface, and can change the terms whenever it wants. Here the content sits in the creator's own repo, each reader is their own DID, and the membership decision is answered live by their own WordPress install. Nobody sits in the middle.

**WordPress is a good fit for the part that decides.** Of note, 0016 deliberately refuses to model membership: a space can hand the access decision to an external app, which gets asked "can this person read this?" when a credential is minted. The protocol never learns what a subscription or a payment is. That hook wants something with durable identity, membership state, payments, and a server that's always up, which describes WordPress with Jetpack pretty exactly and doesn't describe much else in the ecosystem. Creators wouldn't adopt a new membership system; they'd keep the one they're already running.

**It opens a funnel we don't have.** Content above a `jetpack/paywall` block is already public by design, so that part can keep publishing publicly while the gated remainder goes to the space. Someone who isn't subscribed meets the free portion in their feed, and the way onward leads back to the creator's own site.

And the records are ordinary `site.standard.document`, so someone who later moves off WordPress still has the archive in their own repo, in a format other tools already read.

## How

A sketch rather than a plan; all of it depends on where the spec lands.

**The space.** Authority would be the site's already-connected DID, which we get for free: with no `#atproto_space` entries in the DID document, both the authority and the space host fall back to the account's existing PDS. A site that can publish today can host a space.

Records don't change at all. Private is a location, not a schema, so it's the same `site.standard.document`, written through `com.atproto.space.createRecord` with a `space` parameter instead of `repo`. `Transformer\Document` wouldn't need touching; only the write path in `Publisher` would.

I've been assuming one space per access tier rather than a single "gated" space, keyed on Jetpack's own tiers (`_jetpack_newsletter_tier_id`), so a reader's credential only covers what they hold. I'm not sure about that one though. It's more moving parts, and with a single writer the case for splitting is weaker than it is for forums or communities.

**WordPress as the managing app.** Set the space policy to `managing-app` and the PDS calls `com.atproto.simplespace.checkUserAccess` whenever someone wants a credential, which WordPress answers from Jetpack's live subscription state. Nothing to sync and nothing to drift; a cancellation takes effect within the credential lifetime rather than needing to be pushed anywhere.

**The identity bridge is the hard part.** Spaces address members by DID, and Jetpack identifies a subscriber by email, inside a JWT issued by `subscribe.wordpress.com` and kept in a session cookie. On a connected site that subscriber list lives on [WordPress.com]() rather than locally. Nothing joins the two.

The one moment WordPress reliably knows *this visitor is subscriber X at tier Y* is during an authenticated visit, so that's when the link has to be captured. A subscriber shows up with a valid token, we offer to connect their AT Protocol account, they authorize through the same OAuth stack in `includes/oauth/` we already run for the publishing account, and we store the DID against their subscriber record. Later access checks read that mapping.

This is the half Automattic/wordpress-atmosphere#204 couldn't build. That one closed because there's no way to discover a visitor's app, so an OAuth-free subscribe intent wasn't possible. That constraint doesn't apply here, since a permissioned reader needs a credential-bearing session anyway; OAuth stops being the heavy alternative and becomes the mechanism.

**Where it would live.** `integrations/class-load.php` is already a registry for integrations that only load when their target plugin is active, so a Jetpack one belongs there behind a `class_exists( 'Jetpack_Memberships' )` guard, same shape as the `class_exists( 'WP_Connector_Registry' )` guard in `Connectors`. We shouldn't grow a Jetpack dependency for this.

## What we'd be signing up for

These are properties of the protocol rather than gaps we could implement around, and whoever turns this on should be able to see them:

* **Authorized apps read the whole space.** Grant an app access and it syncs every member's repo in that space, not only yours. For a paid tier, that means one subscriber's choice of client governs how exposed that tier's archive is.
* **Revocation is coarse and slow.** Access ends when the space loses every OAuth session for it. Removing a member only bites at credential renewal, on the order of hours, and there's no eviction signal, so apps keep whatever they've already synced for as long as they like.
* **Access control isn't confidentiality.** The PDS and any authorized indexer read plaintext. None of this is encrypted.

None of that is a dealbreaker for subscriber-only posts, where the gate is more about relationship than revenue. For paid tiers it's a real conversation, and I'd want those opt-in with the trade-off spelled out rather than switched on alongside everything else.

## Before any of this

We'd need to know a post's access level in a way that doesn't depend on who's visiting, and right now we don't. `is_post_publishable()` only checks post status and password, and `Transformer\Document` builds its body straight from `post_content`, skipping the `the_content` filter where Jetpack's paywall lives. That's worth fixing on its own merits, and it happens to produce exactly the seam this work would read from. Separate issue to follow.

Otherwise, things worth watching: 0016 leaving draft; [PR #5187]() landing and the shipped lexicons drifting from the draft; whether `at://` vs `ats://` is formally settled; and where a space type NSID for this would even live, given `site.standard.*` belongs to [standard.site]() and isn't ours to declare in.

Related: Automattic/wordpress-atmosphere#204 (reader-side subscribe flow, closed), [#190]() (connection layer extraction, closed).

I'm probably wrong about some of the details here, and the spec will likely invalidate a few more. Something to keep an eye on.

********

Internal reference: peLCyI-Yt-p2

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.