Authentication

Players sign in with a strafe.fun account or Discord. Your game gets a signed token it can trust.

How it works

  1. The browser SDK mounts a small pill in the top-right corner of your page. Before login it reads Log in; after, it shows the player's avatar and name.
  2. Clicking it opens a popup on strafe.fun, email/password, or Sign in with Discord. Your page never sees a password.
  3. On success the popup hands your page a signed JWT. The SDK stores it, tells strafe.fun a player joined, and attaches the account to the session already being measured.
  4. Your game server (if you run one) receives a playerJoin event over a WebSocket, with the player's saved data already loaded.
The token is a JWT signed by strafe.fun, valid for 7 days, and kept in localStorage under strafe_access_token. Anything acting on it, awarding items, saving progress, should verify it server-side rather than trusting a user id sent from the browser.

Hosted on strafe.fun? The player is already signed in

When your game is played at strafe.fun/play/your-game, a visitor who is signed in to strafe.fun arrives signed in to your game too. The page hands the SDK the same token the popup would have, so there is no second login. The pill shows their name from the first frame and onChange fires with a user straight away.

So don't assume getUser() is null at startup. Gate on the onChange state rather than on “has the player clicked Log in yet”, and your game works the same whether it is hosted here or on your own domain, where the popup is still how someone signs in.

Set it up

Login needs nothing beyond the App ID. Creating the client is the whole setup:

<script src="https://strafe.fun/js/strafe.js"></script>
<script>
  const strafe = new Strafe({ appId: 'your-app-id' });
</script>

Read the signed-in player, and react to changes:

strafe.getUser();  // { id, name, picture } | null
strafe.getToken(); // the JWT, or null

// Fires immediately with the current state, then on every login/logout
strafe.onChange(({ user, token }) => {
  if (!user) return showLoginPrompt();
  startGameAs(user.name);
});

Don't trust the browser

getUser() is for your UI. For anything that matters, send the token to your server and verify it there. The browser can claim to be anyone.

Verify a player on your server

With the server SDK (needs your App Secret, never the browser):

const { Strafe } = require('@strafe-fun/sdk/server');
const strafe = new Strafe({
  appId: 'your-app-id',
  appSecret: process.env.STRAFE_APP_SECRET,
});

const player = await strafe.verifyToken(tokenFromBrowser);
if (!player) return reject('not signed in');
player.userId; // trust this one

Or straight over HTTP:

curl -X POST https://strafe.fun/api/auth/verify-token \
  -H 'Content-Type: application/json' \
  -H 'X-App-Id: your-app-id' \
  -H 'X-App-Secret: your-app-secret' \
  -d '{"token":"<player token>"}'

# { "valid": true, "userId": "...", "name": "...", "email": "...",
#   "discordId": "...", "solanaWallet": null }

An invalid or expired token answers 401. The response also carries the player's verified Solana wallet when they have linked one. See Tokens.

Check it worked

Check it worked

1. Open the sandbox with your App ID and click Log in on the widget. The Player login check turns green once the session we're recording carries an account.

2. In your own game, sign in and run this in the console:

await fetch('https://strafe.fun/api/sdk/verify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer ' + strafe.getToken(),
  },
  body: JSON.stringify({ appId: strafe.getAppId(), sessionId: strafe.getSessionId() }),
}).then(r => r.json());

// auth: { valid: true, userId: '...' }   ← the token is one we issued
// session: { identity: 'account' }       ← the session is attributed to it

3. Your game's Settings → Setup panel counts signed-in sessions across all your players, so you can tell “login is broken” from “nobody has tried yet”.

When it doesn't work

The popup opens and closes, nothing happens
The SDK only accepts the token from strafe.fun itself. A custom apiUrl that isn't the host you actually log in on will silently drop it. Leave apiUrl unset unless you know you need it.
The popup never opens
A popup blocker. The widget opens it from a real click, so this usually means the click was intercepted by your own handler. Check for stopPropagation on the page.
Permissions are all false
The player has no Discord linked, isn't in your server, or the Strafe bot isn't in it. The widget states which of the three it is.
Login works, but my server never hears about it
playerJoin arrives over a WebSocket authenticated with your App Secret. A wrong secret closes the socket with code 4003 and logs invalid appId or appSecret.