snoozestackdocs

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.

SurfaceAvailability
Portal UIUsers (delete/ban) and Providers (declared methods + the one callback URL to register) — configuration itself is files
CLIsnoozestack auth push (run by publish) sends the declared config; auth pull writes the email/page templates out as files
SDK / HTTPsnoozestack.identity.* — magic link, password, OAuth, id-token, sessions, sign-out
Why use it

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 on confirm_signups if 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" binds capabilities.auth.user.id into its own queries — the client never sends, and cannot forge, its identity. See Permissions.

Declaring it#

snoozestack/project.toml
[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 ID
secret_name = "APPLE_SIGNIN_KEY" # the .p8 key's contents
team_id = "ABCDE12345"
key_id = "XYZ9876543"
native_client_ids = ["com.example.app"] # iOS bundle ids for id-token sign-in

client_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#

app code
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:

terminal
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.html

The 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.

  1. Create or pick a Google Cloud project at console.cloud.google.com — the project dropdown in the top bar → New Project.
  2. 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.
  3. Add the scopes under Data Access: openid, …/auth/userinfo.email, and …/auth/userinfo.profile — then Update and Save.
  4. 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.)
  5. Declare it in the project. Add the [[auth.providers]] entry with the web client's id, set the secret, and publish:
    terminal
    snoozestack secrets set GOOGLE_OAUTH_SECRET=GOCSPX-
    snoozestack publish
  6. Add yourself as a test user under Audience → Test users — while the Google app is in Testing, only listed accounts can complete sign-in.
  7. Test the flow from your app:
    sign in with Google
    snoozestack.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();
  8. 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.
ErrorCause
redirect_uri_mismatchWhat you registered when creating the OAuth client isn't character-for-character the project's callback URL.
access_blockedThe 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:

ValueWhere it comes fromLands in
App ID / Bundle IDIdentifiers → an App IDs entry with Sign in with Apple tickednative_client_ids, for a native app
Services IDIdentifiers → a Services IDs entry (convention: com.example.app.signin)client_id
Team IDTop right of the developer accountteam_id
Key ID + .p8 keyKeys → a Sign in with Apple key — downloadable exactly oncekey_id, and the key contents via snoozestack secrets set
  1. Register the Services ID and point it at your project. Open the services ID, tick Sign in with AppleConfigure: 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." with errorCode: invalid_request rather 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.
  2. 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.
  3. Declare it and publish:
    terminal
    snoozestack secrets set APPLE_SIGNIN_KEY="$(cat AuthKey_XYZ9876543.p8)"
    snoozestack publish
  4. Test the flow. On the web this is the ordinary redirect (startOAuth({ provider: "apple" })); on iOS, pass the credential from ASAuthorizationController straight 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.

ErrorCause
invalid_clientThe declared Services ID, Team ID, Key ID, and key don't agree — re-check the four values, then publish again.
invalid_requestThe 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 appThe 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)#

application identity — on the project's own host
# Request a magic link
curl -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.