Permissioned Spaces

Credentials

Space credentials are short-lived JWTs for cross-service access to space data. A member requests a delegation token to prove their membership, exchanges the token for a credential JWT, then passes it to an external service that needs to read the space's records.

How credentials work

Credential issuance is a two-step process. The delegation token is a short-lived proof of membership (60-second TTL), and the credential is the bearer token used for cross-service access (2-hour TTL).

Credentials are ES256 JWTs signed with a P-256 keypair unique to each space. The keypair is generated on first credential request and stored encrypted (AES-256-GCM).

Step 1: Get a delegation token

The caller must be an authenticated member of the space. The delegation token is a short-lived proof of membership (60-second TTL).

Note: this endpoint is a GET request (not POST). The previous getMemberGrant endpoint (POST) is available as a legacy alias via dev.happyview.space.getMemberGrant.

const params = new URLSearchParams({
  space: "ats://did:plc:abc123/com.example.forum/main",
});
const response = await fetch(`https://happyview.example.com/xrpc/com.atproto.space.getDelegationToken?${params}`, {
  headers: {
    "X-Client-Key": CLIENT_KEY,
    "Authorization": `DPoP ${ACCESS_TOKEN}`,
    "DPoP": DPOP_PROOF,
  },
});
interface DelegationTokenResponse {
  delegationToken: string;
  expiresAt: string;
}
const data: DelegationTokenResponse = await response.json();

Response:

{
  "delegationToken": "eyJhbGciOiJFUzI1NktFWSJ9...",
  "expiresAt": "2026-05-09T12:01:00Z"
}

Step 2: Get a space credential

Exchange the delegation token for a space credential JWT. The credential is signed by the space's keypair and has a 2-hour TTL.

const response = await fetch("https://happyview.example.com/xrpc/com.atproto.space.getSpaceCredential", {
  method: "POST",
  headers: {
    "X-Client-Key": CLIENT_KEY,
    "Authorization": `DPoP ${ACCESS_TOKEN}`,
    "DPoP": DPOP_PROOF,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    grant: "eyJhbGciOiJFUzI1NktFWSJ9...",
  }),
});
interface CredentialResponse {
  credential: string;
  expiresAt: string;
}
const data: CredentialResponse = await response.json();

Response:

{
  "credential": "eyJhbGciOiJFUzI1NiJ9...",
  "expiresAt": "2026-05-09T14:00:00Z"
}

Credential claims

The JWT payload contains:

ClaimDescription
issThe space authority's DID (who signed it)
subThe full ats:// space URI
iatIssued at (Unix timestamp)
expExpiry (Unix timestamp)
jtiRandom nonce for replay protection

Using a credential

Pass the credential as a standard Bearer token in the Authorization header. HappyView distinguishes space credentials from other tokens by checking the JWT header's typ field (atproto-space-credential+jwt).

const response = await fetch(
  "https://happyview.example.com/xrpc/com.atproto.space.getRecord?space=...&collection=...&rkey=...",
  {
    headers: {
      "Authorization": `Bearer ${SPACE_CREDENTIAL}`,
    },
  },
);
const data = await response.json();

No DPoP auth or client key is needed when authenticating via space credential — the credential itself is sufficient. The sub claim identifies the space being accessed.

HappyView verifies the credential by resolving the issuer's DID document, extracting the #atproto_space signing key, and validating the JWT signature and expiry. If valid, the request is granted read access to the space identified by sub.

App access control

Before issuing a credential, HappyView checks whether the calling app (identified by its DPoP client key) is allowed to access the space:

  • open (default): any app can get credentials
  • allowList: only apps whose client metadata URL appears in the allowed array can get credentials

For open spaces, requests without a client key are allowed. For allowList spaces, a client key is required — requests without one are rejected.

External credential verification

HappyView can also verify credentials issued by other HappyView instances or space-aware services. When a Bearer space credential is presented, HappyView:

  1. Decodes the JWT without verification to extract the iss (issuer DID)
  2. Resolves the issuer's DID document
  3. Extracts the #atproto_space signing key from the DID doc
  4. Verifies the JWT signature and expiry
  5. Checks that the sub claim matches the requested space

A credential issued by one instance can be used to read from another instance that hosts the same space's data.