Flows recipes for games
These recipes solve common game problems with the built-in steps. Each one is a complete flow definition you can rebuild in the console's step cards, test and publish.
Every recipe follows the same pattern: open Settings, select Open the flows storyboard, create a flow under the trigger the recipe names, add the steps, select Save draft, try it with Run test, then Publish draft and Use for project. See Flows for how triggers and versions work.
Block banned Steam IDs
Refuse players whose linked Steam account is on your ban list. Use a Block list step that matches on Steam ID, with one SteamID64 per line and the Account restricted message.
examples/flows/block-banned-steam-ids.json{
"timeBudgetMs": 500,
"steps": [
{
"id": "banned-steam",
"type": "block_list",
"attribute": "steam_id",
"values": ["76561197960287930", "76561198000000001"],
"reasonCode": "banned_steam_id",
"message": "account_restricted"
}
]
}- Use it After sign-in. Before registration there is no player yet, so the list can't see the Steam account. A banned player who signs up for the first time gets an account, but is refused before any token is issued.
- Use the same flow for Device approval too, so a banned player can't approve a game's device code from a browser session that's already signed in.
- A guest who links a banned Steam account is checked the next time they sign in, not during the upgrade itself.
- A list holds up to 500 IDs, and changing it means publishing a new version. For a longer or changing list, keep bans in your own service and use the backend call in the next recipe instead.
Steam profile URLs that use a custom name don't show the SteamID64; take the ID from your game server's records instead.
Create a player profile on first sign-in
Have your game's backend create a profile the first time a player signs in, and return data to store on the player. Use a Call your backend step, with a Condition in front of it that skips guests.
examples/flows/player-profile.json{
"timeBudgetMs": 2000,
"steps": [
{
"id": "skip-guests",
"type": "condition",
"expression": { "==": [{ "var": "method" }, "guest"] },
"then": { "action": "skip", "steps": 1 },
"else": { "action": "continue" }
},
{
"id": "profile",
"type": "call_backend",
"url": "https://api.mygame.dev/auth4/flow-step",
"timeoutMs": 1500,
"onError": "fail_open"
}
]
}The backend checks the request's signature, refuses banned players and creates the profile if it doesn't exist yet. It answers with the player's tier, which is stored in their metadata:
examples/server/shared/flow-step.tslet step;
try {
// secret is the project's whsec_… flows signing secret, kept in your secret store.
step = await verifyFlowStepRequest(request, { secret });
} catch (error) {
if (error instanceof FlowStepVerificationError) return flowStepRejectedResponse();
throw error;
}
const { input } = step.event;
// Before registration there is no user yet, so there is nothing to look up.
if (!("user_id" in input)) return flowStepResponse({ action: "continue" });
if (await players.isBanned(input.user_id)) {
return flowStepResponse({
action: "deny",
reason_code: "banned_player",
message: "account_restricted",
});
}
const profile = await players.ensureProfile(input.user_id);
return flowStepResponse({ action: "continue", claims: { tier: profile.tier } });- Flows can't tell a first sign-in from a later one, so the step runs on every sign-in. Make profile creation safe to repeat, keyed on
user_id, asensureProfileis inexamples/server/shared/flow-step.ts. - The step uses
fail_open, so players can still sign in while your backend is down. Create the profile again later, for example when the game first calls your API with the player's token. - Create the project's flows signing secret first, from the storyboard'sFlows signing secret section, and store it in your server's secret store; see Check the signature.
- A webhook for
user.createdalso tells your backend about new players, but it arrives after sign-in and can't refuse anyone or store data on the player.
Add a tier claim
Give supporters a tier of gold in their tokens. A Condition checks whether the player has the application's supporter role and skips the next step when they don't. A Store metadata step then sets tier.
examples/flows/tier-claim.json{
"timeBudgetMs": 500,
"steps": [
{
"id": "supporters",
"type": "condition",
"expression": { "in": ["supporter", { "var": "roles" }] },
"then": { "action": "continue" },
"else": { "action": "skip", "steps": 1 }
},
{ "id": "gold", "type": "set_claim", "key": "tier", "value": "gold" }
]
}The supporter role must exist for the application, and the condition reads the player's roles, so this flow can't be published for Before registration. The value is stored once and stays until a step or the console changes it. Your server reads it from the verified access token:
const claims = await auth4.validateAccessToken(token);
const tier = claims.metadata.tier ?? "standard";- The value is in the
auth4_metaobject of access tokens issued after it's stored, refreshes included. - To take the tier away again, store
nullfor the key with another Store metadata step, or remove it from the player in the console. - When the tier comes from your own data, such as purchases, return it from your backend instead, as in the profile recipe.