{
"path": "/3mcnqs35sbs25",
"site": "at://did:plc:vyufjtyd3inivvijpwhey34o/site.standard.publication/3maesqgc6ns2g",
"tags": [
"atproto",
"local-first",
"local-stack",
"containers",
"devops",
"bluesky"
],
"$type": "site.standard.document",
"title": "AtProto Local-Stack Notes",
"content": {
"$type": "pub.leaflet.content",
"pages": [
{
"id": "019bcdc7-aa1d-7773-a3f4-f09ba24d69ab",
"$type": "pub.leaflet.pages.linearDocument",
"blocks": [
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 55,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "As of 2026-01-17, this works but things change rapidly."
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "High Level Bullet Points"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.unorderedList",
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 38,
"byteStart": 9
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://www.npmjs.com/package/@atproto/oauth-client-browser",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Used the @atproto/oauth-client-browser to just get building quickly."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 34,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Examples I based my experiment on:"
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 24,
"byteStart": 0
},
"features": [
{
"uri": "https://github.com/spuithori/tokimekibluesky/blob/e234667f628461b86f862d03b2c246a3431d65fa/src/lib/oauth.ts#L27-L56",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "tokimekibluesky oauth.ts"
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 22,
"byteStart": 0
},
"features": [
{
"uri": "https://github.com/whtwnd/whitewind-blog/blob/047c8d44b5a13e5b8518823982c486e9e3df50c0/frontend/src/atoms/Auth.ts#L1-L174",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "whitewind-blog Auth.ts"
},
"children": []
}
]
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 37,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 53,
"byteStart": 37
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
},
{
"uri": "https://www.npmjs.com/package/@atproto/dev-env",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 111,
"byteStart": 53
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "The BlueSky Team published a package @atproto/dev-env for PDS and other backing services for local development."
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 6,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 55,
"byteStart": 36
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Issue: The node script is missing a #!/usr/bin/env node, causing it to fail."
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 24,
"byteStart": 11
},
"features": [
{
"uri": "https://github.com/bluesky-social/atproto/issues/4484",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 39,
"byteStart": 29
},
"features": [
{
"uri": "https://github.com/bluesky-social/atproto/pull/4488",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Created an Issue (#4484) and PR (#4488) to fix it."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 5,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Note: The REPL doesn’t work, but it’s not a big deal."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 11,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Workaround: You have to install it and edit files manually to get it to work until the PR (or another fix) addresses it."
},
"children": []
}
]
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 13,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 38,
"byteStart": 33
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 51,
"byteStart": 43
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Requirements: You must have both Redis and Postgres running and exposed to the service."
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 73,
"byteStart": 58
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 86,
"byteStart": 76
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Expose both connection strings via environment variables: DB_POSTGRES_URL & REDIS_HOST."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 18,
"byteStart": 4
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "See Podman Compose below for container setup."
},
"children": []
}
]
}
]
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 48,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "OAUTH config & metadata for local atproto stack:"
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 14,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 23,
"byteStart": 14
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "clientMetadata for app:"
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 9,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
},
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 20,
"byteStart": 9
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
},
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 36,
"byteStart": 20
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
},
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 53,
"byteStart": 36
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
},
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 59,
"byteStart": 53
},
"features": [
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 71,
"byteStart": 59
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 76,
"byteStart": 71
},
"features": [
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 81,
"byteStart": 76
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
},
{
"index": {
"byteEnd": 115,
"byteStart": 81
},
"features": [
{
"uri": "https://atproto.com/specs/oauth#localhost-client-development",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "client_id has to be http://localhost without the port, but redirect_uri and scope query parameters must be provided."
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 22,
"byteStart": 4
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Use encodeURIComponent to encode them for the URL."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 5,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 26,
"byteStart": 10
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Note: The http://localhost requirement seems inconsistent with the loopback requirement."
},
"children": []
}
]
}
]
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 45,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "OAuth client config in TypeScript/JavaScript:"
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 16,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 25,
"byteStart": 16
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 62,
"byteStart": 25
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 71,
"byteStart": 62
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
},
{
"index": {
"byteEnd": 72,
"byteStart": 71
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "You can’t use localhost; you must use the loopback address (127.0.0.1), otherwise the validation fails in the Oauth client."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 60,
"byteStart": 46
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 77,
"byteStart": 62
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
},
{
"index": {
"byteEnd": 97,
"byteStart": 83
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "For fully local development, you must specify handleResolver, plcDirectoryUrl, and allowHttp=TRUE. See snippet below."
},
"children": []
}
]
}
]
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 34,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Test Users for atproto localstack:"
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 45,
"byteStart": 40
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
}
]
}
],
"plaintext": "Your test user’s handle has to end in .test."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 25,
"byteStart": 21
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "You can use a simple curl request to create it. See below."
},
"children": []
},
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 5,
"byteStart": 0
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#bold"
}
]
}
],
"plaintext": "Note: The default users didn’t work for me."
},
"children": [
{
"$type": "pub.leaflet.blocks.unorderedList#listItem",
"content": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 47,
"byteStart": 9
},
"features": [
{
"$type": "pub.leaflet.richtext.facet#code"
},
{
"uri": "https://github.com/bluesky-social/atproto/blob/716819fb250d59e36396c1c5f15933d0639f5aa3/packages/dev-env/src/seed/users.ts#L31-L64",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Example: handle=alice.test with pass=alice-pass"
},
"children": []
}
]
}
]
}
]
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "OAuth Client Config"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "typescript",
"plaintext": "/* OAUTH client config */\n const client = new BrowserOAuthClient({\n clientMetadata: getConfig(import.meta.env.PROD),\n // https://www.npmjs.com/package/@atproto/oauth-client-browser\n handleResolver: \"http://127.0.0.1:2583\",\n plcDirectoryUrl: \"http://127.0.0.1:2582\",\n allowHttp: true,\n });"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "Podman Compose for local atproto dev-env"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "yaml",
"plaintext": "# Podman Compose configuration for atproto PDS development\n# Includes PostgreSQL and Redis services\n\nversion: '3.8'\n\nservices:\n postgres:\n image: postgres:latest\n container_name: atproto_pds_postgres\n environment:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: postgres\n POSTGRES_DB: postgres\n PGDATA: /var/lib/postgresql/data/pgdata\n ports:\n - '5433:5432'\n healthcheck:\n test: ['CMD-SHELL', 'pg_isready -U postgres']\n interval: 10s\n timeout: 5s\n retries: 5\n volumes:\n - postgres_data:/var/lib/postgresql\n restart: unless-stopped\n networks:\n - atproto_network\n\n redis:\n image: redis:latest\n container_name: atproto_pds_redis\n ports:\n - '6379:6379'\n healthcheck:\n test: ['CMD', 'redis-cli', 'ping']\n interval: 10s\n timeout: 5s\n retries: 5\n volumes:\n - redis_data:/data\n restart: unless-stopped\n networks:\n - atproto_network\nvolumes:\n postgres_data:\n name: atproto_postgres_data\n redis_data:\n name: atproto_redis_data\n\nnetworks:\n atproto_network:\n name: atproto_network\n driver: bridge"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 2,
"facets": [],
"plaintext": "Test User curl command"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.code",
"language": "shellscript",
"plaintext": "curl -X POST http://localhost:2583/xrpc/com.atproto.server.createAccount \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"handle\": \"yolo.test\",\n \"email\": \"yolo@example.com\",\n \"password\": \"password123\"\n }'"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.horizontalRule"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.header",
"level": 1,
"facets": [],
"plaintext": "Why LocalStacks are Important?"
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [],
"plaintext": "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. "
}
},
{
"$type": "pub.leaflet.pages.linearDocument#block",
"block": {
"$type": "pub.leaflet.blocks.text",
"facets": [
{
"index": {
"byteEnd": 60,
"byteStart": 53
},
"features": [
{
"uri": "https://PDS.rip",
"$type": "pub.leaflet.richtext.facet#link"
}
]
}
],
"plaintext": "Likewise, testing data can pollute the main network. PDS.rip helps with this, but it could ultimately disappear any day."
}
}
]
}
]
},
"description": "Notes with my struggle to get it working on my machine",
"publishedAt": "2026-01-17T23:19:21.291Z"
}