atproto—crates

Entryway

atproto-entryway

atproto-entryway is an account service and OAuth authorization server for a group of PDS instances. It handles sign-up, sign-in, tokens, handles, and PLC identity for every account, and each PDS behind it stores the repositories and blobs.

Important

Although atproto-entryway is being used in production, it is part of a release-candidate version of atproto-crates, so proceed with caution. I suggest pinning a revision, and test each upgrade against your own member PDS instances before rolling it out.

§ 01

What is an entryway?

An entryway is a service that sits in front of one or more PDS instances and owns the “account” side of hosting. The PDS instances behind it keep the data side, which is the “repository” part. For example, Bluesky runs one entryway at bsky.social in front of its many PDS hosts like morel.us-east.host.bsky.network. Users sign up and log in at one name and never need to know which host their account is on. This allows an atmosphere identity provider, like Bluesky and pyroclastic.cloud, to flexibly scale and manage their PDS fleet.

In OAuth terms, the entryway is the authorization server and each PDS is a resource server. The entryway checks passwords, approves OAuth clients, and signs tokens. The PDS checks those tokens and serves the user’s repository, records, and blobs.

Bluesky introduced the term in November 2023, when it described a new “account entryway” service running as a “virtual PDS” at bsky.social. Its reference PDS (@atproto/pds) has an “entryway mode” switched on by PDS_ENTRYWAY_URL. An entryway is not a separate role in the AT Protocol specification. atproto-entryway implements the same contract the reference PDS expects, so its member PDS instances can be atproto-pds in entryway mode, Bluesky’s PDS, or a mix of both.

The split looks like this
ConcernOwner
Email, password, app passwords, invite codesEntryway
Legacy sessions, OAuth grants and tokensEntryway
Which PDS each account lives onEntryway
Service handles (e.g. alice.example.com)Entryway
PLC rotation key for every hosted DIDEntryway
Account portal at /account and OAuth consent pageEntryway
Repository signing keysMember PDS
Repositories, records, blobsMember PDS
Repository and account events (firehose)Member PDS
Account statusEntryway holds the primary copy, the PDS keeps a local copy

§ 02

How does an entryway work?

Account creation

Clients create accounts at the entryway with com.atproto.server.createAccount. The entryway picks a PDS, asks it to reserve a repository signing key, and builds a PLC genesis operation. The genesis contains that signing key, the PDS URL as the atproto_pds service, the handle, and the entryway’s rotation key, with the operator’s offline recovery key listed above it. The entryway signs the genesis with its rotation key and sends it to the PDS. The PDS checks it, creates an empty repository and a local account record, and submits the genesis to the PLC directory. The entryway then returns its own session tokens to the client.

Account creation through an entryway The client calls createAccount at the entryway. The entryway asks the chosen PDS to reserve a signing key, builds and signs the PLC genesis, and sends createAccount with the did, handle and plcOp to the PDS. The PDS validates the operation, creates the repository, submits the genesis to the PLC directory, and reports the account created. The entryway returns the did, handle, DID document and session tokens to the client. Client Entryway Chosen PDS PLC directory com.atproto.server.createAccount com.atproto.server.reserveSigningKey signingKey (did:key) build and sign PLC genesis com.atproto.server.createAccount {did, handle, plcOp} validate op, create repo submit genesis account created did, handle, didDoc, accessJwt, refreshJwt
Figure 1Account creation. Solid arrows are requests, dashed arrows are responses.

A sign-up request to the entryway looks like any other PDS sign-up:

Sign-up requestListing 1
POST /xrpc/com.atproto.server.createAccount
Host: account.example.com
Content-Type: application/json

{
  "handle": "alice.example.com",
  "email": "alice@example.org",
  "password": "correct horse battery staple",
  "inviteCode": "account-example-com-abcde-12345"
}
ResponseListing 2
{
  "did": "did:plc:abc123...",
  "handle": "alice.example.com",
  "didDoc": { "...": "..." },
  "accessJwt": "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJhdCtqd3QifQ...",
  "refreshJwt": "eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJyZWZyZXNoK2p3dCJ9..."
}

The PDS is chosen in this order: the PDS the invite code is bound to, then the PDS marked as default, then any PDS accepting sign-ups, weighted. The entryway records every multi-step workflow in a durable operation log, so if it crashes or a call times out partway through sign-up, it resumes at the recorded step. Bluesky’s PDS cannot finish a genesis that PLC already holds. If a sign-up stalls in that state, the entryway waits ENTRYWAY_ORPHAN_GENESIS_WAIT, tombstones that DID, and starts again under a new one.

The resulting DID document names the PDS, not the entryway, as the account’s service endpoint:

DID documentListing 3
{
  "id": "did:plc:abc123...",
  "alsoKnownAs": ["at://alice.example.com"],
  "verificationMethod": [{
    "id": "did:plc:abc123...#atproto",
    "type": "Multikey",
    "controller": "did:plc:abc123...",
    "publicKeyMultibase": "zQ3sh..."
  }],
  "service": [{
    "id": "#atproto_pds",
    "type": "AtprotoPersonalDataServer",
    "serviceEndpoint": "https://pds.example.com"
  }]
}

The PLC data behind it lists the rotation keys in priority order, with the operator’s recovery key ahead of the entryway’s rotation key:

PLC rotation keysListing 4
{
  "rotationKeys": [
    "did:key:zQ3sh...operator-recovery",
    "did:key:zQ3sh...entryway-rotation"
  ]
}

Sign-in and sessions

Clients sign in at the entryway with com.atproto.server.createSession. A client that sends the same call to a member PDS gets the same result, because the PDS forwards it to the entryway. The entryway returns a legacy access token and refresh token, both signed with its ES256K JWT key. The client finds the account’s PDS from the DID document and sends data requests there.

Sign-in, a data request, and a refresh The client calls createSession at the entryway with an identifier and password and receives an access token addressed to the PDS and a refresh token. It calls createRecord at the account's PDS with the access token; the PDS verifies the signature with the entryway's public key and creates the record. To refresh, the client calls refreshSession at the PDS, which forwards the refresh token to the entryway; the entryway returns a new token pair, spending the old refresh token, and the PDS passes it back to the client. Client Entryway Account’s PDS com.atproto.server.createSession {identifier, password} accessJwt (aud = PDS DID), refreshJwt com.atproto.repo.createRecord Authorization: Bearer accessJwt verify signature with entryway public key record created com.atproto.server.refreshSession forward refresh token new token pair (old refresh token spent) new token pair
Figure 2Sign-in, a data request, and a refresh. Solid arrows are requests, dashed arrows are responses.

The access token is addressed to the account’s PDS and the refresh token is addressed to the entryway. The claims look like this:

Token headers and claimsListing 5
// access token
{ "alg": "ES256K", "typ": "at+jwt" }
{
  "sub": "did:plc:abc123...",
  "aud": "did:web:pds.example.com",
  "scope": "com.atproto.access",
  "iat": 1791000000,
  "exp": 1791007200
}

// refresh token
{ "alg": "ES256K", "typ": "refresh+jwt" }
{
  "sub": "did:plc:abc123...",
  "aud": "did:web:account.example.com",
  "scope": "com.atproto.refresh",
  "jti": "unique-token-id",
  "iat": 1791000000,
  "exp": 1798776000
}

Refresh tokens are single-use. Presenting a refresh token that has already been rotated ends the session. Legacy access tokens last two hours and refresh tokens last 90 days by default.

OAuth

The entryway is the OAuth authorization server for every account it hosts. It serves /.well-known/oauth-authorization-server, /oauth/par, /oauth/authorize, /oauth/token, /oauth/revoke, and /oauth/jwks. Each member PDS publishes /.well-known/oauth-protected-resource naming the entryway, so OAuth clients discover it from the PDS in the usual way.

OAuth access tokens are DPoP-bound ES256K JWTs with no kid, and their aud is the account’s PDS. Refresh tokens are opaque, rotating, and prefixed ref-. A scope longer than 256 characters travels in the access token as ref:<cid>, and member PDS instances look up the full scope through the entryway’s unauthenticated com.atproto.temp.dereferenceScope. OAuth access tokens last one hour by default.

How requests reach the right service

Clients send data requests straight to the PDS named in the account’s DID document. The entryway does not sit in the data path. When a request for an account operation reaches a PDS instead of the entryway, the PDS forwards it in one of two ways.

For credential calls such as createSession, refreshSession, deleteSession, activateAccount, deleteAccount, requestPasswordReset, and resetPassword, the PDS passes the original credential through and the entryway acts on it.

For other account calls, the PDS checks the client’s token itself and then calls the entryway with a short-lived service-auth JWT signed by the user’s repository signing key. The iss is the user’s DID, the aud is the entryway’s DID, and lxm is the method being called. This covers app passwords, invite codes, email changes, getSession, deactivateAccount, requestAccountDelete, PLC operation signing, and handle updates. The entryway resolves the DID, verifies the signature against the #atproto key, and uses the verified iss as the account.

When the entryway changes something a PDS needs to know about, it calls back to that PDS with HTTP Basic auth using the PDS’s admin password. Handle changes go to com.atproto.admin.updateAccountHandle, status changes to com.atproto.admin.updateSubjectStatus, and deletions to com.atproto.admin.deleteAccount.

A status change, from the entryway to a PDSListing 6
POST /xrpc/com.atproto.admin.updateSubjectStatus
Host: pds.example.com
Authorization: Basic YWRtaW46PHBkcy1hZG1pbi1wYXNzd29yZD4=
Content-Type: application/json

{
  "subject": {
    "$type": "com.atproto.admin.defs#repoRef",
    "did": "did:plc:abc123..."
  },
  "deactivated": { "applied": true }
}

Handles and PLC

The entryway answers /.well-known/atproto-did for service handles, looking up the account by the Host header. A member PDS in entryway mode returns 404 on that path, so point wildcard DNS for the handle domains at the entryway. Alternatively, a domain can use the Highport handle backend.

Handle changes go through the entryway. It checks the claim, signs a PLC update with its rotation key, waits until the DID document shows the new handle, tells the PDS, and then records the change. The entryway also signs PLC operations requested through identity.requestPlcOperationSignature and identity.signPlcOperation. It refuses an update that keeps the account in the cluster but drops the operator’s recovery key. An update that moves the account to an outside host may drop it, and the account holder is emailed.

What happens when a service is down

If the entryway is down, access tokens that clients already hold keep working at every PDS until they expire, so reads and writes continue. Sign-in, refresh, sign-up, and handle changes fail until it comes back.

A member PDS keeps no record of entryway tokens. When a session ends, its access token still works at the PDS until it expires. On an atproto-pds member, signing out everywhere and changing the password at the portal end those tokens at once through the extension admin.revokeEntrywaySessions. On a Bluesky PDS member, the token works until it expires.

§ 03

When do you need one? When do you not need one?

You need an entryway when you want to run more than one PDS under one name. Users sign up at one URL, sign in at one URL, approve OAuth clients on one consent page, and get handles under shared domains, while their data is spread across several PDS hosts. This lets you add PDS hosts as you grow, put new accounts on whichever host has room, and keep one PLC rotation key and one password database for everyone.

A single PDS already does everything an entryway does for its own accounts: it stores passwords, issues tokens, runs OAuth, and signs PLC operations. If you run one PDS and expect to keep running one PDS, run it on its own. An entryway adds a second service to operate and back up, and it becomes the only copy of your account database and hot keys.

§ 04

What does an entryway not provide?

  • atproto-entryway does not store repositories or blobs, and it does not hold repository signing keys. Those stay on the member PDS instances.
  • It does not proxy repository, blob, or sync requests. Clients send those to the PDS listed in the DID document.
  • It does not run a firehose. Each member PDS emits its own repository and account events and must be crawled by relays like any other PDS.
  • It does not move repository data between member PDS instances.

§ 05

How to deploy atproto-entryway

Build and run

The entryway binary needs the clap feature. Other Cargo features are hickory-dns (on by default, DNS _atproto handle resolution), smtp (outbound mail), and metrics (a Prometheus exporter on ENTRYWAY_METRICS_BIND).

From the workspaceListing 7
cargo run -p atproto-entryway --features clap \
  --bin entryway -- serve
Container imagesListing 8
docker build -f Dockerfile.entryway -t atproto-entryway .

docker build -f Dockerfile.entryway \
  --build-arg ENTRYWAY_FEATURES=clap,hickory-dns,smtp,metrics \
  -t atproto-entryway .

The production image is distroless, built with clap,hickory-dns,smtp, and sets ENTRYWAY_DATA_DIRECTORY=/data and ENTRYWAY_BIND=0.0.0.0. It listens on port 4900 unless the platform sets PORT. Dockerfile.entryway-debug builds the same binary on Debian with a shell and sqlite3 for running operator commands. Use it only for as long as a job takes, because a shell next to the server can read the PLC rotation key and the JWT key.

Run exactly one entryway instance. Two copies on two volumes will issue tokens and sign PLC operations from different account tables. Point your health check at /_ready, which returns 200 when the database answers and 503 when it does not.

Keys

The entryway uses three private keys and the public half of a fourth. The three private keys must all differ, or the entryway refuses to start. Each key file holds a private key as a did:key string. A file of 64 hex characters is read as a K-256 key.

Keys
KeyDefault fileUsed for
JWT (K-256)keys/jwt.keySigning session and OAuth tokens
PLC rotation (K-256 or P-256)keys/plc-rotation.keyThe rotation key in every hosted DID
Service (K-256 or P-256)keys/service.keyThe #atproto key in the entryway’s own did.json
PLC recovery (public only)ENTRYWAY_PLC_RECOVERY_KEYOffline key listed above the rotation key in every hosted DID

Default files live under ENTRYWAY_DATA_DIRECTORY. Configure the recovery key before the first sign-up, because it is written into every genesis. If the hot rotation key leaks, the recovery key can override its changes. The did:plc specification gives a higher-authority rotation key a 72-hour window to rewrite operations signed by a lower-authority key. Keep the private half offline.

Deployment order

  1. Generate keys and an offline recovery key.

    KeysListing 9
    entryway keys generate --kind all
    atpdid key generate k256   # keep the private half offline
  2. Decide the public URL, the service DID, and the handle domains. All three are hard to change later. OAuth clients store the public URL, the service DID must resolve (keep the did:web default), and the handle domains must match every member PDS.

  3. Write the configuration and check it.

    CheckListing 10
    ENTRYWAY_PUBLIC_URL=https://account.example.com \
    ENTRYWAY_SERVICE_HANDLE_DOMAINS=example.com \
      entryway check
  4. Start the server with entryway serve.

  5. Print the settings every member PDS needs.

    Member settingsListing 11
    entryway keys show

    This prints PDS_ENTRYWAY_URL, PDS_ENTRYWAY_DID, PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX, and PDS_ENTRYWAY_PLC_ROTATION_KEY, plus PDS_ENTRYWAY_PLC_RECOVERY_KEY when a recovery key is configured.

  6. Configure each PDS (see below), restart it, and register it with the entryway.

    Register a PDSListing 12
    entryway pds add --did did:web:pds.example.com \
      --url https://pds.example.com \
      --callback-secret-file /data/secrets/pds-admin \
      --peer-secret-file /data/secrets/pds-peer \
      --default
    entryway pds check

The callback secret file holds the PDS’s admin password. pds check confirms the PDS identity, that its protected-resource metadata names this entryway, and that the admin password works. Register each PDS at exactly the URL its accounts’ DID documents name.

A minimal production configuration:

Production environmentListing 13
ENTRYWAY_PRODUCTION=true
ENTRYWAY_PUBLIC_URL=https://account.example.com
ENTRYWAY_SERVICE_HANDLE_DOMAINS=example.com
ENTRYWAY_PLC_RECOVERY_KEY=did:key:zQ3sh...
ENTRYWAY_ADMIN_PASSWORD_FILE=/data/secrets/admin
ENTRYWAY_DENYLIST_PEPPER_FILE=/data/secrets/pepper
ENTRYWAY_EMAIL_SMTP_URL=smtps://user:pass@smtp.example.com:465
ENTRYWAY_EMAIL_FROM=Example <accounts@example.com>

With ENTRYWAY_PRODUCTION=true, the server will not start without the recovery key, the admin password file, the denylist pepper file, an existing data directory, and HTTPS URLs. Without SMTP, mail is logged and dropped, and nobody can confirm an email address, reset a password, or delete an account. If the entryway sits behind a reverse proxy, set ENTRYWAY_TRUSTED_PROXY_HOPS so rate limits see real client addresses.

§ 06

Configuring atproto-pds behind the entryway

Setting PDS_ENTRYWAY_URL puts atproto-pds into entryway mode. Paste the lines from entryway keys show, add a peer secret, and restart the PDS. Keep its PLC URL and handle domains equal to the entryway’s. Leave its own PDS_HIGHPORT_* settings unset, because atproto-pds refuses to run entryway mode and Highport together.

atproto-pds entryway settings
SettingMeaning
PDS_ENTRYWAY_URLThe entryway’s origin and OAuth issuer. Setting it turns on entryway mode.
PDS_ENTRYWAY_DIDThe entryway’s service DID.
PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEXPublic half of the entryway JWT key, as hex. A comma-separated list during key rotation.
PDS_ENTRYWAY_PLC_ROTATION_KEYThe entryway’s PLC rotation key as a public did:key.
PDS_ENTRYWAY_PLC_RECOVERY_KEYThe operator’s offline recovery key, kept first in getRecommendedDidCredentials.
PDS_ENTRYWAY_PEER_SECRET_FILEA secret sent with every call to the entryway so it trusts the forwarded client address. Use the same file as --peer-secret-file.
PDS_ENTRYWAY_REQUIRE_PROVISIONING_AUTHDefault true. reserveSigningKey and createAccount with a plcOp require a service-auth token from the entryway.
atproto-pds in entryway modeListing 14
PDS_ENTRYWAY_URL=https://account.example.com
PDS_ENTRYWAY_DID=did:web:account.example.com
PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX=02ab...
PDS_ENTRYWAY_PLC_ROTATION_KEY=did:key:zQ3sh...rotation
PDS_ENTRYWAY_PLC_RECOVERY_KEY=did:key:zQ3sh...recovery
PDS_ENTRYWAY_PEER_SECRET_FILE=/secrets/entryway-peer
PDS_SERVICE_HANDLE_DOMAINS=example.com
PDS_DID_PLC_URL=https://plc.directory

Give each PDS a peer secret. Sign-in, sign-up, and password resets are rate-limited per client address. Without the secret, every request a PDS forwards appears to come from the PDS’s own address, so all of its users share one rate-limit bucket. With it, the entryway uses the first X-Forwarded-For hop.

In entryway mode, an atproto-pds member redirects its own portal’s sign-in, credential, and session pages to the entryway’s /account pages.

§ 07

Configuring Bluesky’s PDS behind the entryway

Bluesky’s PDS (@atproto/pds, deployed with bluesky-social/pds) reads four entryway settings. The design document says all four are required once PDS_ENTRYWAY_URL is set.

Bluesky PDS entryway settings
SettingValue
PDS_ENTRYWAY_URLThe entryway’s public base URL and OAuth issuer
PDS_ENTRYWAY_DIDThe entryway’s service DID, used as the audience of PDS-to-entryway service-auth tokens and refresh tokens
PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEXThe entryway’s secp256k1 public key as hex (compressed or uncompressed)
PDS_ENTRYWAY_PLC_ROTATION_KEYThe entryway’s PLC rotation public key as a did:key
These existing settings must also agree with the entryway
SettingRequirement
PDS_SERVICE_DIDThis PDS’s unique DID. Legacy access tokens use it as aud. Register the PDS with the entryway under this DID.
PDS_HOSTNAME (and PDS_PORT on localhost)Sets the origin written into DID documents as the atproto_pds endpoint. Register the PDS with the entryway at this exact URL.
PDS_SERVICE_HANDLE_DOMAINSSame handle domains as ENTRYWAY_SERVICE_HANDLE_DOMAINS
PDS_DID_PLC_URLSame PLC directory as ENTRYWAY_PLC_URL
PDS_ADMIN_PASSWORDThe callback secret you register with entryway pds add
Bluesky’s PDS as a memberListing 15
PDS_HOSTNAME=pds.example.com
PDS_SERVICE_DID=did:web:pds.example.com
PDS_ADMIN_PASSWORD=<same value as the callback secret file>
PDS_SERVICE_HANDLE_DOMAINS=.example.com
PDS_DID_PLC_URL=https://plc.directory
PDS_ENTRYWAY_URL=https://account.example.com
PDS_ENTRYWAY_DID=did:web:account.example.com
PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX=02ab...
PDS_ENTRYWAY_PLC_ROTATION_KEY=did:key:zQ3sh...rotation

Keep the rest of the PDS configuration (JWT secret, data directory, blobstore, AppView, crawlers) as usual.

A Bluesky PDS member behaves differently from an atproto-pds member in three ways. It verifies the entryway’s provisioning tokens by resolving PDS_ENTRYWAY_DID, so the entryway’s DID must resolve. Its resolver refuses IP-literal URLs. It accepts one JWT public key, so rotating the entryway JWT key is a hard switch: its users’ access tokens fail until they refresh. It also has no peer secret setting.

The atproto-entryway conformance harness runs a mixed cluster with @atproto/pds 0.5.36 pinned as one member and atproto-pds as another. Test against the exact version you deploy.

§ 08

Rotating the JWT key

The JWT key can be replaced without signing anyone out on atproto-pds members.

  1. Generate a new key with entryway keys generate --kind jwt, with ENTRYWAY_JWT_KEY_FILE pointing at a new path.
  2. Set ENTRYWAY_JWT_KEY_FILE to the new file and ENTRYWAY_JWT_RETIRED_KEY_FILES to the old one. Run entryway keys show, which now prints a comma-separated key list with the new key first. Set it on every member and restart them.
  3. Restart the entryway. It signs with the new key and still accepts the old one.
  4. Wait ENTRYWAY_REFRESH_TTL (90 days by default), or accept that sessions still holding an old refresh token will need to sign in again.
  5. Unset ENTRYWAY_JWT_RETIRED_KEY_FILES, set each PDS back to the single new key, and restart.

§ 09

Day-to-day operation

The pds, ops, reconcile, handles, and accounts commands, and keys show, open the database directly when run beside the server. From anywhere else, set ENTRYWAY_ADMIN_URL to the public URL and give the operator password, and the commands call the matching admin API method instead.

Operator commands, from anywhereListing 16
export ENTRYWAY_ADMIN_URL=https://account.example.com
export ENTRYWAY_ADMIN_PASSWORD_FILE=./entryway-admin

entryway pds list
entryway ops list --failed
entryway ops retry 42
entryway reconcile          # report drift
entryway reconcile --apply  # repair what is safe

serve runs reconciliation every ENTRYWAY_RECONCILE_INTERVAL. It compares the entryway, every PDS, PLC, and Highport, and repairs safe cases. An account whose DID document names a PDS outside the cluster is marked migrated_out, its sessions end, and the member’s copy is deactivated (never deleted). Because of this, a PDS registered at a URL that differs from the one in its accounts’ DID documents makes every one of its accounts look like it has left. Register each PDS at its exact URL.

Back up the data directory with the server stopped. The image has no sqlite3, and a copy of a live database can capture a torn write. The database migrates forward on start with no downgrade, so back up before every upgrade. The entryway holds the only copy of the account-to-PDS map, the password hashes, and the hot keys.

§ 10

What are all of the configuration options for atproto-entryway?

Every setting is an ENTRYWAY_* environment variable with a matching command-line flag. entryway --help lists them all and is the authoritative reference. Secrets are read from files so they do not appear in --help output or process listings. The one exception is ENTRYWAY_ADMIN_PASSWORD, used by operator commands.

Durations take values like 2m, 1h, and 90d.

Identity and network
VariableTypeDefaultRequiredDescription
ENTRYWAY_PUBLIC_URLURLnoneYesPublic origin and OAuth issuer. HTTPS, no path, no trailing slash. Every PDS compares it to each token’s iss byte for byte.
ENTRYWAY_SERVICE_DIDDIDdid:web: + hostNoThe entryway’s DID. Must equal PDS_ENTRYWAY_DID on every PDS and must resolve.
ENTRYWAY_BINDaddress127.0.0.1NoListen address. The container image sets 0.0.0.0.
ENTRYWAY_PORTinteger4900NoListen port. PORT is used when this is unset.
ENTRYWAY_TRUSTED_PROXY_HOPSinteger0NoNumber of your own reverse proxies in front of the entryway, counted from the end of X-Forwarded-For to find the client address.
ENTRYWAY_PLC_URLURLhttps://plc.directoryNoPLC directory. Must match PDS_DID_PLC_URL on every PDS.
ENTRYWAY_SERVICE_HANDLE_DOMAINSlistnoneYesComma-separated handle domains. Must match PDS_SERVICE_HANDLE_DOMAINS on every PDS.
ENTRYWAY_DATA_DIRECTORYpath./entryway-dataNoHolds entryway.sqlite and keys/. Must exist in production.
ENTRYWAY_PRODUCTIONboolfalseNoTurns development warnings into startup refusals.
ENTRYWAY_LOGstringinfoNoLog filter in tracing env-filter syntax.
Keys and secrets
VariableTypeDefaultRequiredDescription
ENTRYWAY_JWT_KEY_FILEpathkeys/jwt.key in the data directoryNoKey that signs session and OAuth JWTs.
ENTRYWAY_JWT_RETIRED_KEY_FILESlist of pathsunsetNoEarlier JWT key files, comma-separated. Still accepted for verification, never used to sign.
ENTRYWAY_PLC_ROTATION_KEY_FILEpathkeys/plc-rotation.key in the data directoryNoHot rotation key listed in every hosted DID.
ENTRYWAY_SERVICE_KEY_FILEpathkeys/service.key in the data directoryNoThe #atproto key in the entryway’s own did.json.
ENTRYWAY_PLC_RECOVERY_KEYdid:keyunsetIn productionPublic key of the offline recovery key.
ENTRYWAY_ADMIN_PASSWORD_FILEpathunsetIn productionBasic admin password. Without it, every admin method is refused.
ENTRYWAY_DENYLIST_PEPPER_FILEpathunsetIn productionSecret that keys the denylist of refused handles and email addresses. Uses atproto-pds’s peppered HMAC format, so a standalone PDS’s list matches when both share the pepper.
Accounts, sessions, and mail
VariableTypeDefaultRequiredDescription
ENTRYWAY_INVITE_REQUIREDbooltrueNoWhether sign-up needs an invite code.
ENTRYWAY_ACCESS_TTLduration2hNoLifetime of a legacy access token.
ENTRYWAY_REFRESH_TTLduration90dNoLifetime of a refresh token.
ENTRYWAY_OAUTH_ACCESS_TTLduration1hNoLifetime of an OAuth access token, and so how long an ended grant’s token still works at a PDS.
ENTRYWAY_LEXICON_NETWORKbooltrueNoResolve include: permission sets over the network first, falling back to the lexicons bundled with atproto-pds. false uses only the bundle, which can lag.
ENTRYWAY_ORPHAN_GENESIS_WAITduration2mNoHow long a sign-up whose genesis PLC holds, with no account at its PDS, waits before tombstoning that DID and starting again.
ENTRYWAY_PRIVACY_POLICY_URLURLunsetNoReturned by describeServer.
ENTRYWAY_TERMS_OF_SERVICE_URLURLunsetNoReturned by describeServer.
ENTRYWAY_CONTACT_EMAILstringunsetNoReturned by describeServer.
ENTRYWAY_EMAIL_SMTP_URLURLunsetNoOutbound SMTP. Needs the smtp feature. When unset, messages are logged by recipient and subject and dropped.
ENTRYWAY_EMAIL_FROMstringunsetWith SMTPSender address.
Handles and Highport
VariableTypeDefaultRequiredDescription
ENTRYWAY_HANDLE_BACKENDstringwell-knownNoWho answers for service handles: well-known (this entryway, by Host), highport, or per domain, e.g. example.org=highport,example.com=well-known.
ENTRYWAY_HIGHPORT_SERVICE_URLURLunsetWith a Highport domainHighport’s origin.
ENTRYWAY_HIGHPORT_CONTROLLER_DIDDIDunsetWith a Highport domainThe account that owns the Highport bases. An ordinary account of this entryway, hosted on a member PDS.
ENTRYWAY_HIGHPORT_SERVICE_AUDstringdid:web:highport.space#bard_controlNoAudience of the service-auth token sent to Highport.
ENTRYWAY_HIGHPORT_ON_FAILURErefuse or deferrefuseNorefuse fails a sign-up or rename when Highport cannot answer. defer continues and reserves later.
ENTRYWAY_HIGHPORT_TIMEOUT_MSinteger10000NoHighport call timeout in milliseconds.
ENTRYWAY_HIGHPORT_DRY_RUNboolfalseNoLog Highport writes instead of sending them.
ENTRYWAY_HIGHPORT_SEND_PROOFbooltrueNoSend the signed genesis with a sign-up’s reservation.
Operations
VariableTypeDefaultRequiredDescription
ENTRYWAY_RECONCILE_INTERVALduration1hNoHow often serve reconciles and repairs. 0 turns it off.
ENTRYWAY_METRICS_BINDaddressunsetNoWhere serve answers GET /metrics, on its own listener. Needs the metrics feature.
ENTRYWAY_ADMIN_URLURLunsetNoFor operator commands: call this entryway’s admin API at its public URL instead of opening the database.
ENTRYWAY_ADMIN_PASSWORDstringunsetNoOperator password for --admin-url. When unset, it is read from ENTRYWAY_ADMIN_PASSWORD_FILE. Prefer the variable or file over the flag, which shows in process listings.

§ 11

How can I migrate an existing PDS to use atproto-entryway?

An atproto-pds that has been running on its own can join an entryway in one maintenance window. Account holders keep their passwords and app passwords. Their sessions and OAuth grants end at the switch, and everyone signs in again at the entryway.

Before you start, set ENTRYWAY_RECONCILE_INTERVAL=0 on the entryway so reconciliation does not act on accounts while you are adopting them, and back up both the PDS and the entryway.

  1. Freeze changes. Close sign-ups on the PDS and ask account holders not to change passwords, email addresses, or handles during the window.

  2. Register the PDS with the entryway using entryway pds add, at the exact URL its accounts’ DID documents name. Leave the PDS running standalone for now.

  3. Open the adoption window. Set PDS_ENTRYWAY_ADOPTION=true on the PDS and restart it. This turns on a route that hands out every password hash on the server, so keep the window short.

  4. Export the accounts from the PDS. Treat the file as a secret and delete it after the import.

    ExportListing 17
    atproto-pds-admin entryway export --out bundle.json
  5. Import the accounts into the entryway. The import places each account on the exporting PDS, keeps password and app-password hashes, and records each handle. It skips accounts already present, so you can run it again to finish a partial import.

    ImportListing 18
    entryway accounts import bundle.json --dry-run
    entryway accounts import bundle.json
  6. Update each DID’s PLC rotation keys so the entryway’s rotation key and your recovery key are listed. Run without --apply first to see the new keys for each DID, then apply. The did:plc specification allows at most five rotation keys per DID. A DID that would go over that limit is refused, and its keys are listed so you can fix it by hand.

    Rotation keys, previewed then appliedListing 19
    atproto-pds-admin entryway adopt \
      --entryway-key did:key:zQ3sh...rotation \
      --recovery-key did:key:zQ3sh...recovery
    
    atproto-pds-admin entryway adopt \
      --entryway-key did:key:zQ3sh...rotation \
      --recovery-key did:key:zQ3sh...recovery \
      --apply
  7. Switch the PDS to entryway mode. Set the PDS_ENTRYWAY_* settings from entryway keys show and the peer secret, remove PDS_ENTRYWAY_ADOPTION and any PDS_HIGHPORT_* settings, and restart the PDS.

  8. Check the result. Every account can sign in at the entryway, getSession works through the PDS, handles resolve, and entryway reconcile reports no drift. Then restore ENTRYWAY_RECONCILE_INTERVAL and reopen sign-ups at the entryway.

DID documents do not change service endpoint during adoption. Each account stays on the same PDS at the same URL, so relays and AppViews see no move. Service handles now resolve through the entryway, so point the handle domains’ wildcard DNS (or /.well-known/atproto-did routing) at the entryway as part of the switch.

Until the PDS’s own rotation keys are removed from each DID, you can undo the switch by restoring the PDS’s standalone settings. Its pre-switch sessions work again. Changes made at the entryway in the meantime are lost on that path.

The adoption commands above belong to atproto-pds. To bring accounts from a Bluesky PDS behind the entryway, run the Bluesky PDS as a new member in entryway mode for new accounts, or move existing accounts to a member PDS with standard atproto account migration.