Releases: hypercerts-org/ePDS
ePDS@0.6.3
Who should read this release
Patch Changes
-
#169
cc65707Thanks @Kzoeps! - The final sign-in screen now lines up its message card with the page title.Affects: End users
End users: After approving an app sign-in, the "Login complete" screen now keeps the "You are being redirected..." card visually aligned with the title instead of sitting slightly lower on the page.
ePDS@0.6.2
Who should read this release
- End users:
Patch Changes
-
#154
2e4d327Thanks @aspiers! - Sign-in pages no longer strand users on a "session expired" dead end, and Resend no longer offers codes that won't work.Affects: End users
End users: if your sign-in times out (you closed the tab and came back, or your wait was longer than the page can keep alive in the background), you are now taken back to the app you were signing in to so it can offer you a retry. The page also no longer offers Resend in the rare case where the new code wouldn't work — instead it tells you the sign-in has timed out and gives you a Start over button. No more typing a fresh code that fails. If for some reason the automatic return is not possible, the page shows a "Return to sign in" button so you can get back to the app yourself in one click.
-
#154
b1fc940Thanks @aspiers! - Slow sign-ins are less likely to time out before you finish entering your code.Affects: End users
End users: if you take a few minutes to find your sign-in code in your inbox before entering it, you will no longer be bounced to a "session expired" page when you submit it. Closing the tab or walking away for a long stretch can still expire the flow, in which case the existing error pages still apply — but reading email at human speed should not.
ePDS@0.6.1
Who should read this release
- End users:
- "Powered by Certified" footer now appears on every auth-service page.
- "Use a different account" on the chooser now reliably takes you to the email form, not the code step for the previous account.
- Sign-in no longer fails with "Authentication session expired" when an OTP code is resent after the original code times out.
- Terms of Use and Privacy Policy links on the sign-in page now open in a new tab.
- OAuth consent buttons stack cleanly on small screens.
- A smoother sign-in code experience: no false error flash on a successful sign-in, no rapid-fire failures when correcting a wrong code, and tidier-looking banners.
- Sign-in no longer fails with a raw JSON error page when a user takes too long on the OTP step.
- Sign-in no longer hits a dead-end on the password form
- Client app developers:
Patch Changes
-
#130
6a8671dThanks @s-adamantine! - "Powered by Certified" footer now appears on every auth-service page.Affects: End users
End users: every page rendered by the auth service now displays the same "Powered by Certified" footer that the main sign-in already shows, so the branding is consistent end-to-end. New surfaces covered: the Account Settings sign-in flow at
/account/login(email-entry and code-entry steps), the "Choose your handle" page shown to new users after email verification, both Account Recovery steps (backup-email entry and recovery-code entry), the/accountsettings dashboard, the backup-email verification confirmation, the post-deletion confirmation, and the generic error pages used by 404 / 500 / session-expired flows. -
#141
899346cThanks @aspiers! - "Use a different account" on the chooser now reliably takes you to the email form, not the code step for the previous account.Affects: End users
End users: when you click "Another account" on the account chooser to sign in as someone else, you now always land on a fresh email entry form. Previously, if the app that started the sign-in had pre-filled an account hint, the page jumped straight to the verification-code step for the previous account — leaving you stuck typing a code for an account you were trying to leave.
-
#122
dacf1d2Thanks @aspiers! - Sign-in no longer fails with "Authentication session expired" when an OTP code is resent after the original code times out.Affects: End users
End users: Previously, if you took longer than 10 minutes to enter the one-time code emailed to you and then clicked Resend code, the new code would verify, but the next page would say "Authentication session expired. Please try again." and you would have to start the whole sign-in over. The OAuth session that was tracking your sign-in had the same 10-minute lifetime as the OTP code itself, so it had already gone away by the time the new code arrived.
The OAuth session now lives long enough to outlast a typical resend cycle, so a slow first attempt followed by Resend completes normally. The OTP code's own 10-minute lifetime is unchanged.
-
#127
8bf888bThanks @s-adamantine! - Terms of Use and Privacy Policy links on the sign-in page now open in a new tab.Affects: End users
End users: clicking Terms of Use or Privacy Policy on the sign-in page no longer navigates away from the in-progress sign-in. The links open in a new tab instead, so you can read the legal page and come back to finish signing in without restarting.
-
#136
143ff35Thanks @Kzoeps! - OAuth consent buttons stack cleanly on small screens.Affects: End users
End users: On phones and narrow browser windows, the consent screen now places the approve and deny buttons on separate lines so they are easier to read and tap. Larger screens keep the existing button layout.
-
#134
bce65b5Thanks @s-adamantine! - A smoother sign-in code experience: no false error flash on a successful sign-in, no rapid-fire failures when correcting a wrong code, and tidier-looking banners.Affects: End users, Client app developers
End users:
- A successful sign-in no longer briefly shows a red "Invalid OTP" message on its way to signing you in.
- After entering a wrong code, the boxes clear and focus jumps back to the first one, so retyping doesn't immediately resubmit the still-wrong code on every keystroke (which previously could lock you out for spamming the server).
- The red "Invalid OTP" and green "Code resent" banners are centred inside their coloured container instead of sitting in the corner of an empty wide box.
Client app developers: the sign-in page's flash-message container now uses a stable
flash-msgbase class witherror/successmodifier classes, so custom client CSS can restyle either variant cleanly via.flash-msg,.flash-msg.error, and.flash-msg.success. -
#128
0e62bd6Thanks @aspiers! - Sign-in no longer fails with a raw JSON error page when a user takes too long on the OTP step.Affects: End users
End users: Previously, if you took more than five minutes between requesting your one-time code and submitting it (a slow inbox, switching tabs, fishing the code out of spam, multiple Resend cycles), sign-in could fail with a blank page showing only
{"error": "Authentication failed"}on the PDS host — even though your OTP code itself was still valid. You now either land back inside the app you were signing into (which can offer a one-click retry), or see a styled error page on the PDS host explaining that sign-in timed out — depending on how far through the flow the timeout is detected. Either way, no more raw JSON. -
#129
14e5033Thanks @aspiers! - Sign-in no longer hits a dead-end on the password formAffects: End users
End users: if you saw a "handle and password" form during sign-in with no way to enter a code, that path is gone. The email-code form will be shown instead, and after entering the code you'll be signed in normally.
ePDS@0.6.0
Who should read this release
- End users:
- Auth-service login page can now offer ATProto/Bluesky handle sign-in alongside email OTP.
- Trusted apps can now show their own icon in the browser tab on the sign-in page.
- Refreshed sign-in page design, with new ways for apps to style it.
- Account settings page now shows your current handle.
- Sign-in, account, error, and OAuth-consent pages now show an icon in the browser tab, with separate assets for light and dark browser themes.
- Signing in once in your browser now works across all apps that use this ePDS.
- Account recovery via backup email now completes the OAuth flow instead of dropping users into signup.
- Visiting the bare auth service URL now takes you to the account page instead of a blank 404.
- Error pages on the sign-in service now match the rest of the signup and login look instead of showing plain default text, and apps calling the sign-in service now receive structured error responses by default instead of HTML pages.
- Client app developers:
- Auth-service login page can now offer ATProto/Bluesky handle sign-in alongside email OTP.
- Preview ePDS's auth-service screens and emails directly in your browser, without walking through the OAuth flow.
- Trusted apps can now show their own icon in the browser tab on the sign-in page.
- Trusted demo client now ships with a custom branded OTP email template.
- Refreshed sign-in page design, with new ways for apps to style it.
- Signing in once in your browser now works across all apps that use this ePDS.
- Security fix: client-supplied email templates now require the client to be on the trusted-clients list.
- Error pages on the sign-in service now match the rest of the signup and login look instead of showing plain default text, and apps calling the sign-in service now receive structured error responses by default instead of HTML pages.
- Operators:
- Auth-service login page can now offer ATProto/Bluesky handle sign-in alongside email OTP.
- Preview ePDS's auth-service screens and emails directly in your browser, without walking through the OAuth flow.
- Trusted apps can now show their own icon in the browser tab on the sign-in page.
- Trusted demo client now ships with a custom branded OTP email template.
- Refreshed sign-in page design, with new ways for apps to style it.
- Sign-in, account, error, and OAuth-consent pages now show an icon in the browser tab, with separate assets for light and dark browser themes.
- Signing in once in your browser now works across all apps that use this ePDS.
- Fix a pds-core crash on the account chooser (
/account) caused by response-rewrite middleware running after upstream had already flushed headers. - Security fix: client-supplied email templates now require the client to be on the trusted-clients list.
- Auth-service rate limiter can now be disabled for single-source-IP test environments.
- Account recovery via backup email now completes the OAuth flow instead of dropping users into signup.
- Visiting the bare auth service URL now takes you to the account page instead of a blank 404.
Minor Changes
-
#115
7f265b7Thanks @aspiers! - Auth-service login page can now offer ATProto/Bluesky handle sign-in alongside email OTP.
Affects: End users, Client app developers, Operators
End users:
- When the app you came from supports it, the sign-in page now shows an "Or sign in with ATProto/Bluesky" button under the email form.
- Clicking the button switches the form into handle-entry mode (e.g.
you.bsky.social). Submitting a handle takes you back to your own PDS to finish signing in there. - Clicking the button again returns you to the email form.
Client app developers: opt in by adding
epds_handle_login_urlto your OAuth client metadata.- The value must be an absolute https:// URL on your client's own origin. ePDS auth-service redirects the browser to that URL with
?handle=<value>appended when the user submits a handle. - Your route is responsible for resolving the handle to its PDS and starting a fresh OAuth flow against that PDS — auth-service is bound to one PDS and cannot start a PAR on your client's behalf, so off-PDS handles only work via this hand-off.
- The reference demo client opts in by exposing
${baseUrl}/api/oauth/login?handle=..., which already accepts ahandlequery parameter and resolves it dynamically. - If you do not declare
epds_handle_login_url, the button is not rendered. Existing clients see no behaviour change.
Operators: no new required configuration. The button only renders for OAuth clients that explicitly opt in via their metadata.
-
#103
226781b/ #93d363b3dThanks @aspiers! - Preview ePDS's auth-service screens and emails directly in your browser, without walking through the OAuth flow.
Affects: Client app developers, Operators
Client app developers:
A new preview route on pds-core renders the account chooser with fixture sessions and your branding CSS, alongside the existing
/preview/consentroute. Open/preview/chooser(linked from the/previewindex) to see how a returning user with one or more bound accounts will see your client. Inline controls on the index let you tweak the preview without editing the URL: a number field for?numAccounts=N(clamped to 1–10) grows or shrinks the fixture account list, and a dropdown for?epds_handle_mode=overrides the handle-picker mode the same way a real OAuth request can. The dropdown defaults to "Auto", which omits the param so client metadata (or the operator's env default) wins — exactly the production resolver order. The same?client_id=<URL-of-your-client-metadata.json>param the other preview routes accept also injects your branding CSS, subject to the standard trusted-clients gate. The existing/preview/choose-handlelink on the auth-service index gains the same?epds_handle_mode=and?error=dropdowns and collapses the four enumerated handle-mode entries into a single link with bound controls.Three new preview routes on the auth service render the exact email HTML real users receive, inside a sandboxed iframe:
/preview/emails/new-user— welcome / email-verification code sent during signup./preview/emails/returning-user— sign-in OTP sent when an existing user logs in to your app./preview/emails/recovery— backup-email verification link sent when a user adds a recovery address.
Each route accepts the same
?client_id=<URL-of-your-client-metadata.json>query param as the other preview pages, so you can see how your branded template will look without walking through a real OAuth flow. Optional extras:?otp=<code>to override the fixture OTP,?app=<name>to override the fixture app name on the returning-user template,?verify_url=<url>to override the bac...
ePDS@0.5.0
Who should read this release
- Client app developers & operators:
- End users of the trusted demo:
Minor Changes
-
#84
fe3ec90Thanks @aspiers! - Add preview routes on auth-service and pds-core for iterating on client branding CSS.Affects: Client app developers, Operators
Client app developers:
- Visit
/previewon either auth-service or pds-core for an index of every preview page. Each page renders against fixture data, so you can iterate on yourbranding.csswithout walking through a real OAuth flow. - Paste your
client-metadata.jsonURL into the input field on the index page. The value is persisted in your browser and wires up every preview link, subject to the samePDS_OAUTH_TRUSTED_CLIENTScheck as a real flow. Leave it blank to see the unbranded baseline. - The workflow becomes: edit
branding.css, refresh any preview page. No OTP emails, no full flow. - The demo app links directly to the auth-service preview index with its own
client_idpre-selected.
Operators:
- Two new env vars gate the preview routes, one per service:
AUTH_PREVIEW_ROUTES=1on auth-service,PDS_PREVIEW_ROUTES=1on pds-core. Both are independent. - Safe to enable on preview deployments (Railway PR previews,
pr-base, dev) and on local development instances. Preview routes don't affect real auth flows — they short-circuit real state — so they can technically run in production too, but they are a developer-only surface and are best left off outside preview/dev envs. - Privacy: enabling previews exposes
/preview/cache-status, which returns the list ofclient_idURLs currently in the shared client-metadata cache — i.e. apps that have recently started an OAuth flow against this PDS. That partially leaks which third-party clients are using the instance, so keep previews disabled in production unless you're comfortable with that. - See
packages/auth-service/.env.exampleandpackages/pds-core/.env.examplefor the full notes.
- Visit
Patch Changes
-
#83
cc722c4Thanks @aspiers! - Demo amber/ocean themes now colour the OAuth consent page correctly.Affects: End users of the trusted demo
End users: The consent screen shown after signing in via the trusted demo now uses the demo's own warm indigo / amber palette throughout — the Authorize and Deny-access buttons, the "Authorize" header strip, and the surrounding surface all match the theme instead of falling back to the default @atproto/oauth-provider dark-mode look.
The previous CSS targeted auth-service's hand-rolled login markup (
.btn-primary,.container,.field), which does not exist on the consent page — that page is built from@atproto/oauth-provider-ui, which is a Tailwind-utility bundle whose colours are driven by CSS custom properties (--branding-color-primaryand friends). The demo theme now overrides those variables at:root, so a single declaration recolours everybg-primary/text-primary/border-primaryutility on the consent page at once, and additionally paints the card surface and body background to match. -
#89
1942ebbThanks @aspiers! - Fix two preview-route cache bugs and remove long-stale debug endpoints.Affects: Client app developers, Operators
Client app developers:
- Preview-route fetch failures no longer poison the shared client-metadata cache. Previously, a failed preview fetch for a
client_idwith a valid 10-minute entry would overwrite that entry with a 60-second branding-less fallback, silently droppingbranding.csson real OAuth flows for up to a minute. The in-memory cache is now only written by real-flow resolution. - The auth-service HTML preview pages (
/preview/login,/preview/login-otp,/preview/choose-handle,/preview/choose-handle-picker,/preview/recovery,/preview/recovery-otp, and the/previewindex) now sendCache-Control: no-store. Without it, a browser refresh could serve a cached page and never ask the server for freshbranding.css, breaking the advertised "editbranding.css, refresh the preview page" workflow. /preview/validatenow flagsbranding.csswhose escaped size exceeds the 32 KB injection limit as an error, instead of reportingokand letting the developer discover later that their CSS was silently dropped on real OAuth flows. Byte counts now matchgetClientCss()'s measurement (escaped UTF-8).
Operators:
- Removed
/_internal/debug-grantsand/_internal/debug-recent-accounts. These were added as temporary HYPER-270 debugging endpoints with a code comment marking them for removal before PR #21 shipped (v0.2.2); they survived through v0.2.2, v0.3.0, v0.4.0, and the pending v0.5.0. The matching env varEPDS_DEBUG_GRANTSis no longer read.
- Preview-route fetch failures no longer poison the shared client-metadata cache. Previously, a failed preview fetch for a
ePDS@0.4.0
Who should read this release
- End users:
- Client app developers:
- Operators:
Minor Changes
-
#48
0c275e4Thanks @Kzoeps & @aspiers! - Trusted apps can now style the sign-in and consent pages to match their own brand.Affects: End users, Client app developers, Operators
End users: When signing in through an app that your ePDS operator has approved for branding, the login page, code entry page, handle picker, account recovery page, and consent page will display that app's colour scheme instead of the default look. The pages still work exactly the same way — only the visual appearance changes.
Client app developers: Add a
branding.cssfield inside abrandingobject in yourclient-metadata.json. The CSS is injected as a<style>tag into every auth-service page and the PDS stock consent page (/oauth/authorize) when yourclient_idis listed in the operator'sPDS_OAUTH_TRUSTED_CLIENTS. The CSS is size-capped at 32 KB (measured in escaped UTF-8 bytes) and sanitised to prevent</style>tag closure. The CSPstyle-srcdirective is updated with a SHA-256 hash of the injected CSS. Example metadata:{ "client_id": "https://app.example/client-metadata.json", "client_name": "My App", "branding": { "css": "body { background: #0f1b2d; color: #e2e8f0; } .btn-primary { background: #3b82f6; }" } }Untrusted clients (not in
PDS_OAUTH_TRUSTED_CLIENTS) never get CSS injection, regardless of what their metadata contains.Operators: CSS branding injection is controlled by the existing
PDS_OAUTH_TRUSTED_CLIENTSenv var on pds-core. No new env vars are required on pds-core or auth-service. The auth-service reads the samePDS_OAUTH_TRUSTED_CLIENTSlist to decide whether to inject CSS on its pages (login, OTP, choose-handle, recovery). Seedocs/configuration.mdfor the full reference.For the demo app, a new optional
EPDS_CLIENT_THEMEenv var selects a named theme preset (e.g.ocean) that applies consistent styling to both the demo's own pages and the CSS served in its client metadata. When unset, the demo uses the default light theme with no branding CSS. Seepackages/demo/.env.examplefor details.
Patch Changes
-
#77
b3c779aThanks @aspiers! - Generate ES256 keypairs withpnpm jwk:generateinstead of re-running full setup.Affects: Client app developers
Client app developers: A new
pnpm jwk:generatecommand outputs a compact ES256 private JWK (with auto-derivedkid) on stdout. Use this when you need a keypair forprivate_key_jwtclient authentication without running the fullscripts/setup.sh. The output is suitable for theEPDS_CLIENT_PRIVATE_JWKenvironment variable (used by the bundled demo app inpackages/demo, not by third-party client apps) or for embedding the public half in any client metadata'sjwksfield. -
#77
0eaded0Thanks @aspiers! - Updated login integration docs to recommend@atproto/oauth-client-nodeand confidential clients.Affects: Client app developers
Client app developers: The tutorial and skill reference now recommend
@atproto/oauth-client-node'sNodeOAuthClientfor Flow 2 (no hint, handle, or DID input), which handles PAR, PKCE, DPoP, and token exchange automatically. Flow 1 (emaillogin_hint) remains hand-rolled. The default client metadata example has been flipped from"token_endpoint_auth_method": "none"to"private_key_jwt"withjwks_urior inlinejwksfor publishing the public key. A new "Confidential vs public clients" section explains the trade-offs — notably that public clients force a consent screen on every login. New sections cover JWKS key generation, publishing, and rotation.
ePDS@0.3.0
Who should read this release
- Client app developers and Operators:
Minor Changes
-
#74
b46273aThanks @aspiers! - The health endpoint now reports the running ePDS version.Affects: Client app developers, Operators
Client app developers: both
/healthendpoints (pds-core and auth-service) now include aversionfield in their JSON response (e.g.{ "status": "ok", "service": "epds", "version": "0.2.2+f37823ee" }). You can use this to check which ePDS release your app is running against. The demo frontend also displays the version in its page footer.Operators: in Docker and Railway deployments the version is automatically set to
<package.json version>+<8-char commit SHA>at build time. In local dev it falls back to the rootpackage.jsonversion (e.g.0.2.2). To override, set theEPDS_VERSIONenvironment variable on both pds-core and auth-service to any string. Docker Compose users should now build withpnpm docker:buildinstead ofdocker compose builddirectly — the wrapper stamps the version before building, and the build will fail if the version stamp is missing.
Patch Changes
-
#76
f709066Thanks @aspiers! - The upstream PDS version now appears on the stock health endpoint.Affects: Client app developers, Operators
/xrpc/_healthnow returns the upstream@atproto/pdsversion in its JSON response (e.g.{ "version": "0.4.211" }). Previously this endpoint returned{}. This is independent of the ePDS version reported by/health.Operators: no configuration is needed — the version is read from the installed
@atproto/pdspackage at startup. To override, set thePDS_VERSIONenvironment variable on pds-core.
ePDS@0.2.2
Who should read this release
- Everyone (end users, client app developers, operators):
- Operators also:
Patch Changes
-
#21
10287caThanks @aspiers! - The permissions shown on the sign-in consent screen now match what the app actually asked for.Affects: End users, Client app developers, Operators
End users: When you sign in to a third-party app through ePDS and are asked to approve what the app can do with your account, the list you see now reflects the permissions that particular app actually requested. Previously the screen always showed the same hard-coded list ("Read and write posts", "Access your profile", "Manage your follows") no matter which app you were signing in to, which was misleading. The consent screen itself also now looks and behaves like the standard AT Protocol consent screen used elsewhere in the ecosystem.
Client app developers: The consent screen rendered at
/oauth/authorizeis now the stock@atproto/oauth-providerconsent-view.tsx, driven by the realscope/permissionSetsyour client requests. The previous auth-service implementation ignored the requested scopes entirely. After OTP verification and (for new users) account creation,epds-callbacknow binds the device session viaupsertDeviceAccount()and redirects through/oauth/authorize, so the upstreamoauthMiddlewarerunsprovider.authorize()— includingcheckConsentRequired()— against the actual request. Clients that only need scopes the user has already approved will now be auto-approved instead of being shown a redundant consent screen. Support for branding the consent screen is currently being worked on.Operators: No configuration changes are required. Consent state now lives in the upstream provider's
authorizedClientstracking. Theclient_loginstable is no longer used but is left in place (not dropped) to avoid breaking rollbacks in case they were ever needed. -
#21
5110845Thanks @aspiers! - Trusted apps can optionally skip the consent screen when new users sign up.Affects: End users, Client app developers, Operators
End users: When you create a new account through a trusted app, ePDS can now send you straight back to that app without showing a separate consent screen first.
Client app developers: To opt in, your client metadata must include
epds_skip_consent_on_signup: true. The skip only applies on initial sign-up, only for trusted clients, and only when the server is configured to allow it.Operators: This feature has separate configuration from the normal consent-screen changes. To enable it, set
PDS_SIGNUP_ALLOW_CONSENT_SKIP=trueon pds-core. The skip only applies to clients already trusted viaPDS_OAUTH_TRUSTED_CLIENTS(also on pds-core) and only when the client metadata opts in withepds_skip_consent_on_signup: true. -
#65
313c071Thanks @aspiers! - Sign-in no longer fails when the login service and your data server share a domain name.Affects: Operators
Operators: Fix for an unreleased bug introduced by the above consent changes in #21. No configuration changes are needed. This is just a heads-up in case anyone deployed an ePDS from git within a small window; if you notice logins failing on your ePDS, make sure to upgrade to v0.2.2 or newer.
Technical details:
The upstream
@atproto/oauth-providerrejectssec-fetch-site: same-siteonGET /oauth/authorize. This caused a400 Forbidden sec-fetch-site headererror on deployments where the auth service and PDS share a registrable domain (e.g.auth.epds1.test.certified.appandepds1.test.certified.app). Browsers sendsame-siteon the 303 redirect chain from the auth subdomain to the PDS, and the upstream code does not allow it.pds-core now includes middleware that rewrites
sec-fetch-site: same-sitetosame-originonGET /oauth/authorizewhen the request originates from the trusted auth subdomain.Additionally, DB migration v9 (which previously dropped the
client_loginstable) is now a no-op. The table is no longer used but is kept in place to avoid breaking emergency rollbacks to older code that still references it.This bug was missed by the comprehensive E2E test suite due to an unfortunate combination of quirks:
- The upstream ATProto PDS does not support
sec-fetch-site: same-site, marked as a@TODOin the source. Stock ATProto never encounterssame-sitebecause the PDS serves its own login UI on the same origin. - Railway does not allow any control over generated domains for PR preview environments. Each service gets a flat
*.up.railway.appsubdomain, andup.railway.appis on the Public Suffix List — so cross-service requests arecross-site(allowed), neversame-site. This creates a small but ultimately significant difference in DNS topology from Certified infrastructure where all services share a registrable domain. - The deliberate introduction (in PR #21) of a double redirect from
auth-service/auth/completetopds-core/oauth/epds-callbacktopds-core/oauth/authorize, which sends the browser through a cross-origin hop on the same site — the exact pattern the upstream validation rejects.
- The upstream ATProto PDS does not support
epds@0.2.1
Who should read this release
Patch Changes
-
#51
cfaeabeThanks @aspiers! - Mask every segment of email addresses on recovery and account-login pages, including the domain and TLD (HYPER-259).Affects: End users
Previously, the partially-masked email shown on the recovery and account-login pages left the entire domain visible (e.g.
jo***@gmail.com), making the user's email address much easier to guess for anyone who entered a known handle on the login flow. Each dot-separated segment of both local part and domain is now masked independently, revealing only the final character of each segment (e.g.persons.address@gmail.com→***s.***s@***l.***m). Hiding the domain is important: leaving a common domain visible would make popular providers trivially identifiable, which in turn makes the local part much more guessable.
epds@0.2.0
Who should read this release
- End users:
- Client app developers:
- Operators:
- Configurable sign-in code length, optionally mixing letters and numbers.
- Choose your own handle when signing up, instead of being given a random one.
- Fail-fast validation of internal environment variables on the auth service.
- Honour the generic
PORTenvironment variable on both services, so Railway's automatic healthcheck succeeds without per-service configuration.
Minor Changes
-
#14 Thanks @Kzoeps! - Configurable sign-in code length, optionally mixing letters and numbers.
Affects: End users, Operators
End users: depending on how the ePDS instance you sign in to is configured, sign-in codes sent to your email may now be shorter (as few as 4 characters) or longer (up to 12 characters) than the previous fixed length of 8, and may include uppercase letters as well as digits. Codes of 8 or more characters are displayed grouped in the email for readability (e.g.
1234 5678), but you can still paste the whole code into the sign-in form as usual — the space is just a visual aid.Operators: two new environment variables on the auth service —
OTP_LENGTH(integer, range 4–12, default 8) andOTP_CHARSET(numeric(default) oralphanumeric;alphanumericuses uppercase A–Z plus 0–9). Values outside the range cause the service to fail on startup. The OTP form fields (input width,pattern,inputmode,autocapitalize) adapt automatically from the configured length and charset; no template changes are required.Operators running custom email templates: the shared email helpers now format OTPs with visual grouping when the code is 8 characters or longer — e.g.
1234 5678in subject lines and plain text, and<span>1234</span><span>5678</span>with CSS spacing in HTML so that copy-paste still yields the flat code. If you render OTPs yourself rather than going throughEmailSender.sendOtpCode(), importformatOtpPlain()andformatOtpHtmlGrouped()from@certified-app/sharedinstead of interpolating the raw code. -
#13 #29 #33 #36 Thanks @Kzoeps & @aspiers! - Choose your own handle when signing up, instead of being given a random one.
Affects: End users, Client app developers, Operators
End users: the signup flow now shows a handle picker by default instead of assigning a random handle. You can type a custom handle and the picker will check availability as you type, or click the random-handle button to take what the old flow would have given you. The picker now accepts handles as short as 5 characters and handles are validated more strictly so that some handles that used to be accepted may now be rejected up-front with a clearer error. The picker layout has been widened to accommodate long PDS domain names without truncation.
Client app developers (building on top of ePDS): a new
epds_handle_modesetting controls which variant of the signup handle picker is shown. Accepted case-sensitive values:picker— always show the picker, no random option offered.random— always assign a random handle, no picker (the
pre-0.2.0 behaviour).picker-with-random(default) — show the picker but include a
"generate random" option.
The setting is resolved with the following precedence (first match wins), falling back to a built-in default:
epds_handle_modequery parameter on the/oauth/authorizerequest.epds_handle_modefield in the OAuth client metadata JSON served at the client'sclient_idURL.EPDS_DEFAULT_HANDLE_MODEenvironment variable on the auth service.- Built-in default:
picker-with-random.
This precedence was previously wrong — the env var was consulted before the client metadata, so clients could not override a server default. If you relied on that bug, your env var setting will now be overridden by whatever the client metadata says.
To force a specific handle mode for users of your app, add the field to the client metadata JSON that your
client_idURL returns, alongside the standard OAuth fields:{ "client_id": "https://example.com/oauth/client-metadata.json", "client_name": "Example", "redirect_uris": ["https://example.com/oauth/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "atproto transition:generic", "token_endpoint_auth_method": "none", "application_type": "web", "dpop_bound_access_tokens": true, "epds_handle_mode": "picker" }Unknown or invalid values are silently ignored and fall through to the next source. If you need to override per-request (e.g. for a specific signup campaign), append
?epds_handle_mode=pickerto your/oauth/authorizeURL.Operators: set
EPDS_DEFAULT_HANDLE_MODEon the auth service to change the default handle-picker variant for clients that don't specify one in their client metadata. Accepted values are the same as those listed in the Client app developers section above (picker,random,picker-with-random). See.env.examplefor documentation.
Patch Changes
-
#3 #6 Thanks @aspiers! - Sign in faster from third-party apps that already know who you are.
Affects: End users
When you sign in to a third-party AT Protocol app (anything built on top of the Bluesky account system, for example) that already knows your handle or DID, ePDS now jumps straight to the "enter your sign-in code" step. Previously you would have been asked to retype your email address first, even though the app you were using had already identified you — that extra step is gone.
This fixes two specific situations that didn't work before: apps that identified you by handle or DID rather than email, and apps that sent the identifier over a back channel rather than in the sign-in URL.
-
#20 #23 Thanks @aspiers! - Fail-fast validation of internal environment variables on the auth service.
Affects: Operators
A new
requireInternalEnv()helper runs at auth service startup and reports exactly which required internal variables are missing or malformed, replacing cryptic downstream errors likeTypeError: Failed to parse URLon the first request.Checks performed:
PDS_INTERNAL_URL— must be set and must begin withhttp://orhttps://(matched case-insensitively). Trailing slashes are stripped automatically.EPDS_INTERNAL_SECRET— must be set to any non-empty string.
If you previously set
PDS_INTERNAL_URLto a bare hostname likecore.railway.internalorcore:3000, the service will now refuse to start with this error:PDS_INTERNAL_URL is missing the http:// or https:// scheme: "core.railway.internal"Add the scheme and port explicitly. The canonical Docker Compose default (shown in
.env.example) ishttp://core:3000; for Railway's private networking the equivalent ishttp://<service>.railway.internal:<PDS_PORT>, substituting whichever service name you gave your pds-core deployment and thePDS_PORTyou configured on it. Railway's internal network uses plain HTTP on explicit ports, not HTTPS. This previously "worked" in the sense that the service started, but then failed on the first internal request; the new behaviour surfaces the misconfiguration immediately. -
#27 Thanks @aspiers! - Honour the generic
PORTenvironment variable on both services, so Railway's automatic healthcheck succeeds without per-service configuration.Affects: Operators
New port-resolution precedence (first set value wins):
- auth service:
AUTH_PORT→PORT→3001 - pds-core:
PDS_PORT→PORT→3000(pds-core readsPDS_PORT; whenPDS_PORTis unset,PORTis copied into it before@atproto/pdsreads its environment)
If you run ePDS on Docker Compose or another orchestrator where you set
AUTH_PORT/PDS_PORTexplicitly: no change — your existing settings take precedence overPORT.If you run ePDS on Railway (or any platform that injects
PORT... - auth service: