quarterturn

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

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

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.

  1. Make a random code_verifier (43 to 128 characters) and its code_challenge, the base64url SHA-256 of it.
  2. 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)
  3. They come back to redirect_uri with ?code=…&state=…, or ?error=access_denied&state=… if they cancelled. Check state is yours. The code works once, for a minute.
  4. 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" }
  5. When the access token runs out, refresh. Each refresh token is spent once and you get a new one, so keep the new one.
    { "grant_type": "refresh_token", "refresh_token": "…", "client_id": "YOUR_CLIENT_ID" }
    A 400 means 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.

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