DOCUMENTATION · DEVELOPERS

Atom9 login for developers.

This area covers one thing: authentication with Atom9 login over standard OpenID Connect. Your app sends people to id.atom9.com and gets back signed proof of who they are. Quickstart, a command-line test, examples for ten stacks, and the rules — all on this page.

Integrating with an AI assistant? Give it the whole guide on one page: atom9.com/docs/add-atom9-login?view=full.

Quickstart

Everything from nothing to a working "Continue with Atom9" button: register your app, wire it up, and — the part most first integrations get slightly wrong — exactly what your app does when someone signs in, signs up, or signs out. Plus a terminal test before you write any code, an optional Atom9-managed-profile mode, and how your users manage or remove their own data.

1. What this is — and what it is not

a. What Atom9 login does. It answers exactly one question for your app: who is this person? Atom9 verifies the person (email, passkey, Google, Microsoft — whatever your app's policy allows) and hands your app a signed token containing a stable user id (sub), their email, and whether that email is verified. The protocol is standard OpenID Connect — the same one behind "Sign in with Google".

b. What it is not. It is not authorization (what a person may do in your app — that is yours) and not a session for your app (you mint your own after login). By default it is also not a user-profile store — your app owns its accounts and data — but you can opt into Atom9 managing the end-user profile instead (step 7).

c. What kind of app can use it. Server-side web applications — "confidential clients" that can keep a secret on a server. Single-page apps or mobile apps without a backend are not supported yet: the token exchange requires the client secret, which must never ship to a browser or device.

2. Add a sign-in app in the Atom9 admin

a. Sign in at admin.atom9.com and open Settings → Authentication → + Sign-in app.

b. Fill in the form: a name, the type (External app for anything that is not an Atom9 surface), and your redirect URLs — your callback, e.g. https://app.example.com/oidc/callback. Matching is exact, so register every environment's URL, including http://localhost:<port>/oidc/callback for development.

c. Copy the two credentials it gives you: the client_id (public) and the client_secret — shown once. Store the secret server-side (environment variable or secret manager). If you lose it, rotate from the same screen.

d. Optionally set the app's security level and allowed sign-in methods. Atom9 enforces those at login time; your app implements none of it.

3. Point your app at the discovery URL

a. Every OIDC library configures itself from one URL:

https://id.atom9.com/.well-known/openid-configuration

b. Give the library your three values — client_id, client_secret, redirect URL — usually via environment variables:

ATOM9_CLIENT_ID=a9-xxxxxxxxxxxx
ATOM9_CLIENT_SECRET=(the secret you copied)
ATOM9_REDIRECT_URI=https://app.example.com/oidc/callback

c. Pick your stack in Examples by stack — each one is a working configuration you can paste.

4. Your first login, end to end

a. Start your app and click your new login button. The browser goes to id.atom9.com.

b. Sign in with a fresh email. Atom9 walks the person through account creation (or straight in, if they already have an Atom9 account), shows your app's consent screen, and returns to your redirect URL with a one-time code.

c. Your library exchanges the code, validates the ID token, and hands you claims: sub, email, email_verified, name.

d. Resolve the account in your own database — key on sub, never on email:

user = db.find_by_atom9_sub(claims.sub)
if user:  log them in
else:     run YOUR onboarding, then db.create({ atom9_sub: claims.sub, ... })

e. Log in a second time with the same account. If your app created a second user, you keyed on email instead of sub — fix that before anything else.

4b. Sign in, sign up, sign out — in plain words

Atom9 proves who the person is. Your app decides what happens next. There are only three moments to handle, and each is short. This is the part most first integrations get slightly wrong, so here it is spelled out.

a. Signing in someone who already has an account. Your callback gets a token, your library checks it is genuine, and you read the claims. Look the person up in your database by their Atom9 sub. You found them → start your app's own session and send them to their dashboard. That is a login. Done.

b. Signing up someone new. It is the same callback and the same token — the only difference is your lookup by sub finds nobody. That is a first-time visitor, and the right move is simple: create the account right now, in the callback, from the claims Atom9 already gave you (sub, email, name), then start your session and continue. There is no separate "sign-up" form to send them to — the first successful login is the sign-up.

// ONE handler covers both. This is the whole thing.
user = db.find_by_atom9_sub(claims.sub)   // look up by the stable Atom9 id
if (!user) {                              // nobody yet? this is a new person
  user = db.create({ atom9_sub: claims.sub, email: claims.email, name: claims.name })
}
startYourSession(user)                    // your own cookie, your own lifetime
redirect("/dashboard")                    // let them in

The one rule that prevents the #1 bug — the sign-in loop. Once you hold a valid, verified token, the person is signed in. Create-or-find the account, start your session, and move on. Never send an authenticated person back to the login button (or to /oidc/authorize) just because a local record didn't exist yet — that is precisely what makes the browser bounce endlessly between your app and Atom9. "No account found" is not a dead end; it is your signal to create one.

c. Signing out. In your app, "log out" means one thing: end your own session — clear your session cookie or server-side session and send the person to your home page. They are now out of your app. That is all most apps ever need.

The Atom9 login is a separate session, and that is on purpose: keeping it is what makes the person's next visit one tap instead of a full sign-in — the same convenience as "Sign in with Google". You do not reach into it. If someone wants to sign out of Atom9 itself — for example on a shared computer — they do that from their own Atom9 account at id.atom9.com, where they can also see every app their login is connected to. There is deliberately no way for one app to sign a person out of all the others.

5. Test from the command line — no app required

You can drive the entire flow with curl and a browser. This is the fastest way to confirm your registration works before writing any code.

a. Check the public endpoints (works even before registering):

curl -s https://id.atom9.com/.well-known/openid-configuration
curl -s https://id.atom9.com/.well-known/jwks.json

b. Register a localhost callback on your sign-in app, e.g. http://localhost:9990/callback. Nothing needs to listen there — the redirect will just fail in the browser, and that is fine for this test.

c. Build an authorize URL (bash; generates PKCE + state):

CLIENT_ID="a9-xxxxxxxxxxxx"
REDIRECT="http://localhost:9990/callback"
VERIFIER=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c 64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary \
  | openssl base64 -A | tr '+/' '-_' | tr -d '=')
STATE=$(openssl rand -hex 12)

echo "https://id.atom9.com/oidc/authorize?response_type=code&client_id=$CLIENT_ID\
&redirect_uri=$REDIRECT&scope=openid%20profile%20email&state=$STATE\
&code_challenge=$CHALLENGE&code_challenge_method=S256"

d. Open that URL in a browser, sign in, and approve. The browser lands on localhost:9990/callback?code=...&state=... and shows a connection error — copy the code value from the address bar. It is single-use and expires within minutes.

e. Exchange the code for tokens (same shell, so $VERIFIER is still set):

curl -s https://id.atom9.com/oidc/token \
  -d grant_type=authorization_code \
  -d code="PASTE_THE_CODE" \
  -d redirect_uri="$REDIRECT" \
  -d client_id="$CLIENT_ID" \
  -d client_secret="PASTE_YOUR_SECRET" \
  -d code_verifier="$VERIFIER"

f. Look inside the ID token — decode its middle segment to see your sub and email claims:

printf %s "PASTE_THE_ID_TOKEN" | cut -d '.' -f2 | tr '_-' '/+' \
  | awk '{ p=length($0)%4; if(p==2) $0=$0"=="; else if(p==3) $0=$0"="; print }' \
  | base64 -d

Decoding is for looking, not for trusting: a real app must validate the token (signature against the JWKS, iss, aud, exp, nonce) — every library in the examples does this for you.

Light or dark — you drive it, and it round-trips. The Atom9 login does not follow the visitor's operating system; your site owns the choice so the theme stays consistent across your site, the Atom9 login, and your app. It's a simple round-trip: (1) track the visitor's theme on your site (default light); (2) when you send them to log in, add theme=dark or theme=light to the authorize request — the login and consent pages render in that theme (no param → light); (3) on the callback, Atom9 returns theme=dark alongside code and state — read it, apply it on your post-login page, and persist it for the session. Only dark and light are ever emitted.

6. Before you go live

a. Redirect URLs are https:// only (localhost is for development).

b. The client secret lives in server-side configuration, not in code or a repository.

c. Your app treats email_verified: false as unverified — no access to anything tied to that address.

d. Read Rules & responsibilities — the short list of practices that keep the integration safe, and where Atom9's responsibility ends and yours begins.

7. Optional: let Atom9 manage the user profile

Everything above assumes your app owns its accounts and profile data — the default. If instead you'd like Atom9 to own the end-user record — run the onboarding, take the person's consent, store their profile, and answer their data requests — you can switch that on per sign-in app. Your login code does not change: you still send people to id.atom9.com and still receive the same token keyed on sub. Only Atom9's behaviour behind the login changes.

a. Turn it on. On your sign-in app in the admin (Settings → Authentication), enable "Create a managed end-user profile on first sign-in." Two settings shape it:

b. Sign-up policy decides who may create an account: open (anyone who signs in), invite-only (a profile is created only for invited people), or request access (the person is onboarded in a pending state and waits for approval).

c. Security level sets an assurance floor (standard, elevated, high, or regulated): Atom9 refuses or steps up weak sign-in methods so every managed profile meets at least the strength you require. If you also define which profile fields your service needs, Atom9 collects the required ones during onboarding.

d. What the person sees. A login themed as you (your name and look, no Atom9 admin surface), then a consent page that spells out exactly what data you will hold and why — required vs optional, no pre-ticked boxes — with anything Atom9 already knows (name, email) offered to share rather than retype. Declining optional data stores only what is required. Returning visitors are not asked again — they just sign in.

e. Who is responsible. Atom9 becomes the custodian of that managed profile and the person manages it themselves at atom9.com ("Your accounts & data"), where they can see every Atom9-managed account they hold and export or erase their data per service — so you do not build export or delete screens. You remain the data controller for the profile; Atom9 processes it on your behalf under a data processing agreement. Everything inside your own app — sessions, permissions, and any data you keep on your side — stays yours.

f. Your callback still does the same job — this is the trap. Turning managed profiles on does not change your login code: you still receive the same token keyed on sub, and you still mint your own session and let the person in. The difference is that in managed mode you may keep no local users table at all — so treat a validated sub as the account, start your session, and go. Do not look the person up in a local database and bounce them back to the login button when the row is missing — there may not be one, and that is the classic managed-mode login loop. A validated token is the green light, every time.

Pick pure-OIDC (steps 1–6) when your app already has its own accounts and data store; pick managed profiles when you'd rather Atom9 own the end-user record, the onboarding, and the data-rights obligations. The choice is per sign-in app and reversible.

8. Your users' data & privacy — and the one link to add

People increasingly expect a clear answer to "what do you know about me, and how do I leave?" Atom9 makes that easy — and where the person goes depends on the mode you picked in step 7.

a. If Atom9 manages the profile (step 7 is on). Atom9 is the custodian of that end-user record, and the person manages it themselves — there are no data screens for you to build. They open Your accounts & data at https://id.atom9.com/login?view=accounts, see every service their Atom9 login is connected to (yours included), and can export or remove their data from any one of them — including removing themselves from your app — in a couple of clicks. Removing your app there deletes the managed profile and revokes the connection.

b. Add one link — that is the whole task. Put a plain "Manage your data" or "Privacy" link in your app (in account settings, or your footer) pointing to https://id.atom9.com/login?view=accounts. That single link gives your users a real, working data-rights centre without you writing a line of export or delete code. They also see the same controls on the Atom9 consent screen every time they connect, and can leave from there too.

c. If your app owns the profile (pure OIDC — the default). Then the data your app stores is yours to manage: you are the data controller for it, and export and deletion of that data are your job. Atom9 still looks after the person's Atom9 account and login data on its own side.

d. Where the responsibilities are written down: the GDPR page explains controller vs custodian in each mode, and Rules & responsibilities has the one-glance table. The login itself is always governed by the Atom9 Privacy Policy and Terms.