Authentication
Every project gets application identity — sign-in for your app's own users — with four methods: magic link (on by default), email/password, and Google and Apple sign-in. What the project offers is declared in project.toml's [auth] section; the sign-in flows themselves run on snoozestack's hosted identity plane; and the resulting session is verified by your project's runtime itself — locally, per request, no round trip — before your function sees it as capabilities.auth.user.
| Surface | Availability |
|---|---|
| Portal UI | Users (delete/ban) and Providers (declared methods + the one callback URL to register) — configuration itself is files |
| CLI | snoozestack auth push (run by publish) sends the declared config; auth pull writes the email/page templates out as files |
| SDK / HTTP | snoozestack.identity.* — magic link, password, OAuth, id-token, sessions, sign-out |
Authentication is the classic thing not to build yourself. Not because the happy path is hard — checking a password hash is twenty lines — but because the rest isn't: secure password storage, session expiry and refresh, reset tokens that can't be replayed, OAuth callback handling, email delivery. Each is a well-known place to get security subtly wrong, and none of it is what makes your product worth using.
Snoozestack splits the problem where the trust boundary actually is. Sign-in runs centrally, on the identity plane, because that's where it has to run: OAuth providers demand a stable hosted callback URL, email needs sending infrastructure, and password hardening should exist once, not in every container next to tenant code. Verification runs in your runtime: sessions are short-lived tokens signed with a per-project key, and the runtime checks the signature locally after fetching the project's public key once — so “who is calling” costs nothing per request and works the same on your laptop. Refresh and revocation stay central, so a revoked user is out within the session's lifetime.
Practical consequence — the user store is the platform's. Put your own profile fields in your own table keyed by capabilities.auth.user.id, rather than trying to extend the identity record.
- Passwordless by default
- Magic link is enabled out of the box: a successful sign-in proves the address and creates the user in one flow — nothing to configure and no password to reset. It doubles as the reset flow when you add passwords later.
- Ordinary email/password
signUpWithPassword()/signInWithPassword(). Turn onconfirm_signupsif you need verified addresses; leave it off while prototyping so sign-ups return a session immediately.- Sign in with Google / Apple
- Lower friction, and Apple is required by App Store review once an iOS app offers another social login. Each needs a one-time registration in the provider's console — walked through below. Native apps skip the browser entirely with
signInWithIdToken. - Per-user data without trusting the client
- A function with
auth = "required"bindscapabilities.auth.user.idinto its own queries — the client never sends, and cannot forge, its identity. See Permissions.
Declaring it#
[auth]redirect_urls = ["https://myapp.example.com/welcome", "myapp://callback"]confirm_signups = false # true: password sign-ups must confirm by email [[auth.providers]]name = "google"client_id = "1234-abc.apps.googleusercontent.com"secret_name = "GOOGLE_OAUTH_SECRET" # a declared [[secrets]] entry [[auth.providers]]name = "apple"client_id = "com.example.app.signin" # the Services IDsecret_name = "APPLE_SIGNIN_KEY" # the .p8 key's contentsteam_id = "ABCDE12345"key_id = "XYZ9876543"native_client_ids = ["com.example.app"] # iOS bundle ids for id-token sign-inclient_id is public by design (it rides in every browser redirect); secret_name is a reference to a declared secret — the value is set with snoozestack secrets set, never committed. redirect_urls is the allowlist of where sign-in may send a visitor afterwards: your project's own URLs are allowed automatically; add your app's domains, deep links, and localhost here. snoozestack publish (or snoozestack auth push) sends the declaration up. Provider registrations are per project — your users see your app's name on the consent screen, and no other tenant shares your registration.
Sessions, in your functions#
await snoozestack.identity.requestMagicLink({ email, redirectTo: "https://myapp.example.com/welcome" });// … user clicks the emailed link …const { user } = await snoozestack.identity.completeMagicLink(); // From here every functions.invoke() carries the session automatically.const { data } = await snoozestack.functions.invoke("my-notes");Inside the function, the runtime has already verified the token's signature before your code runs: capabilities.auth.user is { id, email } (or null under auth = "optional" with no session). Sessions are short-lived and refresh automatically in the SDK; identity.onStateChange() observes anonymous | pending | authenticated | expired reactively.
Local development: the dev account#
Under snoozestack dev, the runtime serves the project's own /auth/… routes itself, backed by one built-in account: user / 1234. Password sign-in works, and so do the Google and Apple buttons — locally the OAuth start route skips the provider and redirects straight back signed in as that account, so your callback handling is exercised without any provider registration. Its sessions are signed with a local keypair only that process accepts; no hosted project, custom domain, or preview ever honors this credential.
Email & page templates are files#
The three templates a project owns — the confirm-signup email, the sign-in-link (magic link) email, and the reset-password page — live in the repo, so they diff, review, and roll back like everything else:
snoozestack auth pull # writes the current templates into the project# snoozestack/authentication/email/confirm-signup.html# snoozestack/authentication/email/sign-in-link.html# snoozestack/authentication/page/reset-password.htmlThe two emails carry their subject on the first line as an HTML comment (<!-- subject: Confirm your {{ .ProjectName }} account -->), so the file stays a fragment you can open in a browser. auth push — which publish runs for you — sends whichever exist; the console shows them read-only. There is no template editor UI, on purpose: the files are the one copy.
Sign in with Google#
A one-time setup in the Google Cloud console, about ten minutes. You are creating an OAuth client at Google, then declaring its credentials in the project. The callback URL to register is shown on the console's Authentication → Providers page: https://<ref>.snoozestack.com/auth/oauth/google/callback — there is exactly one, and it must be registered character-for-character.
- Create or pick a Google Cloud project at console.cloud.google.com — the project dropdown in the top bar → New Project.
- Configure the OAuth consent screen. Under APIs & Services → OAuth consent screen (newer consoles: Google Auth Platform → Branding), click Get started, fill in the app name and support email, pick External as the audience (or Internal on Workspace for org-only sign-in), and Create.
- Add the scopes under Data Access:
openid,…/auth/userinfo.email, and…/auth/userinfo.profile— then Update and Save. - Create the OAuth client on the Clients tab: application type Web application, with your project's callback URL under Authorized redirect URIs. Save the client ID and secret — the secret can't be viewed again. (An iOS app additionally gets its own iOS-type client — client ID only — for native sign-in, below.)
- Declare it in the project. Add the
[[auth.providers]]entry with the web client's id, set the secret, and publish:terminalsnoozestack secrets set GOOGLE_OAUTH_SECRET=GOCSPX-…snoozestack publish - Add yourself as a test user under Audience → Test users — while the Google app is in Testing, only listed accounts can complete sign-in.
- Test the flow from your app:sign in with Googlesnoozestack.identity.startOAuth({provider: "google",redirectTo: "https://myapp.example.com/welcome", // must be on redirect_urls});// … back on that page:const { user } = await snoozestack.identity.completeOAuthCallback();
- Publish the Google app (Audience → Publish app) — with only these three non-sensitive scopes it goes live immediately, and any Google account can sign in from then on.
| Error | Cause |
|---|---|
redirect_uri_mismatch | What you registered when creating the OAuth client isn't character-for-character the project's callback URL. |
access_blocked | The consent screen is still in Testing and the account isn't on its test-user list — add it under Audience, or publish the app. |
Sign in with Apple#
Same shape as Google with one difference worth knowing up front: Apple issues no static client secret. The secret is a token signed with a .p8 key you download once — the identity plane signs it for you from the four values the provider entry declares (Services ID, Team ID, Key ID, and the key itself). Everything on Apple's side happens under Certificates, Identifiers & Profiles:
| Value | Where it comes from | Lands in |
|---|---|---|
| App ID / Bundle ID | Identifiers → an App IDs entry with Sign in with Apple ticked | native_client_ids, for a native app |
| Services ID | Identifiers → a Services IDs entry (convention: com.example.app.signin) | client_id |
| Team ID | Top right of the developer account | team_id |
| Key ID + .p8 key | Keys → a Sign in with Apple key — downloadable exactly once | key_id, and the key contents via snoozestack secrets set |
- Register the Services ID and point it at your project. Open the services ID, tick Sign in with Apple → Configure: primary App ID from above; Domains is
<ref>.snoozestack.com(host only); Return URLs is the callback from the console's Providers page —https://<ref>.snoozestack.com/auth/oauth/apple/callback. The final Save on the identifier page is easy to miss, and without it the configuration is discarded.
Skipping the Return URL is the most common way Apple sign-in fails, and the error says so only if you know the phrasing: Apple's page answers"Invalid web redirect url."witherrorCode: invalid_requestrather than naming the URL. A native app signing in with an id_token needs no Return URL at all — only the browser redirect flow does. - Create the signing key under Keys: tick Sign in with Apple, configure it against the same App ID, register, note the Key ID, and download
AuthKey_<keyid>.p8— Apple lets you download it exactly once; a lost key can only be revoked and replaced. - Declare it and publish:terminalsnoozestack secrets set APPLE_SIGNIN_KEY="$(cat AuthKey_XYZ9876543.p8)"snoozestack publish
- Test the flow. On the web this is the ordinary redirect (
startOAuth({ provider: "apple" })); on iOS, pass the credential fromASAuthorizationControllerstraight to an id-token sign-in so the user never leaves the app:iOS (Swift)let result = try await snoozestack.identity.signInWithIdToken(provider: "apple", idToken: idToken, nonce: nonce)
On iOS, prefer signInWithIdTokenResolvingPrivateRelay over plain signInWithIdToken when users can pick Hide My Email: Apple puts the relay address in the identity token on every sign-in but drops it from the credential object after the first authorization, so a user whose record was created without it comes back with user.email == nil from then on. The variant reads the token's claim when the session has none and remembers it on the device.
| Error | Cause |
|---|---|
invalid_client | The declared Services ID, Team ID, Key ID, and key don't agree — re-check the four values, then publish again. |
invalid_request | The Return URL isn't registered character-for-character on the services ID, or its domain hasn't been verified by Apple. |
bad ID token from a native app | The app's bundle ID isn't in native_client_ids, or the nonce it was issued with isn't the one passed to signInWithIdToken. |
Native id-token sign-in (Google too)#
Both Google and Apple mint an id_token a native app can present directly to POST /auth/oauth/<provider>/id-token — no browser round trip, no redirect URL. Declare the native client id (Google: the iOS client's id; Apple: the bundle ID) in native_client_ids, and use the SDKs' signInWithIdToken. The response includes isNewUser, for routing first-time sign-ins to onboarding.
Users#
The console's Authentication → Users page lists the project's signed-up users with the sign-in method each account used, and can ban/unban or delete one. Accounts are created by people signing up — there is deliberately no “add a user with a password you typed” button. Each namespace has its own user population: a preview's users are not live's.
HTTP API (/api/auth)#
# Request a magic linkcurl -X POST "https://<ref>.snoozestack.com/api/auth/magic-link" \ -H "Content-Type: application/json" \ -d '{"email": "me@example.com", "redirect_to": "https://myapp.example.com/welcome"}' # Password sign-in → { session_token, refresh_token, user }curl -X POST "https://<ref>.snoozestack.com/api/auth/password/sign-in" \ -H "Content-Type: application/json" \ -d '{"email": "me@example.com", "password": "secret123"}' # The project's session-verification public keys (what the runtime fetches once)curl "https://<ref>.snoozestack.com/api/auth/jwks"Also under /api/auth: password/sign-up, password/change, magic-link/verify, session (refresh), sign-out, delete (a signed-in user removes their own account — remove the app's own data about them first), oauth/<provider>/start|callback|id-token, and the hosted reset page. Password reset needs no second system — the reset flow is the magic link. The provider callback URLs registered at Google and Apple are the one exception without the /api prefix (…snoozestack.com/auth/oauth/<provider>/callback) — deliberately, since a registration must keep resolving permanently. See snoozestack-js for the same operations through snoozestack.identity.