Documentation
Integrating loggedin.fyi
Use loggedin.fyi to fetch the Atmosphere accounts a user has recently used in their browser, from other Atmosphere apps participating with loggedin.fyi, and allow the user to quick-start the same OAuth login your app already implements. This is not single sign-on, and it is not a new identity provider.
loggedin.fyi stores a short-lived, record that a DID was used on a participating AT Proto app. Your AT Proto app can call to loggedin.fyi to retrieve that list. Then you can choose how to display that list of DIDs, and offer them as login hints to the user, showing them which accounts they have already logged into, across the Atmosphere (of apps that are participating with loggedin.fyi). Selecting an account does not create a session with loggedin.fyi. Your AT Proto app still must complete a normal AT Proto OAuth flow with the user’s PDS.
In a browser, the session key is an HttpOnly cookie on the loggedin.fyi origin (loggedin_session, seven-day TTL). Cross-site embeds use Secure; SameSite=None. A grant of storage access is scoped to the pair (your AT Proto app, loggedin.fyi). Native iOS and Android clients reach that same cookie through the system browser once, then carry a signed X-Loggedin-Token that names the same session.
Getting Started
To add loggedin.fyi to your client, complete the following steps:
- Publish your existing OAuth client-metadata.json document at
https://your-app.example/oauth-client-metadata.jsonon your AT Proto app origin. If you have an AT Proto app already, you likely have this document; place it at that conventional path (or ensure it is reachable there). - Load the loggedin.fyi JavaScript interface and embed
/embedwith thestorage-accesspermission. - Render the returned accounts and, when the user selects one, start your existing OAuth login with that DID.
- After sign-in, update loggedin.fyi with the DID of the active user, so other participating apps on this device can offer it. On logout, remove the hint.
- Optionally, you can also periodically call loggedin.fyi to confirm that the user is still logged in to your app, and update the list of accounts on loggedin.fyi.
- On iOS or Android, use the native SDK instead of the embed. Those clients bootstrap through the browser’s shared cookie jar once, then send
X-Loggedin-Token.
The sections below expand each step. Replace https://your-app.example with your production origin, and set LOGGEDIN_ORIGIN to the loggedin.fyi host you are targeting.
Publishing Client Metadata
Any HTTPS origin that publishes valid OAuth client metadata at /oauth-client-metadata.json may participate. There is no static allowlist and no deploy gate. loggedin.fyi fetches that document when your app embeds, enrolls, saves, or heartbeats.
Serve the document at that URL with an application/json content type and status 200 without a redirect. loggedin.fyi requires all of the following:
client_idin the document equalshttps://your-app.example/oauth-client-metadata.jsonexactly.- If
client_uriis present, its origin matches your app origin. - At least one
redirect_urisentry uses that same origin. HTTPS is required except on localhost.
When validation succeeds, client_name and logo_uri appear as labels in the chooser and when clients call GET /origins. Localhost remains available even if metadata cannot be fetched, so you can develop against a self-signed certificate.
Embedding the Interface
Load the JavaScript interface from /example-renderer.js. The script defines window.Loggedin. Attach the message listener before you set the iframe src, and include the storage-access permission on the iframe.
Loggedin.mountEmbed (or prepareEmbed plus listenToEmbed before assigning src). If you set src first, the embed can post accounts before your listener exists.",
)}
Displaying Hinted Accounts
When storage access is available, the embed posts { type: "accounts", accounts }. Each account is a DID and the origins that loggedin.fyi has recorded for it:
Use Loggedin.renderAccountList to draw the list, or render it yourself. Fetch GET /origins?origin=… for the origins that appear on those accounts if you want human-readable names.
If the embed cannot use the cookie silently, it shows a Continue control and may post needs-access or popup. In the popup path, listen for picked and start OAuth with that DID.
Starting a Normal OAuth Login
Treat a selected DID as a login hint for the OAuth flow you already ship. Do not exchange anything with loggedin.fyi for a session.
${listing( "Listing 5", `function startOauth(identifier) { const url = new URL("/login", location.origin); url.searchParams.set("handle", identifier); location.href = url.toString(); }`, )} ${callout( "note", "Note", "Retrieved accounts are only a suggestion for the end user. The user still authenticates with their PDS. If they decline the retrieved accounts, your app should still offer a handle field as you would without loggedin.fyi.", )}Heartbeating a Signed-In Session
After the person is signed in, your app may choose to periodically send that user's DID to loggedin.fyi so other participating apps on this device can offer it. This heartbeat occurs when the person is actively using your client — for example, on load, or after an intentional action. loggedin.fyi stores one DID and origin pair with a last-seen timestamp. Records expire after seven days.
${listing( "Listing 6", `Loggedin.heartbeat(iframe, session.did, LOGGEDIN_ORIGIN);`, )}On Chrome, this posts { type: "heartbeat", did } to the embed, which calls POST /xrpc/fyi.loggedin.account.loggedIn. If the embed is in popup mode, heartbeat opens a first-party /save window instead. The origin query parameter must be your app, not loggedin.fyi.
You may also send people through Loggedin.enrollUrl after login if you want an explicit Save / Not now step on the loggedin.fyi origin.
Removing a Hint on Logout
When the person signs out of your app, remove the hint for that DID so other clients stop offering it.
${listing( "Listing 8", `Loggedin.logout(iframe, session.did, LOGGEDIN_ORIGIN);`, )}This posts { type: "logout", did } to the embed, or opens /save?action=logout when storage access is unavailable.
iOS and Android SDKs
A native app cannot hold the loggedin.fyi cookie, and neither platform will give you a device identifier to use instead — Apple’s Developer Program License Agreement forbids deriving one, and Android restricts hardware ids to privileged callers. Both platforms do, however, expose the browser’s shared cookie jar: ASWebAuthenticationSession on iOS and Custom Tabs on Android. That is the same loggedin_session cookie mobile Safari or Chrome already holds, so a native client and the web share one device session.
The SDKs make one visible trip through that sheet. The page is first-party on loggedin.fyi, so the cookie is attached; in exchange the worker mints a signed token carrying the same session id. Every call after that is a plain HTTPS request with X-Loggedin-Token and no browser.
The callback you pass to the SDK must appear verbatim in redirect_uris. Anything else is refused with 403, so a token can only ever be handed to a URL you published.
Pass the signed-in DID to bootstrap to show a “remember this account?” confirmation instead of the chooser. A picked DID is a login hint: start the same OAuth flow you already ship.
Tokens last at most seven days, matching the hint TTL. A 401 means the token expired or the person cleared their browser data — discard it and bootstrap again. The packages live in sdks/ios and sdks/android.
Storage Access Across Browsers
The embed and its XRPC calls use the unpartitioned loggedin_session cookie. Parent iframes must set allow="storage-access".
- Chrome can activate storage access with Storage Access Headers and a silent embed.
- Safari and Firefox use
document.requestStorageAccess(). If that fails, the embed opens a first-party/chooseor/savewindow on loggedin.fyi.
Do not call hasStorageAccess() inside the Continue click handler. That call can consume the user gesture. The embed requests storage access first; only then does it fall back to a popup.
A hidden iframe can reuse an existing grant on that site. It cannot collect a first grant. Show a visible Continue control when the embed posts needs-access.
Rate Limits
Open participation is gated by client metadata and edge rate limits, not a static list. Expect 429 with RateLimitExceeded when a client IP, app origin, or metadata fetch exceeds its budget. Heartbeats for the same DID and origin are coalesced for two minutes inside the session store. Each browser session keeps a bounded number of DID and origin pairs.
Endpoints
XRPC methods are unauthenticated. The cookie is the browser session key. Native SDKs send X-Loggedin-Token instead and never receive a session cookie; their writes are pinned to the origin inside the token. Write calls from the embed, enroll, and save pages also send the parent origin.
| Lexicon / page | HTTP |
|---|---|
fyi.loggedin.account.loggedIn | POST /xrpc/fyi.loggedin.account.loggedIn |
fyi.loggedin.account.logout | POST /xrpc/fyi.loggedin.account.logout |
fyi.loggedin.account.list | GET /xrpc/fyi.loggedin.account.list |
| JavaScript interface | GET /example-renderer.js |
| Embed | GET /embed |
| Origin labels | GET /origins?origin=… |
| First-party save | GET /save?did=&origin=&action=save|logout |
| Enroll | GET /enroll?did=&return= |
| Chooser | GET /choose?parent= |
| Native bootstrap | GET /app/start?origin=&callback=&state= |
| Native session | X-Loggedin-Token request header |
GET /xrpc/fyi.loggedin.account.list returns { accounts: [{ did, origins }] }. See the Lightbox and Cratewalk demos for two complete clients that demonstrate how the hinted DIDs are used across different apps.