···3535/sonar
3636/sonar-cli
3737/stress
3838-/tap
3838+/tab
39394040# Don't ignore this file itself, or other specific dotfiles
4141!.gitignore
+60-60
README.md
···11-`tap`: atproto sync utility
11+`tab`: atproto sync utility
22========================================
3344-Tap simplifies AT sync by handling the firehose/jetstream connection, verification, backfill, and filtering. Your application connects to a Tap and receives simple JSON events for only the repos and collections you care about, no need to worry about binary formats for validating cryptographic signatures.
44+tab simplifies AT sync by handling the firehose/jetstream connection, verification, backfill, and filtering. Your application connects to a tab and receives simple JSON events for only the repos and collections you care about, no need to worry about binary formats for validating cryptographic signatures.
5566Features and design decisions:
77···1414- SQLite or Postgres backend
1515- designed for moderate scale (millions of repos, 30k+ events/sec)
16161717-There is a convenient client library in Typescript for working with Tap at [@atproto/tap](https://github.com/bluesky-social/atproto/blob/main/packages/tap/README.md)
1717+There is a convenient client library in Typescript for working with tab at [@atproto/tab](https://github.com/bluesky-social/atproto/blob/main/packages/tab/README.md)
18181919-⚠️ Tap is still in beta and may have bugs. Please report any issues you encounter.
1919+⚠️ tab is still in beta and may have bugs. Please report any issues you encounter.
20202121## Quick Start
22222323```bash
2424-# Run tap
2525-go run ./cmd/tap run --disable-acks=true
2626-# By default, the service uses SQLite at `./tap.db` and binds to port `:2480`.
2424+# Run tab
2525+go run ./cmd/tab run --disable-acks=true
2626+# By default, the service uses SQLite at `./tab.db` and binds to port `:2480`.
27272828# In a separate terminal, connect to receive events:
2929websocat ws://localhost:2480/channel
···38383939## Local Development
40404141-When running Tap locally, restarts are frequent (e.g. sleep/wake, code changes). By default, Tap resumes from its saved firehose cursor on restart, which can mean replaying millions of events if the cursor is hours or days old. For local dev with a small number of tracked repos, this replay is unnecessary — any repos that changed while offline will be detected and resynced automatically.
4141+When running tab locally, restarts are frequent (e.g. sleep/wake, code changes). By default, tab resumes from its saved firehose cursor on restart, which can mean replaying millions of events if the cursor is hours or days old. For local dev with a small number of tracked repos, this replay is unnecessary — any repos that changed while offline will be detected and resynced automatically.
42424343Recommended local dev config:
44444545```bash
4646-go run ./cmd/tap run --no-replay --disable-acks
4646+go run ./cmd/tab run --no-replay --disable-acks
4747```
48484949Tips:
···73737474Environment variables or CLI flags:
75757676-- `TAP_DATABASE_URL`: database connection string, SQLite or PostgreSQL (default: `sqlite://./tap.db`)
7777-- `TAP_MAX_DB_CONNS`: maximum number of database connections (default: `32`)
7878-- `TAP_BIND`: HTTP server address (default: `:2480`)
7979-- `TAP_PLC_URL`: PLC directory HTTP/HTTPS URL (default: `https://plc.directory`)
8080-- `TAP_RELAY_URL`: AT Protocol relay HTTP/HTTPS URL (default: `https://relay1.us-east.bsky.network`)
8181-- `TAP_LIGHT_RAIL_URL`: Light Rail HTTP/HTTPS URL (default: ``, but `https://lightrail.microcosm.blue` is a instance you can use)
8282-- `TAP_JETSTREAM_URL`: If set uses the Jetstream over the firehose/relay. Other firehose settings apply to the jetstream connection (default: ``, but `https://jetstream1.us-east.bsky.network` is a public node hosted by Bluesky that you can use)
8383-- `TAP_FIREHOSE_PARALLELISM`: concurrent firehose event processors (default: `10`)
8484-- `TAP_RESYNC_PARALLELISM`: concurrent resync workers (default: `5`)
8585-- `TAP_OUTBOX_PARALLELISM`: concurrent outbox workers (default: `1`)
8686-- `TAP_CURSOR_SAVE_INTERVAL`: how often to persist upstream firehose cursor (default: `1s`)
8787-- `TAP_NO_REPLAY`: skip saved cursor and connect to firehose head on startup; incompatible with `TAP_FULL_NETWORK`, not recommended for production (default: `false`)
8888-- `TAP_REPO_FETCH_TIMEOUT`: timeout for fetching repo CARs from PDS (default: `300s`)
8989-- `TAP_IDENT_CACHE_SIZE`: size of in-process identity cache (default: `2000000`)
9090-- `TAP_OUTBOX_CAPACITY`: rough size of outbox before back pressure is applied (default: `100000`)
9191-- `TAP_FULL_NETWORK`: track all repos on the network (default: `false`)
9292-- `TAP_SIGNAL_COLLECTION`: track all repos with at least one record in this collection (e.g. `app.bsky.actor.profile`)
9393-- `TAP_LIGHT_RAIL_SIGNAL_COLLECTIONS`: track all repos with at least one record in these collections. Can not be used with `TAP_SIGNAL_COLLECTION` (e.g. `community.lexicon.calendar.rsvp,community.lexicon.calendar.event`)
9494-- `TAP_COLLECTION_FILTERS`: comma-separated collection filters, wildcards accepted (e.g., `app.bsky.feed.post,app.bsky.graph.*`)
9595-- `TAP_DISABLE_ACKS`: fire-and-forget mode, no client acks (default: `false`)
9696-- `TAP_WEBHOOK_URL`: webhook URL for event delivery (disables WebSocket mode)
9797-- `TAP_OUTBOX_ONLY`: run in outbox-only mode (no firehose, resync, or enumeration) (default: `false`)
9898-- `TAP_ADMIN_PASSWORD`: Basic auth admin password required for all requests (if set)
9999-- `TAP_RETRY_TIMEOUT`: timeout before retrying unacked events (default: `60s`)
100100-- `TAP_LOG_LEVEL`: log verbosity (`debug`, `info`, `warn`, `error`, default: `info`)
101101-- `TAP_METRICS_LISTEN`: address for metrics/pprof server (disabled if empty)
7676+- `tab_DATABASE_URL`: database connection string, SQLite or PostgreSQL (default: `sqlite://./tab.db`)
7777+- `tab_MAX_DB_CONNS`: maximum number of database connections (default: `32`)
7878+- `tab_BIND`: HTTP server address (default: `:2480`)
7979+- `tab_PLC_URL`: PLC directory HTTP/HTTPS URL (default: `https://plc.directory`)
8080+- `tab_RELAY_URL`: AT Protocol relay HTTP/HTTPS URL (default: `https://relay1.us-east.bsky.network`)
8181+- `tab_LIGHT_RAIL_URL`: Light Rail HTTP/HTTPS URL (default: ``, but `https://lightrail.microcosm.blue` is a instance you can use)
8282+- `tab_JETSTREAM_URL`: If set uses the Jetstream over the firehose/relay. Other firehose settings apply to the jetstream connection (default: ``, but `https://jetstream1.us-east.bsky.network` is a public node hosted by Bluesky that you can use)
8383+- `tab_FIREHOSE_PARALLELISM`: concurrent firehose event processors (default: `10`)
8484+- `tab_RESYNC_PARALLELISM`: concurrent resync workers (default: `5`)
8585+- `tab_OUTBOX_PARALLELISM`: concurrent outbox workers (default: `1`)
8686+- `tab_CURSOR_SAVE_INTERVAL`: how often to persist upstream firehose cursor (default: `1s`)
8787+- `tab_NO_REPLAY`: skip saved cursor and connect to firehose head on startup; incompatible with `tab_FULL_NETWORK`, not recommended for production (default: `false`)
8888+- `tab_REPO_FETCH_TIMEOUT`: timeout for fetching repo CARs from PDS (default: `300s`)
8989+- `tab_IDENT_CACHE_SIZE`: size of in-process identity cache (default: `2000000`)
9090+- `tab_OUTBOX_CAPACITY`: rough size of outbox before back pressure is applied (default: `100000`)
9191+- `tab_FULL_NETWORK`: track all repos on the network (default: `false`)
9292+- `tab_SIGNAL_COLLECTION`: track all repos with at least one record in this collection (e.g. `app.bsky.actor.profile`)
9393+- `tab_LIGHT_RAIL_SIGNAL_COLLECTIONS`: track all repos with at least one record in these collections. Can not be used with `tab_SIGNAL_COLLECTION` (e.g. `community.lexicon.calendar.rsvp,community.lexicon.calendar.event`)
9494+- `tab_COLLECTION_FILTERS`: comma-separated collection filters, wildcards accepted (e.g., `app.bsky.feed.post,app.bsky.graph.*`)
9595+- `tab_DISABLE_ACKS`: fire-and-forget mode, no client acks (default: `false`)
9696+- `tab_WEBHOOK_URL`: webhook URL for event delivery (disables WebSocket mode)
9797+- `tab_OUTBOX_ONLY`: run in outbox-only mode (no firehose, resync, or enumeration) (default: `false`)
9898+- `tab_ADMIN_PASSWORD`: Basic auth admin password required for all requests (if set)
9999+- `tab_RETRY_TIMEOUT`: timeout before retrying unacked events (default: `60s`)
100100+- `tab_LOG_LEVEL`: log verbosity (`debug`, `info`, `warn`, `error`, default: `info`)
101101+- `tab_METRICS_LISTEN`: address for metrics/pprof server (disabled if empty)
102102103103## Delivery Modes
104104105105-Tap supports three delivery modes:
105105+tab supports three delivery modes:
106106107107-**WebSocket with acks** (default): Client sends acks each event once it has been processed/persisted. Ensures that no data is lost and client does not need to handle cursors. It's recommended to use a client library such as [@atproto/tap](https://github.com/bluesky-social/atproto/tree/main/packages/tap/README.md) when using this mode.
107107+**WebSocket with acks** (default): Client sends acks each event once it has been processed/persisted. Ensures that no data is lost and client does not need to handle cursors. It's recommended to use a client library such as [@atproto/tab](https://github.com/bluesky-social/atproto/tree/main/packages/tab/README.md) when using this mode.
108108109109-**Fire-and-forget**: Set `TAP_DISABLE_ACKS=true`. Events are sent and considered "acked" once the client receives them. Simpler but may result in data loss. Recommended for testing purposes or when data integrity is not critical.
109109+**Fire-and-forget**: Set `tab_DISABLE_ACKS=true`. Events are sent and considered "acked" once the client receives them. Simpler but may result in data loss. Recommended for testing purposes or when data integrity is not critical.
110110111111-**Webhook**: Set `TAP_WEBHOOK_URL=http://...`. Events are POSTed as JSON. Events considered "acked" once the webhook responds with a 200. Recommended for lower throughput serverless environments.
111111+**Webhook**: Set `tab_WEBHOOK_URL=http://...`. Events are POSTed as JSON. Events considered "acked" once the webhook responds with a 200. Recommended for lower throughput serverless environments.
112112113113114114## Network Boundary Modes
115115116116-Tap syncs a subset of repos in the network. It can operate in three modes for determining this network boundary.
116116+tab syncs a subset of repos in the network. It can operate in three modes for determining this network boundary.
117117118118-**Dynamically Configured** (default): Tap starts out tracking no repos. Specific repos can then by added via `/repos/add` and removed via `/repos/remove`.
118118+**Dynamically Configured** (default): tab starts out tracking no repos. Specific repos can then by added via `/repos/add` and removed via `/repos/remove`.
119119120120-**Collection Signal**: Set `TAP_SIGNAL_COLLECTION=com.example.nsid`. Track all repos that have at least one record in the specified collection. Many applications create a "declaration" or "profile" in a repo when that repo uses that application
120120+**Collection Signal**: Set `tab_SIGNAL_COLLECTION=com.example.nsid`. Track all repos that have at least one record in the specified collection. Many applications create a "declaration" or "profile" in a repo when that repo uses that application
121121122122-**Full Network**: Set `TAP_FULL_NETWORK=true`. Enumerates and tracks all findable repos on the entire network. Resource-intensive and takes days/weeks to complete backfill.
122122+**Full Network**: Set `tab_FULL_NETWORK=true`. Enumerates and tracks all findable repos on the entire network. Resource-intensive and takes days/weeks to complete backfill.
123123124124## Collection Filtering
125125126126-After narrowing down the network to a subset of repos, Tap can further filter records down to a specified set of collections. Filters apply to record events only. Identity events are always delivered for tracked repos.
126126+After narrowing down the network to a subset of repos, tab can further filter records down to a specified set of collections. Filters apply to record events only. Identity events are always delivered for tracked repos.
127127128128-If you are interested syncing all of a single record type, it is important to specify that collection as both the signal collection and the filter collection. For example: `TAP_SIGNAL_COLLECTION=com.example.nsid TAP_COLLECTION_FILTERS=com.example.nsid`
128128+If you are interested syncing all of a single record type, it is important to specify that collection as both the signal collection and the filter collection. For example: `tab_SIGNAL_COLLECTION=com.example.nsid tab_COLLECTION_FILTERS=com.example.nsid`
129129130130Collection filters use wildcards but only at the period breaks in NSIDs. For example:
131131132132-`TAP_COLLECTION_FILTERS=app.bsky.feed.post,app.bsky.graph.*`
132132+`tab_COLLECTION_FILTERS=app.bsky.feed.post,app.bsky.graph.*`
133133134134135135## Event Format
···178178179179When a repo is added (via `/repos/add`, full network mode, or collection discovery):
180180181181-1. **Historical backfill**: Tap fetches the full repo from the account's PDS using `com.atproto.sync.getRepo`
181181+1. **Historical backfill**: tab fetches the full repo from the account's PDS using `com.atproto.sync.getRepo`
1821822. **Live event buffering**: Any firehose events for this repo during backfill are held in memory
1831833. **Ordering guarantee**: Historical events (marked `live: false`) are delivered first
1841844. **Cutover**: After historical events complete, buffered live events are drained
···188188189189### Per-Repo Ordering Rules
190190191191-Tap offloads cursor management and takes care of delivery guarantees. Events are delivered *at least once*. Events may be delivered more than once if Tap crashes and restarts before receiving an ack for a given event or if the event times out before being acked (default 10s).
191191+tab offloads cursor management and takes care of delivery guarantees. Events are delivered *at least once*. Events may be delivered more than once if tab crashes and restarts before receiving an ack for a given event or if the event times out before being acked (default 10s).
192192193193-There is no global ordering of events across repos. However Tap will ensure ordering within each repo and will avoid sending the next event until the previous event has completed processing.
193193+There is no global ordering of events across repos. However tab will ensure ordering within each repo and will avoid sending the next event until the previous event has completed processing.
194194195195Events for the same repo are delivered with strict ordering:
196196···208208209209## Authentication
210210211211-If exposing Tap to the internet, you should set `TAP_ADMIN_PASSWORD` to require authentication for all API requests.
211211+If exposing tab to the internet, you should set `tab_ADMIN_PASSWORD` to require authentication for all API requests.
212212213213-Tap uses HTTP Basic authentication with the username `admin`. Basic auth works by concatenating the username and password with a colon (`admin:yourpassword`), then base64-encoding the result. This is sent in the `Authorization` header as `Basic <encoded-value>`.
213213+tab uses HTTP Basic authentication with the username `admin`. Basic auth works by concatenating the username and password with a colon (`admin:yourpassword`), then base64-encoding the result. This is sent in the `Authorization` header as `Basic <encoded-value>`.
214214215215Example with curl:
216216```bash
···219219 -d '{"dids": ["did:plc:..."]}'
220220```
221221222222-When using webhook mode, Tap sends the same Basic auth credentials to your webhook endpoint.
222222+When using webhook mode, tab sends the same Basic auth credentials to your webhook endpoint.
223223224224## Operations
225225226226-Tap logs to stdout in JSON format. The firehose consumer automatically reconnects with exponential backoff on relay failures. Cursor position is saved periodically (default 1 second) and restored on restart.
226226+tab logs to stdout in JSON format. The firehose consumer automatically reconnects with exponential backoff on relay failures. Cursor position is saved periodically (default 1 second) and restored on restart.
227227228228SQLite is tuned for high write throughput: WAL mode, 10-second busy timeout, `synchronous=NORMAL`, 64MB cache, batched deletes. The outbox buffers up to 1M pending events in memory.
229229···233233234234## Distribution & Deployment
235235236236-Tap is distributed as a single Go binary and is easy to build and run.
236236+tab is distributed as a single Go binary and is easy to build and run.
237237238238**Build from source:**
239239```bash
240240-go build -o tap ./cmd/tap
241241-./tap run
240240+go build -o tab ./cmd/tab
241241+./tab run
242242```
243243244244**Docker:**
245245246246A pre-built Docker image is also available:
247247```bash
248248-docker pull ghcr.io/bluesky-social/indigo/tap:latest
249249-docker run -p 2480:2480 ghcr.io/bluesky-social/indigo/tap:latest
248248+docker pull ghcr.io/bluesky-social/indigo/tab:latest
249249+docker run -p 2480:2480 ghcr.io/bluesky-social/indigo/tab:latest
250250```
251251252252To persist data, mount a volume at `/data`:
253253```bash
254254-docker run -p 2480:2480 -v ./data:/data ghcr.io/bluesky-social/indigo/tap:latest
254254+docker run -p 2480:2480 -v ./data:/data ghcr.io/bluesky-social/indigo/tab:latest
255255```
256256257257-The Dockerfile is included in the repo at `cmd/tap/Dockerfile` if you need to build your own image.
257257+The Dockerfile is included in the repo at `cmd/tab/Dockerfile` if you need to build your own image.
+5-5
cmd/tap/Dockerfile
cmd/tab/Dockerfile
···11# Run this dockerfile from the top level of the indigo git repository like:
22#
33-# podman build -f ./cmd/tap/Dockerfile -t tap .
33+# podman build -f ./cmd/tab/Dockerfile -t tab .
4455### Compile stage
66FROM golang:1.26-alpine3.22 AS build-env
···11111212# timezone data for alpine builds
1313RUN GIT_VERSION=$(git describe --tags --long --always) && \
1414- go build -tags timetzdata -o /tap ./cmd/tap
1414+ go build -tags timetzdata -o /tab ./cmd/tab
15151616### Run stage
1717FROM alpine:3.22
···21212222RUN mkdir -p /data
2323WORKDIR /data
2424-COPY --from=build-env /tap /
2424+COPY --from=build-env /tab /
25252626# small things to make golang binaries work well under alpine
2727ENV GODEBUG=netdns=go
···29293030EXPOSE 2480
31313232-CMD ["/tap", "run"]
3232+CMD ["/tab", "run"]
33333434LABEL org.opencontainers.image.source=https://github.com/bluesky-social/indigo
3535-LABEL org.opencontainers.image.description="AT Protocol Sync Utility (tap)"
3535+LABEL org.opencontainers.image.description="AT Protocol Sync Utility (tab)"
3636LABEL org.opencontainers.image.licenses=MIT
···44 "testing"
55 "time"
6677- "github.com/bluesky-social/indigo/cmd/tap/models"
87 "github.com/stretchr/testify/assert"
98 "github.com/stretchr/testify/require"
99+ "tangled.org/pds.dad/tab/cmd/tab/models"
1010)
11111212func TestFireAndForget_BasicDelivery(t *testing.T) {
···208208// TestWebsocketAck_FastPathLiveBlocksHistorical verifies that when a live event
209209// is sent via the DIDWorker's fast path (first event for a DID), subsequent
210210// historical events are blocked until the live event is acked.
211211-// Regression test for: bugfix(cmd/tap): prevent DID worker live-barrier stall
211211+// Regression test for: bugfix(cmd/tab): prevent DID worker live-barrier stall
212212func TestWebsocketAck_FastPathLiveBlocksHistorical(t *testing.T) {
213213 te := newTestEnv(t, testEnvOpts{
214214 outboxMode: OutboxModeWebsocketAck,