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:

  • 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:

    • 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.