Permissioned Spaces

Write Notifications

Write notifications tell syncers when a repo in a space changes. A syncer registers its service identifier for a space, and HappyView calls com.atproto.space.notifyWrite on that service after every commit to any repo in the space. The syncer then reads the changed repo with a space credential.

Notifications flow in two directions:

  • Outbound: HappyView notifies registered syncers when a repo in the space advances, whether HappyView hosts the repo or the author's PDS does.
  • Inbound: a PDS hosting a repo in the space notifies HappyView, as the space authority, when that repo changes.

Registrations cover the whole space and expire after 24 hours. Registering again renews the registration and replaces the previous one for the same service.

Registering for notifications

Requires an OAuth session or a space credential. With a credential, the request's audience is the space authority's DID.

const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.registerNotify", {
  method: "POST",
  headers: {
    ...(await signSpaceRequest(`Atproto-Space ${SPACE_CREDENTIAL}`, "did:web:happyview.example.com")),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    space: "at://did:web:happyview.example.com/space/com.example.forum/main",
    service: "did:web:syncer.example.com#atproto_space_syncer",
  }),
});
interface RegisterNotifyResponse {
  expiresAt: string;
}
const data: RegisterNotifyResponse = await response.json();

The signSpaceRequest helper is defined in Signing requests.

Input:

FieldTypeRequiredDescription
spacestringYesSpace URI (at://...)
servicestringYesService identifier of the syncer: a DID with an optional fragment naming a service entry in its DID document. A bare DID means the account's #atproto_pds service.

HappyView resolves the service identifier to its endpoint when the syncer registers. An identifier that does not resolve fails with 400 ServiceNotResolvable.

Response (200):

{
  "expiresAt": "2026-10-01T12:00:00Z"
}

Unregistering

com.atproto.space.unregisterNotify withdraws a service's registration. It takes the same authentication as registerNotify.

Input:

FieldTypeRequiredDescription
spacestringYesSpace URI
servicestringYesThe service identifier to unregister

The caller must be the registered service or the space's creator. The call is idempotent.

Response (200):

{
  "removed": 1
}

Receiving notifications

For each commit to a repo in the space, HappyView POSTs to <endpoint>/xrpc/com.atproto.space.notifyWrite on every registered service. Each call carries service auth from HappyView: a Bearer JWT whose iss is HappyView's instance DID, whose aud is the registered service identifier, and whose lxm is com.atproto.space.notifyWrite.

{
  "space": "at://did:web:happyview.example.com/space/com.example.forum/main",
  "repo": "did:plc:author456",
  "repoRev": "3l2tkbx7225co",
  "rev": "3l2tkbx7225co",
  "hash": { "$bytes": "q83vEjRWeJC..." },
  "spaceRev": "3l2tkbx7a3k2s",
  "prevSpaceRev": "3l2tkbwz5xq2c"
}
FieldTypeDescription
spacestringThe space URI
repostringDID of the repo that changed
repoRevstringThe repo's new revision (TID)
revstringThe same value as repoRev, under the alpha lexicon's name. Sent until v3.
hashbytesThe repo's new LtHash digest
spaceRevstringThe space revision this update was assigned
prevSpaceRevstring?The space revision before it. Absent on the first update in the space.

Every accepted update advances the space revision, a TID that increases across all repos in the space. A syncer that receives a prevSpaceRev newer than the last spaceRev it saw has missed a notification. It catches up by calling listRepos with cursor set to the last space revision it processed.

Delivery is best effort. HappyView does not retry a failed call.

Sending notifications to HappyView

A PDS hosting a repo in the space calls com.atproto.space.notifyWrite on HappyView after each commit to that repo. HappyView then pulls the new commit from the PDS and indexes it.

The call requires service auth signed by the account that wrote. The token's aud is HappyView's instance DID, either with the #atproto_space_host fragment or bare, and its lxm must be com.atproto.space.notifyWrite.

const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.notifyWrite", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${SERVICE_AUTH_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    space: "at://did:web:happyview.example.com/space/com.example.forum/main",
    repo: "did:plc:author456",
    repoRev: "3l2tkbx7225co",
    hash: { $bytes: "q83vEjRWeJC..." },
  }),
});
const data = await response.json();
// { "success": true }

Input:

FieldTypeRequiredDescription
spacestringYesSpace URI (at://...)
repostringYesDID of the repo that changed. Must match the service auth issuer.
repoRevstringYesThe repo's new revision (TID). rev, the alpha lexicon's name, is accepted in its place until v3.
hashbytesYesThe repo's new LtHash digest

HappyView handles the notification as follows:

  • The writer must pass the space's write policy. Otherwise the call fails with 403.
  • A repoRev that is not newer than the last one recorded for the repo is accepted and ignored.
  • A repoRev more than 5 minutes in the future fails with 400 FutureRev.
  • A space HappyView does not host fails with 400 SpaceNotFound.
  • A newer repoRev joins the repo to the space's writer set, advances the space revision, and is forwarded to registered syncers.

Response (200):

{
  "success": true
}

HappyView also checks every repo hosted on its author's PDS every 5 minutes, so a write whose notification was lost is indexed on the next check.

Notifying space deletion

When a space is deleted, HappyView tells every service registered for it. A service registered by identifier receives com.atproto.space.notifySpaceDeleted at its endpoint, with service auth from HappyView and the body { "space": "<space URI>" }. A legacy webhook receives { "space": "<space id>" }. Delivery is best effort.

The space's creator or a HappyView super admin can also send the same notification for a space that still exists by calling com.atproto.space.notifySpaceDeleted with { "space": "<space URI>" }. HappyView responds with { "success": true }.

Legacy webhooks

The following forms are deprecated and kept until v3.

Webhook registration. registerNotify with serviceDid and endpoint in place of service registers a webhook URL. The response includes the registration id:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "expiresAt": "2026-10-01T12:00:00Z"
}

HappyView POSTs a JSON payload to the endpoint for each record created, updated, or deleted in the space, without authentication:

FieldTypeDescription
spacestringInternal space ID
didstringDID of the author
collectionstring (NSID)Collection of the record
rkeystringRecord key
cidstring?CID of the new record value, null for deletes

Per-record notifyWrite. notifyWrite also accepts {space, did, collection, rkey, cid} from the author, the space's creator, or a super admin. HappyView syncs the author's repo and sends the per-record payload to webhook registrations.