PDS Forwarding
As a convenience for local-first applications, habitat proxies a scoped set of standard AT Protocol XRPC calls on behalf of authenticated users. Rather than forwarding all unknown /xrpc/ paths, Habitat explicitly handles two namespaces and one additional method.
Which requests are forwarded
com.atproto.repo.* and com.atproto.sync.*
All methods under these two namespaces are forwarded. Habitat routes them in one of two ways depending on whether the method reads from a specific target repository or writes to the caller's own:
Forwarded to the caller's PDS (authenticated, via DPoP):
- Write methods:
createRecord,putRecord,applyWrites,uploadBlob,importRepo com.atproto.syncmethods with no specific target:listRepos,listReposByCollection,requestCrawl
Forwarded to the target's PDS (unauthenticated):
These methods accept a repo or did query parameter identifying a specific account. Habitat resolves that identity's DID document to find their PDS and forwards the request there without auth headers.
| Method | Target param |
|---|---|
com.atproto.repo.getRecord | repo |
com.atproto.repo.listRecords | repo |
com.atproto.repo.describeRepo | repo |
com.atproto.sync.getRepo | did |
com.atproto.sync.getRecord | did |
com.atproto.sync.listBlobs | did |
com.atproto.sync.getLatestCommit | did |
com.atproto.sync.getRepoStatus | did |
com.atproto.sync.getBlob | did |
com.atproto.server.getServiceAuth
Forwarded to the caller's PDS (authenticated, via DPoP). Used to obtain a signed service auth token from the user's own PDS for inter-service calls.
Methods NOT forwarded through Habitat
Everything else. For example, app.bsky.* and com.atproto.identity.* methods should be called directly against the public Bluesky AppView:
https://public.api.bsky.app/xrpc/<lexicon-id>
or the user's PDS directly. These are public, unauthenticated endpoints. No authentication token is needed.
How forwarding works
For calls routed to the caller's PDS:
- Habitat validates your access token and resolves the caller's DID
- Habitat creates a DPoP-authenticated HTTP client for that user's PDS
- The request (method, headers, body) is forwarded to the user's PDS
- The PDS response (status, headers, body) is streamed back to your app
Your app never directly authenticates with the PDS. Habitat holds encrypted PDS credentials server-side (obtained during habitat sign-up/login) and handles the DPoP token binding required by AT Protocol PDSes.
For calls routed to the target's PDS:
- Habitat extracts the
repoordidquery parameter - Habitat resolves the target's DID document to find their
atproto_pdsservice endpoint - The request is forwarded to that PDS without auth headers (these are public endpoints)
Making a forwarded request
GET /xrpc/com.atproto.repo.listRecords?repo=did:plc:...&collection=app.bsky.feed.post
Authorization: Bearer <habitat-access-token>
Habitat-Auth-Method: oauth
For target-routed read requests, auth headers are not required (and are stripped before forwarding):
GET /xrpc/com.atproto.repo.getRecord?repo=did:plc:...&collection=app.bsky.feed.post&rkey=...
Error handling
| Condition | Status returned |
|---|---|
| Invalid or expired Habitat token (caller-PDS path) | 401 Unauthorized |
Missing repo/did param on target-routed method | 400 Bad Request |
| Target DID cannot be resolved | 502 Bad Gateway |
Target has no atproto_pds service in their DID doc | 502 Bad Gateway |
| PDS is unreachable or returns an unexpected error | 502 Bad Gateway |
| Other forwarding errors | 500 Internal Server Error |
The response body from the PDS (including AT Protocol error payloads) is passed through unchanged.