A native game (Godot 4, exported for Windows, Linux or Steam Deck) uses Gipity as its backend: player accounts, server functions, a database, and leaderboards with ghost recordings. The game talks to it over HTTPS with the Gipity Godot addon. There is no server to run.

Who does what:

Need Use
Player sign-in (Steam, guest) Gipity app players (this skill)
Leaderboards (scores or times, daily/weekly/season), personal bests, replays The leaderboard kit (see the leaderboard skill)
Saved progress, race results, ratings, game rules on the server Your own functions + database (app-development)
Live race traffic (car positions, 30 Hz) Steam Networking Sockets through GodotSteam: Steam's relay network, free, global, NAT-free
Lobbies, invites, matchmaking between Steam players Steam lobbies (GodotSteam)

Don't route per-tick game state through Gipity: functions are request/response, not a game relay. Report outcomes (race results, lap times) to Gipity when they happen.

Set up the backend

Keep the backend in a backend/ folder of the game repo, with a .gdignore file in it so Godot doesn't import it:

mkdir backend && touch backend/.gdignore && cd backend
gipity init my-game            # link this folder to a new Gipity project
gipity add api                 # functions + database (no web frontend)
gipity add leaderboard         # optional: leaderboards with replays
gipity project auth app        # players sign in to THIS game, no Gipity account needed
gipity project auth --unique-names on   # optional: no two players share a display name
gipity deploy dev

gipity project auth modes: gipity (default: Sign in with Gipity only), app (Steam and guest players only), both. App sign-in is refused until the mode is app or both. --unique-names on|off sets whether display names must be unique in this game (see Display names); it is off by default.

Steam sign-in keys

Steam sign-in verifies each player's session ticket with Valve using the game's Steamworks publisher Web API key. The developer creates it in Steamworks: Users & Permissions > Manage Groups > (a group with the app) > Create WebAPI Key. Store it and the AppID as project secrets:

gipity secrets set STEAM_WEB_API_KEY <publisher key>
gipity secrets set STEAM_APP_ID <appid>

Only the developer can get these; ask them. Without them, Steam sign-in returns a setup error, and guest sign-in still works. Valve's test AppID 480 (Spacewar) has no publisher key you can use, so Steam sign-in needs the game's real AppID.

Add the addon to the game

  1. Copy examples/godot/addons/gipity/ from the Gipity registry (https://github.com/GipityAI/registry/tree/main/examples/godot) into the Godot project's addons/ folder and enable Gipity under Project Settings > Plugins. That adds the Gipity autoload. To fetch just that folder: git clone --depth 1 --filter=blob:none --sparse https://github.com/GipityAI/registry.git && git -C registry sparse-checkout set examples/godot. The same folder has Gip Racer, a demo game with its backend, to copy from.
  2. Set Project Settings > gipity/app_guid to the project guid (gipity project info).
  3. For Steam: install GodotSteam, call Steam.steamInitEx() at startup, and Steam.run_callbacks() every frame.
func _ready():
	var r = await Gipity.sign_in_steam()
	if not r.ok:
		r = await Gipity.sign_in_guest("Player")   # non-Steam build, or Steam unavailable
	if r.ok:
		print("Signed in as ", Gipity.player.displayName)

func save_result(result: Dictionary):
	var r = await Gipity.call_function("race-result", result)   # r.data = the function's return value

Every call returns { ok, data, error, code, status, offline } and never throws. code is the server's error code (e.g. DISPLAY_NAME_REJECTED, PLAYER_CLEANUP_FAILED, GAME_VERSION_TOO_OLD), "" when there is none; a refused leaderboard run keeps ok: true with data.accepted: false and its code in code. offline means the server couldn't be reached: the game must keep working without online features. Leaderboard submissions made offline are queued and sent after the next sign-in; a queued run the server then refuses is dropped and reported through the queued_run_dropped(run, code, reason) signal. Runs carry the game's application/config/version as gameVersion. The addon renews expired player tokens by signing in again.

Other calls: link_steam() (attach Steam to the current guest, keeping their progress), sign_out(), rename_player(name) (the player's name in this game, for guests and Steam players; null clears it), check_name(name) (is a name free, with suggestions; works before sign-in), Gipity.player.nameConflict (true when the game should ask the player to pick a name), delete_player() (account deletion requests; on PLAYER_CLEANUP_FAILED the session is kept so you can retry), and the Gipity.leaderboard client (submit, top, around_me, me, friends, friends_steam, ghost, boards, seasons).

Players in your functions

A signed-in player is an ordinary signed-in user to your functions: auth: user functions accept them, and ctx.auth.userGuid / ctx.auth.displayName are set (displayName is the player's name in this game: a name they set, else their Steam persona). Key per-player rows on ctx.auth.userGuid, as in any app.

ctx.auth.identity says how they signed in: { provider: 'steam', id: '<SteamID64>' } or { provider: 'guest', id: '<hash>' }, and null for Gipity users. Use the Steam id to match Steam friends to players.

A player belongs to one game. The same Steam account is a different player in each of your games, and a player token only works on its own game's API.

// functions/race-result.js  (gipity.yaml: auth: user, tables: [race_results])
export default async function raceResult(ctx, { db, guid }) {
  const { track, position, timeMs } = ctx.body;
  await db.insert('race_results', {
    id: guid('rr'), user_guid: ctx.auth.userGuid, steam_id: ctx.auth.identity?.id ?? null,
    track, position, time_ms: timeMs,
  });
  return { saved: true };
}

The server can't see the game, so treat everything a client sends as a claim: check it for plausibility (bounds, consistency, rate) and keep a way to ban. The leaderboard kit does this for scores.

Calling the API without the addon

Any client can use plain HTTPS against https://a.gipity.ai/api/<appGuid>:

Call Body Returns
POST /auth/steam { ticket }: the bytes from GetAuthTicketForWebApi("gipity"), hex-encoded. Add "link": true with a guest's Bearer token to link. { data: { token, expiresIn, user } }
POST /auth/guest { deviceSecret, displayName? }: 32-256 random characters the client generates once and keeps same
GET /auth/player Bearer token { data: { guid, displayName, provider, providerUserId } }
PATCH /auth/player { displayName } (1-40 characters, or null to clear) with a player's Bearer token { data: { guid, displayName, nameConflict } }. 409 DISPLAY_NAME_TAKEN when names are unique and it's taken
GET /auth/player/name-available?name= none, or a player's Bearer token (their own name counts as free). 30 checks a minute per IP { data: { name, available, uniqueNames, suggestions } }: up to 5 free names that pass the filter when taken
DELETE /auth/player Bearer token { data: { deleted: true } } after the app's cleanup ran (see below)
POST /fn/<name> Bearer token, JSON body { data: <function return> }

The sign-in user is { guid, displayName, avatarUrl, provider, providerUserId, isNew, nameConflict }.

The ticket identity must be exactly gipity. Player tokens last 24 hours; there is no refresh token, so sign in again (a fresh Steam ticket, or the same device secret) when one expires or a call returns 401.

Display names

The filter. A name the player chooses (a guest's displayName at sign-in, or any PATCH /auth/player) is checked against a profanity and slur filter that sees through leetspeak, spacing and lookalike letters. A refused name is a 400 with code: 'DISPLAY_NAME_REJECTED' and a message to show; nothing is silently rewritten, so ask the player for another name. Steam persona names are used as Steam provides them (only control characters are dropped and the length is capped at 40).

Steam players' name in your game. A Steam player is named after their Steam persona, refreshed on every sign-in. PATCH /auth/player gives them a name for this game instead: it wins over the persona on every later sign-in (a persona change on Steam doesn't touch it), and { displayName: null } clears it, going back to their current persona. Linking Steam to a guest keeps a name the guest chose; a guest without one takes the persona. Functions see the effective name as ctx.auth.displayName, and the leaderboard kit stores it with each new personal best.

Unique names (opt-in). gipity project auth --unique-names on makes display names unique among the game's players. Names are compared ignoring case, accents, spacing, invisible characters and lookalike letters (fullwidth, Cyrillic and Greek), so "Turbo", "TURBO", "Turbo" and "Тurbо" are one name; leetspeak is not folded ("TURB0" is a different name). The database enforces it, so two players racing for the same name can't both get it. With it on:

Deleting a player

DELETE /auth/player erases the player (for account-deletion requests). Your app's data about them is erased first: every function declared with hooks: [user_deleted] in gipity.yaml runs with ctx.trigger = { type: 'user_deleted', userGuid }, and the leaderboard kit ships one. If any of them fails, nothing is deleted and the call returns 502 PLAYER_CLEANUP_FAILED; retry it later. Keep your own per-player tables clean the same way:

# gipity.yaml, under the functions phase
function_definitions:
  - name: forget-player
    auth: member            # the platform calls it; players and visitors can't
    hooks: [user_deleted]
    tables: [race_results]
// functions/forget-player.js
export default async function forgetPlayer(ctx, { db }) {
  if (ctx.trigger?.type !== 'user_deleted') return { error: 'Runs when a player is deleted.' };
  await db.query('DELETE FROM race_results WHERE user_guid = $1', [ctx.trigger.userGuid]);
  return { ok: true };
}

Common mistakes