Sign in with Quarterturn
For your own game, site or app. Free, no review to wait on.
People sign in with the Quarterturn account they already have, by email code, with no password. You get a stable id for them, their name and, if you ask, their email address. You can also keep one save per person in their account, so they can pick up your game on another device. Your app shows up on their account page, where they can sign it out on any device.
1. Register your app
Sign in and open the developer portal, then choose New application. Give it the name people will see and every address sign-ins may come back to, one a line. That's where the code goes, so only addresses you control: an https:// page, http://localhost while you build, or your app's own scheme such as com.you.game://auth. You get a client id. There is no secret: the browser or app proves itself with PKCE instead, which is safe to ship in public code.
Apps not made by Quarterturn say so on the sign-in screen, with the address people will go back to.
On the app's own page you can add an icon and a banner, reset a client secret (shown once; once the app is set to confidential, /api/oauth/token needs client_secret too, so keep it on your server), make an app token for your server (GET /api/app/me with Authorization: Bearer qta_… says who the app is), and ask to be verified. A verified app gets a tick on the sign-in screen.
2. Add the button
One script, nothing to build. The button opens a small sign-in window and resolves with the person once they've said yes.
<script src="https://www.qturn.dev/sdk/v1.js"></script>
<div id="signin"></div>
<script>
Quarterturn.init({ clientId: 'YOUR_CLIENT_ID', scope: ['profile', 'saves'] });
// Back from a full-page sign-in (when popups are blocked)? Finish it.
Quarterturn.handleRedirect().then(show);
Quarterturn.user().then(show);
Quarterturn.button('#signin', { theme: 'dark', onSignIn: show });
function show(user) {
if (user) console.log('Hello', user.name, user.sub);
}
</script>
redirectUri defaults to the page you're on (without its query), so register that address. Pass redirectUri to init to use another one on the same site.
What the script gives you
Quarterturn.button(target, { theme, label, onSignIn, onError, redirect }): the button, in'light'(white) or'dark'. Keep the label: people know it.Quarterturn.signIn({ redirect, prompt }): the same, from your own button.prompt: 'consent'asks again even if they said yes before.Quarterturn.user({ fresh }):{ sub, name, email, email_verified }, as far as your scope allows, ornull. Kept in this site's storage, so it's instant on load.Quarterturn.accessToken(): a current access token, refreshed for you. Send it to your own server if it needs to know who's calling (see userinfo).Quarterturn.saves.get(),.put(save, { ifMatch, force }),.remove(): the person's save for your app.Quarterturn.onChange(fn): called with the person, ornull, whenever that changes.Quarterturn.signOut(): forgets them here and revokes the token.
sub is the id to key your records on. It's different for every app, so two apps can't match people up through it, and it never changes, even if they change their email address.
Scopes
profile: their name, andsub. Always included.email: their email address. It's always one they've proved they own, soemail_verifiedistrue.saves: one save for your app in their account.
Cloud saves
Any JSON up to 256 KB. Each save carries updatedAt and the device that wrote it. Pass back the updatedAt you last read as ifMatch, and a newer save from another device is never overwritten by accident: you get a 409 with that save's updatedAt and device, and can ask the player which one to keep.
const cloud = await Quarterturn.saves.get(); // { save, updatedAt, device } or null
try {
await Quarterturn.saves.put(mySave, { ifMatch: cloud?.updatedAt });
} catch (e) {
if (e.error === 'conflict') { /* ask: this device's or the cloud's? */ }
}
// They picked this device's:
await Quarterturn.saves.put(mySave, { force: true });
Without the script
It's OAuth 2.0's authorization code flow with PKCE (S256 only), so any OAuth library works too. Every API answer is JSON; an error is { "error", "message" } with the status.
- Make a random
code_verifier(43 to 128 characters) and itscode_challenge, the base64url SHA-256 of it. - Send the person to:
https://www.qturn.dev/authorize ?client_id=YOUR_CLIENT_ID &redirect_uri=https://yourgame.example/signed-in &scope=profile%20saves &state=RANDOM &code_challenge=CHALLENGE &code_challenge_method=S256 &device_id=OPTIONAL_STABLE_ID (8 to 64 of A–Z a–z 0–9 _ -) &device_name=OPTIONAL_NAME (shown on their account page) - They come back to
redirect_uriwith?code=…&state=…, or?error=access_denied&state=…if they cancelled. Checkstateis yours. The code works once, for a minute. - Swap it for tokens:
POST https://www.qturn.dev/api/oauth/token Content-Type: application/json { "grant_type": "authorization_code", "code": "…", "code_verifier": "…", "client_id": "YOUR_CLIENT_ID", "redirect_uri": "https://yourgame.example/signed-in" } → { "access_token": "…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "…", "scope": "profile saves" } - When the access token runs out, refresh. Each refresh token is spent once and you get a new one, so keep the new one.
A{ "grant_type": "refresh_token", "refresh_token": "…", "client_id": "YOUR_CLIENT_ID" }400means the person signed you out from their account page: forget them.
A device signs in once: signing in again on the same device_id replaces that device's earlier sign-in rather than adding another line to their account page.
Endpoints
All take Authorization: Bearer ACCESS_TOKEN but token and revoke, and all answer any origin.
GET /api/oauth/userinfo:{ sub, name }, plusemailandemail_verifiedwith theemailscope. Your server can call this with a token from the browser to learn who it is.POST /api/oauth/token: above.POST /api/oauth/revokewith{ "token" }: signs this device out of your app.GET /api/saves:{ save, updatedAt, device }, or404if there's none.PUT /api/saveswith{ "save", "ifMatch"? }:409 conflictififMatchis older than what's there,413over 256 KB.DELETE /api/saves.
Apps and games
Open /authorize in the system browser (Custom Tabs on Android, ASWebAuthenticationSession on iOS), never in a web view inside your app, and register your own scheme, such as com.you.game://auth, as the redirect. Catch that address when the browser opens it and swap the code as above. Pass a device_id you keep on the device and a device_name like "Pixel 9", so people can tell their devices apart.
The rules
- Ask for the least you need. Don't ask for
emailjust to say hello. - Use what you get only for your app, and delete it when someone asks or signs you out for good.
- Say in your own privacy policy that you use Sign in with Quarterturn, and what you keep.
- Don't make the button look like Quarterturn made your app, and don't change its words or tiles.
- Our terms apply. We can switch off an app that breaks them.