simocracy-indexer · dual-tap-epds-firehose

Simocracy Indexer — dual-tap fan-in from certified.one ePDS firehose

@daviddao

Routed the indexer's firehose ingest around bsky.network's 100-account-per-PDS cap by adding a second tap pointed directly at the ePDS, then proved the path end-to-end with a fresh ePDS account.

Date: 2026-05-06 · Target: simocracy-indexer-production.up.railway.app · pass


Headline

2 / 2
Tap upstreams connected
1
ePDS sim observed via firehose
~2 h
Total implementation time
100 → ∞
Effective ePDS account ceiling

Headline. The simocracy-indexer now consumes two atproto firehoses in parallel: relay1.us-east.bsky.network (Bluesky's relay, for accounts on bsky.social and other public PDSes) and https://certified.one (the ePDS, read directly via com.atproto.sync.subscribeRepos). A new account created on certified.one through the email-login flow had its org.simocracy.sim commit observed on the new tap-epds upstream and ingested into DuckDB without going through bsky.network at all. The 100-account cap on bsky.network's per-PDS quota no longer affects ePDS sims.

Why this matters

Bluesky's reference relay (bsky.network) applies a default 100-account-maximum quota to new PDS instances it crawls. The simocracy-indexer's tap consumer was previously connected only to that relay. So as soon as certified.one (the ePDS used by simocracy's email-login flow) crossed account #100, accounts #101+ would be invisible to our indexer — silently breaking the GraphQL/lookup paths the simocracy-v2 webapp relies on.

The com.atproto.sync.subscribeRepos lexicon is identical on every PDS and every relay — a PDS just emits its own accounts' commits instead of the whole network. So we can subscribe directly to wss://certified.one/xrpc/com.atproto.sync.subscribeRepos for a complete view of certified.one accounts, no matter how many of them exist, with zero coordination required (the firehose is public — see the lexicon: "Public and does not require auth; implemented by PDS and Relay.").

Topology change

Before ───────────────────────────────────────────────────────────────────── bsky.network (relay) ──► tap ──WS──► indexer ──► DuckDB + GraphQL ▲ │ TAP_RELAY_URL=relay1.us-east.bsky.network │ TAP_WS_URL=ws://tap.railway.internal:2480/channel │ └─ blind to certified.one accounts past the 100-cap After ───────────────────────────────────────────────────────────────────── bsky.network (relay) ──► tap ┐ ├─WS─► indexer ──► DuckDB + GraphQL certified.one (ePDS) ──► tap-epds ┘ ▲ │ TAP_WS_URL=ws://tap.railway.internal:2480/channel, │ ws://tap-epds.railway.internal:2480/channel │ └─ each upstream reconnects independently; inserts are idempotent (upsert by URI) so duplicate deliveries are no-ops

What changed

Code (simocracy-indexer @ 3074513)

Railway (project simocracy-indexer, env production)

New servicetap-epds (ghcr.io/bluesky-social/indigo/tap:latest) with TAP_RELAY_URL=https://certified.one Updated varsimocracy-indexer.TAP_WS_URL = ws://tap.railway.internal:2480/channel,ws://tap-epds.railway.internal:2480/channel Deploy methodrailway add --service tap-epds --image … + railway up --service simocracy-indexer

Smoke test

The smoke test exercises the new path end-to-end: a real new account on certified.one, created via simocracy.org's email-login flow, all the way through to a successful GraphQL lookup against the indexer.

1 — Disposable inbox

Acquire a temporary email from mail.tm (temp-mail.org refused with a Cloudflare challenge). The inbox came pre-allocated as 2920iseabal@deltajohnsons.com.

mail.tm temporary inbox provisioned with the email 2920iseabal@deltajohnsons.com
01mail.tm provisions a disposable address with no account creation.

2 — Email login on simocracy.org

Open simocracy.org, click Sign In, switch to the EMAIL tab, paste the disposable address. The webapp calls GET /api/oauth/login?email=…, which performs the ATProto OAuth PAR dance and 302-redirects to auth.certified.one with login_hint=<email>.

03Login dialog with the email tab selected.
04Email pasted in; clicking Continue 302s to certified.one.

3 — certified.one account creation

certified.one emails a 6-digit code (102231 in this run) to the inbox, which we read back from mail.tm.

05Verification challenge on auth.certified.one.
06The "Welcome to Certified" email with the code.

After verification, certified.one prompts for a handle (epds-smoke-test) and renders the OAuth consent screen for the https://www.simocracy.org/client-metadata.json client.

08Handle picker — entered epds-smoke-test.
10OAuth consent: "Grant access to your epds-smoke-test.certified.one account."

Resolving the new handle confirms the PDS placement is exactly what we need:

$ curl -s "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=epds-smoke-test.certified.one"
{"did":"did:plc:njw357tdisvgovexfks6xem2"}

$ curl -s https://plc.directory/did:plc:njw357tdisvgovexfks6xem2 | jq '.service[0].serviceEndpoint'
"https://certified.one"

4 — Mint a sim

Authorising returns to simocracy.org/my-sims as the new account. Clicking MINT YOUR FIRST SIM opens the avatar editor; we name the sim ePDS Smoke 045633, randomise the appearance, and hit SAVE TO ATPROTO to write a org.simocracy.sim commit to certified.one.

14Sim configured, ready to save.
15Toast: "Sim 'ePDS Smoke 045633' created! URL: simocracy.org/sims/epds-smoke-045633"

5 — Verify on the PDS

The record lands on certified.one. Listing the repo confirms the AT-URI:

$ curl -s "https://certified.one/xrpc/com.atproto.repo.listRecords?repo=did:plc:njw357tdisvgovexfks6xem2&collection=org.simocracy.sim" | jq '.records[0] | {uri,cid,name:.value.name}'
{
  "uri": "at://did:plc:njw357tdisvgovexfks6xem2/org.simocracy.sim/3ml7oyn62wk2x",
  "cid": "bafyreidxg7l63xhiodrabidfdrf6qu373dez3nntpywyxu7x66i6kkodwu",
  "name": "ePDS Smoke 045633"
}

6 — Verify via the indexer

The indexer's /stats endpoint reports both upstreams as live, and the GraphQL compat layer returns the new record. Both fields are new in this run — tapUpstreams[] and tapConnected[] are the proof that the multi-upstream code path is the one running in production.

$ curl -s https://simocracy-indexer-production.up.railway.app/stats
{
  "recordCount": 957,
  "knownDids": 51,
  "tapUpstreams": [
    "ws://tap.railway.internal:2480/channel",
    "ws://tap-epds.railway.internal:2480/channel"
  ],
  "tapConnected": [true, true],
  "wsConnected": true,
  "inFlightBackfills": 0,
  ...
}

$ curl -s https://simocracy-indexer-production.up.railway.app/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"query Q($collection:String!,$first:Int){records(collection:$collection,first:$first){edges{node{uri did rkey value}}}}",
       "variables":{"collection":"org.simocracy.sim","first":1000}}' \
  | jq '.data.records.edges | map(select(.node.did == "did:plc:njw357tdisvgovexfks6xem2"))'
[
  {
    "node": {
      "uri":  "at://did:plc:njw357tdisvgovexfks6xem2/org.simocracy.sim/3ml7oyn62wk2x",
      "did":  "did:plc:njw357tdisvgovexfks6xem2",
      "rkey": "3ml7oyn62wk2x",
      "value": { "name": "ePDS Smoke 045633", "$type": "org.simocracy.sim", ... }
    }
  }
]

The CID and AT-URI match the PDS-side listRecords exactly.

7 — Profile page renders

The simocracy webapp resolves the slug epds-smoke-045633 via its alias map and the indexer feeds the profile route the sim record. The rendered page is end-to-end proof that the user-facing path is healthy:

16simocracy.org/sims/epds-smoke-045633 — profile served from indexer data sourced via tap-epds.

Which upstream actually delivered the record?

Two paths could have produced the record-in-DuckDB outcome: (a) tap-epds's firehose subscription, or (b) the simocracy webapp's post-write ping to GET /backfill-did?did=…. They log differently:

SourceCode pathLog line format
Firehose handleTapMessage()maybeAutoBackfillNewDid() on first sighting First record from new DID <did> — auto-backfilling + Auto-backfill <did>: inserted=N
Webapp ping GET /backfill-did handler in the HTTP server Backfill-did <did>: inserted=N

The production logs from this run show only the firehose log line, which is conclusive:

simocracy-indexer:
  Connected to tap[1] (ws://tap-epds.railway.internal:2480/channel)
  Connected to tap[0] (ws://tap.railway.internal:2480/channel)
  ...
  First record from new DID did:plc:njw357tdisvgovexfks6xem2 — auto-backfilling
  Auto-backfill did:plc:njw357tdisvgovexfks6xem2: inserted=1

tap-epds:
  [INFO] connecting to firehose ... url="wss://certified.one/xrpc/com.atproto.sync.subscribeRepos" cursor=0
  [INFO] connected to firehose
  ...
  [INFO] processing resync did="did:plc:njw357tdisvgovexfks6xem2"
  [INFO] fetching repo from PDS pds="https://certified.one"
  [INFO] resync repo complete

Findings

Pass — dual-tap fan-in works as designed

Both upstreams are healthy, the indexer reports them correctly, and a fresh account on certified.one was indexed via the new tap-epds path within seconds of the sim record being written.

Pass — backwards compatibility preserved

Existing single-upstream deployments don't need to change anything; TAP_WS_URL still accepts a single URL. The legacy wsConnected stat is preserved (now true iff any upstream is open) so existing uptime monitors continue to function.

Warning — tap-epds logs a benign 401 on listReposByCollection

The tap binary tries to enumerate the upstream's accounts via com.atproto.sync.listReposByCollection. certified.one auth-gates that endpoint and returns 401 AuthMissing. This is decoupled from the firehose subscription itself (which is public and works) — it just means tap can't pre-warm its cursor cache from a network enumeration. Functionally harmless; the firehose-driven path catches all live commits.

Info — tap-epds has no Railway volume

Unlike tap (which has tap-volume mounted at /data), tap-epds currently runs without a persistent volume, so a redeploy resets its SQLite cursor to 0. The firehose path itself is fine — tap reconnects from the relay's current head — but a long disconnect after a redeploy could miss commits the relay is no longer sending. Mitigated by the indexer's three other recovery layers (STARTUP_BACKFILL, AUTO_BACKFILL_NEW_DIDS, hourly reconciliation) so this is informational, not a blocker.

Reproducing

CodeGainForest/simocracy-indexer @ 3074513 Indexersimocracy-indexer-production.up.railway.app/stats Webappsimocracy.org (email-login flow) Test simsimocracy.org/sims/epds-smoke-045633 Test DIDdid:plc:njw357tdisvgovexfks6xem2 Test recordat://did:plc:njw357tdisvgovexfks6xem2/org.simocracy.sim/3ml7oyn62wk2x Disposable inboxmail.tm (temp-mail.org Cloudflare-blocks headless) Driveragent-browser Screenshotsassets/01-… through assets/16-…