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.
| Concern | Owner |
|---|---|
| Email, password, app passwords, invite codes | Entryway |
| Legacy sessions, OAuth grants and tokens | Entryway |
| Which PDS each account lives on | Entryway |
Service handles (e.g. alice.example.com) | Entryway |
| PLC rotation key for every hosted DID | Entryway |
Account portal at /account and OAuth consent page | Entryway |
| Repository signing keys | Member PDS |
| Repositories, records, blobs | Member PDS |
| Repository and account events (firehose) | Member PDS |
| Account status | Entryway 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.
A sign-up request to the entryway looks like any other PDS sign-up:
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"
}
{
"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:
{
"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:
{
"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.
The access token is addressed to the account’s PDS and the refresh token is addressed to the entryway. The claims look like this:
// 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.
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).
cargo run -p atproto-entryway --features clap \
--bin entryway -- serve
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.
| Key | Default file | Used for |
|---|---|---|
| JWT (K-256) | keys/jwt.key | Signing session and OAuth tokens |
| PLC rotation (K-256 or P-256) | keys/plc-rotation.key | The rotation key in every hosted DID |
| Service (K-256 or P-256) | keys/service.key | The #atproto key in the entryway’s own did.json |
| PLC recovery (public only) | ENTRYWAY_PLC_RECOVERY_KEY | Offline 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
-
Generate keys and an offline recovery key.
KeysListing 9 entryway keys generate --kind all atpdid key generate k256 # keep the private half offline 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:webdefault), and the handle domains must match every member PDS.-
Write the configuration and check it.
CheckListing 10 ENTRYWAY_PUBLIC_URL=https://account.example.com \ ENTRYWAY_SERVICE_HANDLE_DOMAINS=example.com \ entryway check Start the server with
entryway serve.-
Print the settings every member PDS needs.
Member settingsListing 11 entryway keys showThis prints
PDS_ENTRYWAY_URL,PDS_ENTRYWAY_DID,PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX, andPDS_ENTRYWAY_PLC_ROTATION_KEY, plusPDS_ENTRYWAY_PLC_RECOVERY_KEYwhen a recovery key is configured. -
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:
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.
| Setting | Meaning |
|---|---|
| PDS_ENTRYWAY_URL | The entryway’s origin and OAuth issuer. Setting it turns on entryway mode. |
| PDS_ENTRYWAY_DID | The entryway’s service DID. |
| PDS_ENTRYWAY_JWT_VERIFY_KEY_K256_PUBLIC_KEY_HEX | Public half of the entryway JWT key, as hex. A comma-separated list during key rotation. |
| PDS_ENTRYWAY_PLC_ROTATION_KEY | The entryway’s PLC rotation key as a public did:key. |
| PDS_ENTRYWAY_PLC_RECOVERY_KEY | The operator’s offline recovery key, kept first in getRecommendedDidCredentials. |
| PDS_ENTRYWAY_PEER_SECRET_FILE | A 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_AUTH | Default true. reserveSigningKey and createAccount with a plcOp require a service-auth token from the entryway. |
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.
| Setting | Value |
|---|---|
| PDS_ENTRYWAY_URL | The entryway’s public base URL and OAuth issuer |
| PDS_ENTRYWAY_DID | The 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_HEX | The entryway’s secp256k1 public key as hex (compressed or uncompressed) |
| PDS_ENTRYWAY_PLC_ROTATION_KEY | The entryway’s PLC rotation public key as a did:key |
| Setting | Requirement |
|---|---|
| PDS_SERVICE_DID | This 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_DOMAINS | Same handle domains as ENTRYWAY_SERVICE_HANDLE_DOMAINS |
| PDS_DID_PLC_URL | Same PLC directory as ENTRYWAY_PLC_URL |
| PDS_ADMIN_PASSWORD | The callback secret you register with entryway pds add |
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.
- Generate a new key with
entryway keys generate --kind jwt, withENTRYWAY_JWT_KEY_FILEpointing at a new path. - Set
ENTRYWAY_JWT_KEY_FILEto the new file andENTRYWAY_JWT_RETIRED_KEY_FILESto the old one. Runentryway keys show, which now prints a comma-separated key list with the new key first. Set it on every member and restart them. - Restart the entryway. It signs with the new key and still accepts the old one.
- Wait
ENTRYWAY_REFRESH_TTL(90 days by default), or accept that sessions still holding an old refresh token will need to sign in again. - 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.
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.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
| ENTRYWAY_PUBLIC_URL | URL | none | Yes | Public origin and OAuth issuer. HTTPS, no path, no trailing slash. Every PDS compares it to each token’s iss byte for byte. |
| ENTRYWAY_SERVICE_DID | DID | did:web: + host | No | The entryway’s DID. Must equal PDS_ENTRYWAY_DID on every PDS and must resolve. |
| ENTRYWAY_BIND | address | 127.0.0.1 | No | Listen address. The container image sets 0.0.0.0. |
| ENTRYWAY_PORT | integer | 4900 | No | Listen port. PORT is used when this is unset. |
| ENTRYWAY_TRUSTED_PROXY_HOPS | integer | 0 | No | Number 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_URL | URL | https://plc.directory | No | PLC directory. Must match PDS_DID_PLC_URL on every PDS. |
| ENTRYWAY_SERVICE_HANDLE_DOMAINS | list | none | Yes | Comma-separated handle domains. Must match PDS_SERVICE_HANDLE_DOMAINS on every PDS. |
| ENTRYWAY_DATA_DIRECTORY | path | ./entryway-data | No | Holds entryway.sqlite and keys/. Must exist in production. |
| ENTRYWAY_PRODUCTION | bool | false | No | Turns development warnings into startup refusals. |
| ENTRYWAY_LOG | string | info | No | Log filter in tracing env-filter syntax. |
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
| ENTRYWAY_JWT_KEY_FILE | path | keys/jwt.key in the data directory | No | Key that signs session and OAuth JWTs. |
| ENTRYWAY_JWT_RETIRED_KEY_FILES | list of paths | unset | No | Earlier JWT key files, comma-separated. Still accepted for verification, never used to sign. |
| ENTRYWAY_PLC_ROTATION_KEY_FILE | path | keys/plc-rotation.key in the data directory | No | Hot rotation key listed in every hosted DID. |
| ENTRYWAY_SERVICE_KEY_FILE | path | keys/service.key in the data directory | No | The #atproto key in the entryway’s own did.json. |
| ENTRYWAY_PLC_RECOVERY_KEY | did:key | unset | In production | Public key of the offline recovery key. |
| ENTRYWAY_ADMIN_PASSWORD_FILE | path | unset | In production | Basic admin password. Without it, every admin method is refused. |
| ENTRYWAY_DENYLIST_PEPPER_FILE | path | unset | In production | Secret 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. |
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
| ENTRYWAY_INVITE_REQUIRED | bool | true | No | Whether sign-up needs an invite code. |
| ENTRYWAY_ACCESS_TTL | duration | 2h | No | Lifetime of a legacy access token. |
| ENTRYWAY_REFRESH_TTL | duration | 90d | No | Lifetime of a refresh token. |
| ENTRYWAY_OAUTH_ACCESS_TTL | duration | 1h | No | Lifetime of an OAuth access token, and so how long an ended grant’s token still works at a PDS. |
| ENTRYWAY_LEXICON_NETWORK | bool | true | No | Resolve 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_WAIT | duration | 2m | No | How 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_URL | URL | unset | No | Returned by describeServer. |
| ENTRYWAY_TERMS_OF_SERVICE_URL | URL | unset | No | Returned by describeServer. |
| ENTRYWAY_CONTACT_EMAIL | string | unset | No | Returned by describeServer. |
| ENTRYWAY_EMAIL_SMTP_URL | URL | unset | No | Outbound SMTP. Needs the smtp feature. When unset, messages are logged by recipient and subject and dropped. |
| ENTRYWAY_EMAIL_FROM | string | unset | With SMTP | Sender address. |
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
| ENTRYWAY_HANDLE_BACKEND | string | well-known | No | Who 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_URL | URL | unset | With a Highport domain | Highport’s origin. |
| ENTRYWAY_HIGHPORT_CONTROLLER_DID | DID | unset | With a Highport domain | The account that owns the Highport bases. An ordinary account of this entryway, hosted on a member PDS. |
| ENTRYWAY_HIGHPORT_SERVICE_AUD | string | did:web:highport.space#bard_control | No | Audience of the service-auth token sent to Highport. |
| ENTRYWAY_HIGHPORT_ON_FAILURE | refuse or defer | refuse | No | refuse fails a sign-up or rename when Highport cannot answer. defer continues and reserves later. |
| ENTRYWAY_HIGHPORT_TIMEOUT_MS | integer | 10000 | No | Highport call timeout in milliseconds. |
| ENTRYWAY_HIGHPORT_DRY_RUN | bool | false | No | Log Highport writes instead of sending them. |
| ENTRYWAY_HIGHPORT_SEND_PROOF | bool | true | No | Send the signed genesis with a sign-up’s reservation. |
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
| ENTRYWAY_RECONCILE_INTERVAL | duration | 1h | No | How often serve reconciles and repairs. 0 turns it off. |
| ENTRYWAY_METRICS_BIND | address | unset | No | Where serve answers GET /metrics, on its own listener. Needs the metrics feature. |
| ENTRYWAY_ADMIN_URL | URL | unset | No | For operator commands: call this entryway’s admin API at its public URL instead of opening the database. |
| ENTRYWAY_ADMIN_PASSWORD | string | unset | No | Operator 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.
Freeze changes. Close sign-ups on the PDS and ask account holders not to change passwords, email addresses, or handles during the window.
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.Open the adoption window. Set
PDS_ENTRYWAY_ADOPTION=trueon the PDS and restart it. This turns on a route that hands out every password hash on the server, so keep the window short.-
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 -
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 -
Update each DID’s PLC rotation keys so the entryway’s rotation key and your recovery key are listed. Run without
--applyfirst 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 Switch the PDS to entryway mode. Set the
PDS_ENTRYWAY_*settings fromentryway keys showand the peer secret, removePDS_ENTRYWAY_ADOPTIONand anyPDS_HIGHPORT_*settings, and restart the PDS.Check the result. Every account can sign in at the entryway,
getSessionworks through the PDS, handles resolve, andentryway reconcilereports no drift. Then restoreENTRYWAY_RECONCILE_INTERVALand 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.