Skip to content

Getting Started ​

This guide explains how to integrate Stratos private namespace functionality into ATprotocol client applications. It is based on how Stratos was integrated with pdsls and maps patterns to the Bluesky social-app codebase as a reference architecture.

What is Stratos? ​

Stratos enables private, domain-scoped content within ATprotocol. Users can create posts visible only to members of specific groups or communities.

ConceptDescription
Stratos ServiceA server that stores private records (separate from PDS)
EnrollmentUser must enroll with a Stratos service to create private content
Domain BoundarySpecifies which community can view a record. Values are fully qualified as {serviceDid}/{name} (e.g. did:web:stratos.example.com/general)
Private PostA zone.stratos.feed.post record with boundary restrictions

The stratos-client Helper Library ​

The @northskysocial/stratos-client package provides the building blocks for enrollment discovery, service routing, record verification, and OAuth scope management:

ModuleWhat it provides
discoverygetEnrollmentByServiceDid() — find enrollment for a specific service
routingresolveRepositoryTarget(), createServiceFetchHandler(), findEnrollmentByService() — select custody-aware XRPC targets
verificationfetchAndVerifyRecord(), verifyCidIntegrity(), resolveServiceSigningKey(), resolveUserSigningKey() — three-tier record verification
scopesbuildStratosScopes(), STRATOS_SCOPES — build OAuth scope strings for Stratos collections

Install it alongside your AT Protocol client library:

bash
npm install @northskysocial/stratos-client

When to use stratos-client vs. raw XRPC

The code examples in this guide show both approaches. Use stratos-client when you want concise, tested helpers — use raw XRPC when you need full control or are using a framework that doesn't fit the helper signatures.

Quick Start ​

1. Check for Stratos Support ​

Determine if the user's AppView knows about their Stratos access:

typescript
import { Agent } from '@atproto/api'

const agent = new Agent('https://appview.example.com')

const profile = await agent.getProfile({ actor: agent.session.did })
const stratosDomains = profile.data.associated?.stratosDomains ?? []

if (stratosDomains.length > 0) {
  console.log('User has stratos access for:', stratosDomains)
}

2. Discover Enrollment ​

Discover enrollment records from the user's PDS:

typescript
import {
  getEnrollmentByServiceDid,
  resolveRepositoryTarget,
} from '@northskysocial/stratos-client'

const enrollment = await getEnrollmentByServiceDid(did, pdsUrl, serviceDid)
const STRATOS_ENDPOINT = 'https://stratos.example.com'

STRATOS_ENDPOINT is a trusted authority endpoint from app configuration. The published enrollment.service and repoHost are hints, not trusted repository hosts. Bind the PDS URL to the authenticated session, then resolve the repository target before forwarding credentials.

See User Enrollment for the full enrollment record schema and all discovery variants.

3. Create a Routed Agent ​

When using @atproto/api with an OAuth session, you must wrap the session's fetchHandler to route requests to the selected repository target URL.

Common mistake

new Agent(session) followed by agent.serviceUrl = new URL(target.url) can still resolve requests against the OAuth token's audience instead of the selected target. Use the wrapper below to pass the target's absolute URL to the authenticated handler.

Using stratos-client (with @atcute/client):

typescript
import { Client } from '@atcute/client'
import { createServiceFetchHandler } from '@northskysocial/stratos-client'

const target = resolveRepositoryTarget(enrollment, {
  authorityServiceUrl: STRATOS_ENDPOINT,
  sessionPdsUrl: pdsUrl,
})
if (target.kind === 'unresolved') throw new Error(target.reason)
const handler = createServiceFetchHandler(authenticatedHandler, target.url)
const rpc = new Client({ handler })

Use repo methods only when target.kind === 'stratos'; use space methods when it is pds.

Using @atproto/api directly:

typescript
import { Agent } from '@atproto/api'
import type { OAuthSession } from '@atproto/oauth-client-browser'

function createRepositoryAgent(
  session: OAuthSession,
  targetUrl: string,
): Agent {
  return new Agent((url: string, init: RequestInit) => {
    const fullUrl = new URL(url, targetUrl)
    return session.fetchHandler(fullUrl.href, init)
  })
}

4. Create a Private Post ​

typescript
const repositoryAgent = createRepositoryAgent(session, target.url)

if (target.kind === 'stratos')
  await repositoryAgent.com.atproto.repo.createRecord({
    repo: userDid,
    collection: 'zone.stratos.feed.post',
    record: {
      $type: 'zone.stratos.feed.post',
      text: 'This is a private post for my community!',
      boundary: {
        $type: 'zone.stratos.boundary.defs#Domains',
        values: [
          {
            $type: 'zone.stratos.boundary.defs#Domain',
            value: 'did:web:stratos.example.com/general',
          },
        ],
      },
      createdAt: new Date().toISOString(),
    },
  })

For PDS custody, use com.atproto.space.createRecord on the trusted PDS endpoint. Pass space: at://{authorityDid}/space/zone.stratos.space.feed/{spaceKey} alongside repo, collection, and record. The returned record URI has seven segments and starts with the authority DID. A legacy repo write to the PDS origin does not create a space record.

Service Routing ​

Repository routing depends on custody. Stratos custody uses authority repo methods. PDS custody uses space methods on the user's PDS.

When a user has multiple enrollments, select the target enrollment first (see findEnrollmentByService in User Enrollment), then select the repository target from its custody and trusted host information.

Routing logic ​

typescript
import { resolveRepositoryTarget } from '@northskysocial/stratos-client'

const target = resolveRepositoryTarget(enrollment, {
  authorityServiceUrl,
  authoritativeRepoHost,
  sessionPdsUrl,
})
if (target.kind === 'unresolved') throw new Error(target.reason)

Use target.url with repo methods for stratos custody or space methods for pds custody, as shown in Create a Private Post. resolveServiceUrl returns the authority URL; it does not select a repository host.

Which operations route to Stratos ​

OperationRoutes to Stratos?
com.atproto.repo.getRecordStratos custody only
com.atproto.repo.listRecordsStratos custody only
com.atproto.repo.describeRepoStratos custody only
com.atproto.repo.createRecordStratos custody only
com.atproto.repo.deleteRecordStratos custody only
com.atproto.repo.applyWritesStratos custody only
com.atproto.sync.getRecordStratos custody only
zone.stratos.space.listBlobsYes (lists blob CIDs in a space)
zone.stratos.sync.getRepoYes (full repo export as CAR)
zone.stratos.repo.importRepoYes (import repo from CAR)
zone.stratos.sync.subscribeRecordsYes (WebSocket firehose)
com.atproto.sync.getBlobNo (not yet implemented)

DPoP-Aware Transport ​

Stratos endpoints require authenticated requests using the same DPoP credentials as the user's PDS session. The key insight: pass an absolute URL to the OAuth agent's fetch handler to redirect requests to a different origin while keeping DPoP proof generation valid.

The underlying DPoP implementation derives htu (the HTTP URI claim in the DPoP proof JWT) from the actual request URL. By passing an absolute URL with the Stratos origin, the proof is generated for that origin rather than the PDS.

Transport wrapper ​

This authority transport applies to stratos custody only. For pds custody, route to the authenticated PDS host with space methods; see Create a Private Post.

typescript
import { createServiceFetchHandler } from '@northskysocial/stratos-client'

// agent.handle is the FetchHandler from your OAuth session
if (enrollment.custody !== 'stratos')
  throw new Error('Expected Stratos custody')
const handler = createServiceFetchHandler(agent.handle, authorityServiceUrl)

createServiceFetchHandler accepts any FetchHandler from @atcute/client (a function (pathname, init) => Promise<Response>) and returns a FetchHandlerObject that resolves relative pathnames against the target service URL.

Client construction ​

Construct the RPC client from the resolved target in Create a Private Post. The enrollment's authority service URL is not the PDS repository host.

Minimum Viable Adoption Path ​

For apps that want to add basic Stratos support incrementally:

Step 1: Read-only hydration ​

  1. Add enrollment discovery to session establishment
  2. Store enrollment state
  3. Add a Stratos mode toggle (settings or UI chrome)
  4. For Stratos-custody records, route getRecord / listRecords through the authority client; for PDS-custody space records, use the trusted PDS target
  5. Handle empty Stratos-custody collections gracefully (the enrolled repo may have no records yet)

Step 2: Write routing ​

  1. For stratos custody, route repo createRecord / deleteRecord / applyWrites through the authority client; for pds custody, use the corresponding space methods on the authenticated PDS target shown in Create a Private Post
  2. Add scope declarations to OAuth metadata
  3. Add scope selector UI with dependency gating

Step 3: Verification ​

  1. Implement two-tier verification for Stratos records
  2. Surface verification level in UI

Step 4: Rich features ​

  1. Boundary-aware UI (show boundary chips, filter by boundary)
  2. AppView-side hydration for feed integration
  3. Blob support when available

social-app Integration Mapping ​

For a React Native/Expo app like Bluesky's social-app:

State layer ​

Conceptsocial-app locationPattern
Enrollment statesrc/state/stratos.tsx (new)React Context with StratosEnrollment | null | undefined
Active mode togglesrc/state/stratos.tsx (new)Boolean state with setter
Discovery triggersrc/state/session/index.tsxCall getEnrollmentByServiceDid in resumeSession / login flows
Cleanup on logoutsrc/state/session/index.tsxReset enrollment and active state in logout handler

Query hooks ​

HookLocationImplementation
useStratosEnrollment()src/state/stratos.tsxContext consumer returning enrollment state
useStratosActive()src/state/stratos.tsxContext consumer returning active boolean
useStratosClient()src/state/queries/stratos.ts (new)Returns correctly-routed Client based on mode

Agent/transport ​

In src/state/session/agent.ts, the BskyAppAgent wraps transport. In a query hook, resolve the repository target before constructing a client. Use authority repo methods for stratos custody; use authenticated PDS space methods for pds custody. See Create a Private Post for the write route.

View integration ​

ViewIntegration pointWhat changes
Post threadsrc/screens/PostThread/index.tsxHydrate from Stratos when viewing boundary-scoped posts
Feedsrc/state/queries/post-feed.tsInclude Stratos records in feed via hydration
Settingssrc/screens/Settings/Stratos enrollment status, mode toggle
Record creationPost composerSelect repo or space createRecord by custody

Key differences from pdsls ​

Aspectpdsls (SolidJS)social-app (React)
StatecreateSignal()React Context / useState
ReactivitySignal auto-trackingQuery key invalidation / useEffect
Refetch triggerResource source signalQuery key includes stratosActive
Client constructionPer-calluseMemo with deps or query hook

Next Steps ​