AtProto Local-Stack Notes

As of 2026-01-17, this works but things change rapidly.

High Level Bullet Points

Used the @atproto/oauth-client-browser to just get building quickly.

Examples I based my experiment on:

tokimekibluesky oauth.ts

whitewind-blog Auth.ts

The BlueSky Team published a package @atproto/dev-env for PDS and other backing services for local development.

Issue: The node script is missing a #!/usr/bin/env node, causing it to fail.

Created an Issue (#4484) and PR (#4488) to fix it.

Note: The REPL doesn’t work, but it’s not a big deal.

Workaround: You have to install it and edit files manually to get it to work until the PR (or another fix) addresses it.

Requirements: You must have both Redis and Postgres running and exposed to the service.

Expose both connection strings via environment variables: DB_POSTGRES_URL & REDIS_HOST.

See Podman Compose below for container setup.

OAUTH config & metadata for local atproto stack:

clientMetadata for app:

client_id has to be http://localhost without the port , but redirect_uri and scope query parameters must be provided .

Use encodeURIComponent to encode them for the URL.

Note: The http://localhost requirement seems inconsistent with the loopback requirement.

OAuth client config in TypeScript/JavaScript:

You can’t use localhost; you must use the loopback address (127.0.0.1), otherwise the validation fails in the Oauth client.

For fully local development, you must specify handleResolver, plcDirectoryUrl, and allowHttp=TRUE. See snippet below.

Test Users for atproto localstack:

Your test user’s handle has to end in .test.

You can use a simple curl request to create it. See below.

Note: The default users didn’t work for me.

Example: handle=alice.test with pass=alice-pass

OAuth Client Config

/* OAUTH client config */
const client = new BrowserOAuthClient({
clientMetadata: getConfig(import.meta.env.PROD),
// https://www.npmjs.com/package/@atproto/oauth-client-browser
handleResolver: "http://127.0.0.1:2583",
plcDirectoryUrl: "http://127.0.0.1:2582",
allowHttp: true,
});

Podman Compose for local atproto dev-env

# Podman Compose configuration for atproto PDS development
# Includes PostgreSQL and Redis services

version: '3.8'

services:
postgres:
image: postgres:latest
container_name: atproto_pds_postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- '5433:5432'
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql
restart: unless-stopped
networks:
- atproto_network

redis:
image: redis:latest
container_name: atproto_pds_redis
ports:
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 5s
retries: 5
volumes:
- redis_data:/data
restart: unless-stopped
networks:
- atproto_network
volumes:
postgres_data:
name: atproto_postgres_data
redis_data:
name: atproto_redis_data

networks:
atproto_network:
name: atproto_network
driver: bridge

Test User curl command

curl -X POST http://localhost:2583/xrpc/com.atproto.server.createAccount \
-H "Content-Type: application/json" \
-d '{
"handle": "yolo.test",
"email": "yolo@example.com",
"password": "password123"
}'

Why LocalStacks are Important?

Being able to run software locally and debug it is vital for a healthy atproto atmosphere and to stop lock in. Developers have been wrestling this back from cloud providers. We should be vigilant about it with atproto.

Likewise, testing data can pollute the main network. PDS.rip helps with this, but it could ultimately disappear any day.