A PDS proxy
Gleam 78%
Shell 13%
Nix 8%
Python 1%

README.md

tamis WIP #

Lightweight reverse proxy for atproto PDS record writes (com.atproto.repo.createRecord / putRecord / deleteRecord).

tamis resolves each user's PDS from their DID document, keeps a per-user OAuth session in a supervised OTP actor, signs each write with a DPoP proof, and transparently refreshes + retries once on ExpiredToken. A client authenticates to tamis with a per-user API key; tamis authenticates to the PDS with the OAuth session.

Onboarding & admin UI #

Users self-serve through a small web UI (server-rendered with Lustre):

  1. Log in at / with atproto OAuth (handle or DID). DPoP/PKCE/PAR are handled by atproto_client. tamis is a confidential client (private_key_jwt): it authenticates to each authorization server with an assertion signed by a key it generates on first run and persists, publishing the public half at /oauth/jwks.json and its metadata at /oauth-client-metadata.json.
  2. That login is the onboarding — the resulting token set (DPoP-bound access token + rotating refresh token) becomes tamis's write credential, and tamis mints you an API key. No app-password is involved.
  3. Set your OAuth scope on the dashboard (e.g. atproto transition:generic, or narrower). It's requested at your next login and the PDS enforces it on every write. Change it and re-connect to widen or narrow what tamis can do.

The session is kept alive indefinitely by a background liveness job (every 72h) that refreshes every stored session, rotating its refresh token and resetting the ~180-day refresh window long before it can lapse. A session ends only if the user revokes the grant or the server is down continuously for the whole window.

The single admin (the DID in ADMIN_DID) additionally gets an allow-list panel to authorize/remove DIDs; each authorized user connects by logging in. The allow-list, API keys, and OAuth sessions persist to the JSON file at TAMIS_STATE.

Writing through tamis #

Send the record-write XRPC calls to tamis with your API key as a bearer token:

curl -X POST "$TAMIS/xrpc/com.atproto.repo.createRecord" \
  -H "authorization: Bearer $TAMIS_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"repo":"did:plc:you…","collection":"app.bsky.feed.post","record":{…}}'

The repo must be your own DID (the key's owner); writing to any other repo is 403. A missing/invalid key is 401. Only createRecord, putRecord, and deleteRecord are proxied. Rotating the key on the dashboard revokes the old one.

Configure #

All configuration is via the environment. For convenience a .env file is loaded at startup (path is the first CLI argument, default .env); real environment variables take precedence over it, and a missing file is ignored.

TAMIS_PUBLIC_URL must be the URL the PDS's authorization server uses to reach tamis. In production that's a public https:// URL (the auth server fetches /oauth-client-metadata.json and /oauth/jwks.json from it), and tamis acts as a confidential client. For local development, set it to an http loopback origin (e.g. http://127.0.0.1:3000) and tamis automatically falls back to atproto's "localhost" public dev client — no hosted metadata, keyset, or tunnel needed. Use 127.0.0.1 (not localhost) and drive the browser flow through the same origin so the redirect and session cookie line up; the auth server must permit localhost dev clients (Bluesky's does).

Env var Default Meaning
ADMIN_DID — (required) DID granted admin rights in the UI
TAMIS_PUBLIC_URL — (required) public base URL; derives Oauth client_id/redirect
TAMIS_SECRET — (required) secret for signing session cookies
TAMIS_ADDR 127.0.0.1:3000 listen address
TAMIS_STATE tamis-state.json persisted allow-list + OAuth sessions
TAMIS_CLIENT_KEY tamis-client-key.pem confidential-client signing key (generated if absent)
TAMIS_PLC_URL https://plc.directory PLC directory for did:plc lookup (write-proxying)
TAMIS_OAUTH_RESOLVER https://slingshot.microcosm.blue handle/DID → PDS resolver for OAuth login
TAMIS_OAUTH_SCOPE atproto default OAuth scope (per-user, editable in the UI)
TAMIS_ALLOW_HTTP unset (https-only) set to allow plain-http upstreams (dev)

Develop #

nix develop            # gleam + erlang + rebar3
gleam test
gleam format
ADMIN_DID=did:plc:... TAMIS_PUBLIC_URL=https://... TAMIS_SECRET=... gleam run
gleam run                 # or put the vars in a .env file
gleam run -- prod.env     # ...or point at a specific env file
nix run .#e2e          # full local PDS+PLC end-to-end suite (Linux only)

License #

MIT — see LICENSE.