SlateFire developer docs

Everything you need to integrate SlateFire into your Godot 4 project — from the GDScript SDK to the raw HTTP API. Every method, parameter, and response shape documented below comes from the actual addons/slatefire/ source.


Install & setup

Requirements Requires Godot 4.x. Works in any project (2D, 3D, UI-only).
  1. Install the plugin. Drop the addons/slatefire/ directory into your Godot project, or install from the Asset Library.
  2. Enable it. Go to Project → Project Settings → Plugins and toggle SlateFire on. The plugin registers a singleton named SlateFire (extends Node) that is available everywhere in GDScript.
No build step The plugin is pure GDScript — no compilation, no asset bundles, no C++ modules. Enable and go.

Configure

Call SlateFire.configure() once, typically in your main scene's _ready(). You need a publishable API key from the SlateFire dashboard.

func configure(api_key: String, opts: Dictionary = {}): void
api_keyStringYour publishable API key from the dashboard (e.g. pk_live_xxxxxxxx). Must not be empty. opts.base_urlStringBase URL for API requests. Defaults to https://slatefire-guard.john-malmin.workers.dev/v1. Set this to https://api.slatefire.dev/v1 when the custom domain is live. opts.auto_retrybool (default true)Auto-back-off and retry on rate_limited responses. Retries up to max_retries times. opts.max_retriesint (default 3)Maximum retry attempts when auto-retry is active. opts.request_timeout_sfloat (default 10.0)HTTP request timeout in seconds.

When configure() is called, the SDK automatically:

func _ready(): SlateFire.configure("pk_live_xxxxxxxx") # With custom options: SlateFire.configure("pk_live_xxxxxxxx", { auto_retry = true, max_retries = 5, request_timeout_s = 15.0, })

set_player_token

Write endpoints (submit score, save data, delete saves) require a player token. Set it after your player authenticates. The SDK does not issue player tokens itself — tokens must be handed to the game client from your own trusted system (or hand-minted for testing).

func set_player_token(token: String): void
tokenStringHMAC-SHA256 signed player token. Sent as x-player-token header on write requests.
Anonymous sign-in works auth.sign_in_anonymous() is fully deployed — it creates a player, issues an HMAC-signed token (30-day expiry), sets it automatically, and emits signed_in. Other auth methods (register_email, sign_in_email, etc.) are still not_implemented.

Make your first call

Here is the full getting-started flow — sign in anonymously, then submit a score and read the leaderboard. All three calls work against the live backend.

func _ready(): SlateFire.configure("pk_live_xxxxxxxx") # Step 1: sign in anonymously (issues a player token automatically) var auth := await SlateFire.auth.sign_in_anonymous() if not auth.ok: return # Step 2: submit a high score (uses the token from step 1) var r := await SlateFire.leaderboards.submit("high_scores", 9450) # Step 3: read the top of the leaderboard var board := await SlateFire.leaderboards.top("high_scores") if board.ok: for entry in board.data: print(entry.rank, " ", entry.score)

All working sign_in_anonymous, leaderboards.submit, leaderboards.top, saves.put, saves.get, and saves.delete all work against the live backend.


SDK Reference

Every public member of the SlateFire singleton and its sub-modules. Methods are tagged by implementation status against the live backend.

SlateFire (singleton)

The autoload registered by the plugin. Extends Node. Created automatically when the plugin is enabled — no manual instantiation.

Properties

PropertyTypeDescription
authSlateFireAuthPlayer authentication sub-module.
leaderboardsSlateFireLeaderboardsLeaderboard read/write sub-module.
savesSlateFireSavesCloud save sub-module.
analyticsSlateFireAnalyticsAnalytics event tracking sub-module.
configSlateFireConfigRemote config sub-module (local-only for now).
is_onlineboolRead-only. false when the SDK detects a connectivity failure.
is_pausedboolRead-only. true while writes are blocked due to a service_paused response.

configure

func configure(api_key: String, opts: Dictionary = {}): void

Initializes the SDK. Creates sub-modules, fetches config, sets saves.max_bytes. Must be called before any other SDK method. The api_key must not be empty.

Options (all optional):

KeyTypeDefaultDescription
auto_retrybooltrueAuto-backoff and retry on rate_limited responses for write operations.
max_retriesint3Maximum retry attempts.
request_timeout_sfloat10.0HTTP request timeout in seconds.

set_player_token

func set_player_token(token: String): void

Sets the HMAC-signed player token sent as x-player-token on write requests. Also notifies SlateFire.auth via _on_token_changed().

Signals

SignalArgumentsFired when
service_pausedretry_after: intBackend returns HTTP 503. Writes are blocked for retry_after seconds.
service_resumedPause period expires. Writes may resume.
request_failederror: SlateFireErrorAny error from an SDK network call.
quota_warningpercent: intLocal usage estimate crosses 80% or 95% (not currently emitted by the SDK — reserved for future).
quota_reachedkind: StringBackend returns HTTP 402 with mau_quota or write_quota.

SlateFireResponse

SlateFireResponse extends RefCounted

Every await-able SDK call returns one of these.

PropertyTypeDescription
okbooltrue on success, false on error.
dataVariantThe response payload. Type depends on the call (Dictionary, Array, String, bool, null). null on error.
errorSlateFireErrornull when ok == true, otherwise a SlateFireError instance.

SlateFireError

SlateFireError extends RefCounted
PropertyTypeDescription
codeStringStable error code (see error reference).
messageStringHuman-readable message.
statusintHTTP status code; 0 for offline/timeout errors.
retry_afterintSeconds to wait before retrying. 0 when not applicable.

SlateFire.auth Working

Player authentication. All sign-in methods call their respective backend endpoints and automatically set the returned player token on the HTTP client. Email registration uses PBKDF2 password hashing server-side. Provider sign-in creates a new player on first use and links the provider for future sign-ins.

Properties

PropertyTypeDescription
current_playerDictionaryCurrently signed-in player data. Empty dict when signed out.
is_signed_inboolfalse until a sign-in method succeeds.

Signals

SignalArguments
signed_inplayer: Dictionary
signed_out

Methods

func sign_in_anonymous() -> SlateFireResponse

POSTs to /v1/players/anonymous. On success, automatically sets the returned player token via set_player_token(), populates current_player, sets is_signed_in = true, and emits signed_in.

func register_email(email: String, password: String) -> SlateFireResponse

Creates a new player account with email and password. Password is hashed server-side with PBKDF2. Returns the standard player object with a signed token. Returns email_taken (409) if the email is already registered for this project.

Errors: email_and_password_required (400), email_taken (409).

func sign_in_email(email: String, password: String) -> SlateFireResponse

Signs in with email and password. Verifies the password hash server-side. Returns the standard player object with a signed token. Returns invalid_credentials (401) if the email or password is wrong.

Errors: email_and_password_required (400), invalid_credentials (401).

func sign_in_provider(provider: String, token: String) -> SlateFireResponse

Signs in with an external provider (e.g. "steam", "google", "discord", "apple"). The token is the provider's user ID for this player. On first use, a new player is created and the provider link is stored. On subsequent calls, the existing player is returned. The player token is automatically set.

Errors: provider_and_token_required (400).

func link_email(email: String, password: String) -> SlateFireResponse

Links an email/password to the currently signed-in player. Requires an active player token (call sign_in_anonymous or another sign-in method first). Returns email_taken (409) if the email is already in use.

Errors: unauthorized (401, if not signed in), email_and_password_required (400), email_taken (409).

func link_provider(provider: String, token: String) -> SlateFireResponse

Links an external provider to the currently signed-in player. Requires an active player token. Returns provider_already_linked (409) if that provider account is already linked to a player.

Errors: unauthorized (401), provider_and_token_required (400), provider_already_linked (409).

func sign_out(): void

Clears current_player, sets is_signed_in = false, and emits signed_out. Works locally only — no network call.

func update_player(traits: Dictionary) -> SlateFireResponse

Updates the current player's profile. Accepted fields: display_name (String), avatar_url (String). Requires an active player token. Sends POST /v1/players/update with the traits as the JSON body.

Errors: unauthorized (401).

SlateFire.leaderboards

Submit and read leaderboard scores. Boards are created in the developer dashboard. Methods below are tagged with their backend status.

func submit(board_id: String, score: int, metadata: Dictionary = {}) -> SlateFireResponse Working

Submits a score for the current player. The server keeps the best score per player (uses ON CONFLICT ... DO UPDATE SET score = MAX(score, excluded.score)).

ParameterTypeDescription
board_idStringThe leaderboard ID (created in the dashboard).
scoreintThe score to submit.
metadataDictionaryOptional metadata (currently ignored by the backend — stored and returned as empty {} in the response).

Success: data is a Dictionary with rank, player_id, display_name, score, metadata, is_current_player.

Errors: invalid_api_key, unauthorized, rate_limited, mau_quota, write_quota, service_paused, score_required (400 if score is missing or not a number).

var r := await SlateFire.leaderboards.submit("weekly", 12450) if r.ok: print("Rank: ", r.data.rank)
func top(board_id: String, count: int = 100) -> SlateFireResponse Partial

Returns the top entries for a leaderboard. The count parameter is accepted by the SDK but the backend hard-codes a LIMIT 100 and ignores the count — you always get up to 100 entries regardless of what you pass.

Success: data is an Array of Dictionaries with rank, player_id, display_name, score, metadata, is_current_player.

Errors: invalid_api_key, not_found.

var r := await SlateFire.leaderboards.top("high_scores") for entry in r.data: print(entry.rank, " ", entry.score)
func around_player(board_id: String, radius: int = 5) -> SlateFireResponse Working

Returns up to 2*radius + 1 entries centered around the current player's rank. Requires a signed x-player-token. The player must have a score on the board.

Success: data is an Array of Dictionaries with rank, player_id, display_name, score, metadata, is_current_player.

Errors: invalid_api_key, unauthorized, not_found (if the player hasn't submitted a score on this board).

var r := await SlateFire.leaderboards.around_player("high_scores") for entry in r.data: print(entry.rank, " ", entry.score)
func player_entry(board_id: String) -> SlateFireResponse Working

Returns the current player's score and rank on a leaderboard. Requires a signed x-player-token.

Success: data is a Dictionary with rank, player_id, display_name, score, metadata, is_current_player.

Errors: invalid_api_key, unauthorized, not_found (if the player hasn't submitted a score on this board).

var r := await SlateFire.leaderboards.player_entry("high_scores") if r.ok: print("You are rank ", r.data.rank)
func friends(board_id: String, count: int = 50) -> SlateFireResponse Working

Returns scores for the current player's friends on a leaderboard. Friends are managed with add_friend() and remove_friend(). Requires a signed x-player-token.

Success: data is an Array of Dictionaries with rank, player_id, display_name, score, metadata, is_current_player.

Errors: invalid_api_key, unauthorized.

var r := await SlateFire.leaderboards.friends("high_scores") for entry in r.data: print(entry.score)
func add_friend(friend_id: String) -> SlateFireResponse Working

Adds a friend relationship. The friend_id is the other player's UUID. Requires an active player token.

Errors: unauthorized, friend_id_required (400).

func remove_friend(friend_id: String) -> SlateFireResponse Working

Removes a friend relationship. Requires an active player token.

Errors: unauthorized, friend_id_required (400).

SlateFire.saves

Per-player key/value JSON blobs. The SDK caches reads to user://slatefire/saves/ for offline access.

PropertyTypeDescription
max_bytesintPer-slot size limit for the current plan (default 245760 / 240 KB). Set automatically from config on configure().
func put(key: String, data: Dictionary, expected_version: int = -1) -> SlateFireResponse Working

Writes a JSON save slot. The SDK serializes the Dictionary to JSON, checks the byte size against max_bytes before uploading, and returns save_too_large immediately if it exceeds the limit. The backend tracks version and updated_at via R2 custom metadata. Pass expected_version for optimistic concurrency (returns 409 on mismatch).

ParameterTypeDescription
keyStringSave slot identifier (e.g. "profile", "level_7_state").
dataDictionaryJSON-serializable save data.
expected_versionint (default -1)Optimistic concurrency version. Sends as ?expected_version=N query param. Returns version_conflict (409) if the stored version doesn't match.

Success: data is a Dictionary with key, data, version, updated_at.

Errors: save_too_large (413, checked client-side before request), version_conflict (409), invalid_api_key, unauthorized, rate_limited, service_paused.

var r := await SlateFire.saves.put("profile", { coins = 1200, level = 7 }) if r.ok: print("Saved version ", r.data.version)
func get(key: String) -> SlateFireResponse Working

Reads a save slot. The backend returns a JSON envelope with the parsed data, version, and updated_at. On success, the SDK caches the result to user://slatefire/saves/{key}.json. On failure, it falls back to the cached copy if available.

Success: data is a Dictionary with key, data (parsed JSON Dictionary), version, updated_at.

Errors: invalid_api_key, unauthorized, not_found (404).

var r := await SlateFire.saves.get("profile") if r.ok and r.data: var coins := r.data.data.get("coins", 0)
func list() -> SlateFireResponse Working

Returns an Array of save key strings for the current player. Calls GET /v1/saves, which lists all keys under the player's R2 prefix.

Errors: invalid_api_key, unauthorized.

var r := await SlateFire.saves.list() for key in r.data: print(key)
func delete(key: String) -> SlateFireResponse Working

Deletes a save slot. Also removes the local cache. On success, data is true.

Errors: invalid_api_key, unauthorized, not_found.

await SlateFire.saves.delete("temp_save")

SlateFire.analytics Working

Fire-and-forget event tracking. Events queue locally and are flushed to the backend in batches on a 5-second debounce timer. The backend stores events in D1 with the player's ID.

func track(event: String, props: Dictionary = {}): void

Appends an event to the local queue with the current timestamp. The queue is persisted to user://slatefire/analytics/queue.json. A 5-second debounce timer triggers a batch flush via POST /v1/analytics/batch. Flushed events are cleared from the queue on success.

SlateFire.analytics.track("level_complete", { level = 3, deaths = 2 })
func identify(traits: Dictionary = {}): void

Sends a POST /v1/analytics/identify call to associate traits with the current player. Stubbed to acknowledge the request; player identification can also be managed via auth.update_player().

SlateFire.config Working

Remote feature flags and game balance values. config.fetch() calls GET /v1/config which returns the project's stored config. Falls back to the local cache if the backend is unreachable. Set config values in the dashboard.

Signals

SignalArguments
updatedEmitted after each fetch() call.
func fetch() -> SlateFireResponse

Returns a copy of the cached flags. data is a Dictionary.

func get_bool(key: String, default_value: bool = false) -> bool
func get_int(key: String, default_value: int = 0) -> int
func get_float(key: String, default_value: float = 0.0) -> float
func get_string(key: String, default_value: String = "") -> String
func get_dict(key: String, default_value: Dictionary = {}) -> Dictionary

Each getter checks the cached flags. If the key is missing or the type doesn't match, the default is returned.


HTTP API Reference

For developers integrating without the GDScript SDK, or who want to understand the wire protocol. All endpoints are under the base URL https://slatefire-guard.john-malmin.workers.dev/v1.

Authentication

Every request requires x-api-key (the publishable key). Write operations (POST, PUT, DELETE) also require x-player-token (a signed HMAC token identifying the player).

CORS is open (access-control-allow-origin: *) so browsers can call the API directly.

POST /v1/players/anonymous
Issue a signed player token to an anonymous player. This is the bootstrapping endpoint — no existing player token is needed. Call this first, then use the returned token as the x-player-token header on all subsequent write requests.
x-api-key: publishable key
# cURL: $ curl -X POST "https://slatefire-guard.john-malmin.workers.dev/v1/players/anonymous" \ -H "x-api-key: pk_live_xxxxxxxx"
# Success response (200): { "player_id": "a1b2c3d4-e5f6-...", "token": "eyJzdWIiOiJhMWIyYzNkNC1lNWY2In0.abc123...", "display_name": "Player", "is_anonymous": true, "created_at": "2026-06-05T17:30:00.000Z", "traits": {} }
POST /v1/leaderboards/{board}/submit
Submit or update a player's best score on a leaderboard.
x-api-key: publishable key
x-player-token: signed player token
Content-Type: application/json
# Request body: { "score": 12450 } # cURL example: $ curl -X POST "https://slatefire-guard.john-malmin.workers.dev/v1/leaderboards/weekly/submit" \ -H "x-api-key: pk_live_xxxxxxxx" \ -H "x-player-token: your-token" \ -H "Content-Type: application/json" \ -d '{"score": 12450}'
# Success response (200): { "ok": true, "board": "weekly", "player_id": "uuid-here", "score": 12450 }

The server uses INSERT ... ON CONFLICT DO UPDATE SET score = MAX(score, excluded.score) — only best scores are kept.

GET /v1/leaderboards/{board}/top
Read the top 100 entries for a leaderboard. Hard-coded to LIMIT 100; the count parameter is not supported.
x-api-key: publishable key
# cURL: $ curl "https://slatefire-guard.john-malmin.workers.dev/v1/leaderboards/high_scores/top" \ -H "x-api-key: pk_live_xxxxxxxx"
# Success response (200): { "board": "high_scores", "entries": [ { "player_id": "abc123", "score": 10000 }, { "player_id": "def456", "score": 9500 } ] }
GET /v1/leaderboards/{board}/me
Returns the requesting player's score and rank for a leaderboard.
x-api-key: publishable key
x-player-token: signed player token
# cURL: $ curl "https://slatefire-guard.john-malmin.workers.dev/v1/leaderboards/high_scores/me" \ -H "x-api-key: pk_live_xxxxxxxx" \ -H "x-player-token: your-token"
# Success response (200): { "board": "high_scores", "player_id": "abc123", "score": 5000, "rank": 7 }

Returns 404 if the player hasn't submitted a score on this board yet.

GET /v1/leaderboards/{board}/around?radius=5
Returns up to 2*radius + 1 entries centered around the requesting player's rank.
x-api-key: publishable key
x-player-token: signed player token
# cURL: $ curl "https://slatefire-guard.john-malmin.workers.dev/v1/leaderboards/high_scores/around?radius=3" \ -H "x-api-key: pk_live_xxxxxxxx" \ -H "x-player-token: your-token"
# Success response (200): { "board": "high_scores", "entries": [ { "player_id": "def456", "score": 5200, "rank": 6, "is_current_player": false }, { "player_id": "abc123", "score": 5000, "rank": 7, "is_current_player": true }, { "player_id": "ghi789", "score": 4800, "rank": 8, "is_current_player": false } ] }

Returns 404 if the player hasn't submitted a score on this board yet. The radius defaults to 5 and is capped at 100.

PUT /v1/saves/{key}
Write a save slot. The key is scoped per-project and per-player on the server. Send raw JSON bytes as the request body.
x-api-key: publishable key
x-player-token: signed player token
Content-Type: application/octet-stream
# cURL: $ curl -X PUT "https://slatefire-guard.john-malmin.workers.dev/v1/saves/profile" \ -H "x-api-key: pk_live_xxxxxxxx" \ -H "x-player-token: your-token" \ -H "Content-Type: application/octet-stream" \ -d '{"coins":1200,"level":7}'
# Success response (200): { "ok": true, "key": "profile", "bytes": 24 }

Server-internal key format: {projectId}/{playerId}/{key}. The SDK handles this transparently.

GET /v1/saves/{key}
Read a save slot. Returns the raw bytes as application/octet-stream. No metadata envelope. Requires the player token so saves are properly scoped per-player.
x-api-key: publishable key
x-player-token: signed player token
# cURL: $ curl "https://slatefire-guard.john-malmin.workers.dev/v1/saves/profile" \ -H "x-api-key: pk_live_xxxxxxxx" \ -H "x-player-token: your-token"
# Success response (200): # Raw JSON bytes (Content-Type: application/octet-stream): {"coins":1200,"level":7}

Returns 404 if the slot doesn't exist. Server-internal key format: {projectId}/{playerId}/{key} — same as the write path, so reads and writes address the same slot.

DELETE /v1/saves/{key}
Delete a save slot.
x-api-key: publishable key
x-player-token: signed player token
$ curl -X DELETE "https://slatefire-guard.john-malmin.workers.dev/v1/saves/profile" \ -H "x-api-key: pk_live_xxxxxxxx" \ -H "x-player-token: your-token"
# Success response (200): { "ok": true, "key": "profile" }
GET /v1/usage
Read current usage stats for the project: writes and MAU consumption vs plan limits.
x-api-key: publishable key
$ curl "https://slatefire-guard.john-malmin.workers.dev/v1/usage" \ -H "x-api-key: pk_live_xxxxxxxx"
# Success response (200): { "projectId": "...", "period": "2026-06", "writes": { "used": 15234, "limit": 2000000, "pct": 0.8 }, "maus": { "used": 342, "limit": 5000, "pct": 6.8 } }

Error codes

The backend returns error responses as JSON with an error field. The SDK maps these to SlateFireError.code values.

error.codeHTTP statusMeaning
invalid_api_key401The x-api-key is missing or doesn't match any project.
unauthorized401Write request without a valid x-player-token.
rate_limited429Too many writes too fast. Includes retry-after header. The SDK auto-retries by default.
mau_quota402Monthly active-player cap reached for the project's plan.
write_quota402Monthly write cap reached for the project's plan.
save_too_large413Save data exceeds the plan's per-slot limit or the absolute 8 MB ceiling. The SDK also checks this client-side before uploading.
body_too_large413Request body exceeds the absolute 8 MB limit (enforced before any plan check).
service_paused503Backend is temporarily read-only (panic flag active for free-tier projects). Includes retry-after. The SDK caches this and blocks writes without network calls.
not_found404The requested resource (save slot, leaderboard) doesn't exist.
score_required400Leaderboard submit request body is missing or has a non-numeric score.
conflict409Defined in the SDK for future save version conflicts; not yet returned by the backend.
offline0Network error / no connectivity. SDK-only error code.
timeout0Request exceeded request_timeout_s. SDK-only error code.
not_implemented0Returned by SDK methods whose backend routes don't exist yet.

Concepts

API key vs Player token

SlateFire uses a two-key authentication model:

Publishable API keyPlayer token
What it isA project-level key from the dashboard (e.g. pk_live_xxxxxxxx)An HMAC-SHA256 signed token identifying the current player
Where it goesx-api-key header (every request)x-player-token header (write requests only)
Required forAll reads and writesWrites only (POST, PUT, DELETE)
Set viaSlateFire.configure("pk_...")SlateFire.set_player_token("...")
Who issues itSlateFire dashboardauth.sign_in_anonymous() issues one automatically. You can also call set_player_token() with a hand-minted token for testing.

Why two keys? The API key identifies the project. The player token identifies who is acting. Reads (leaderboard top, save load) need only the project key. Writes (score submit, save put) need both, so the server can enforce per-player rate limits, MAU counting, and panic gating.

Plan limits

Every project has a plan that governs resource limits. The backend enforces them server-side — these are walls, not suggestions.

LimitHobby (free)Indie ($19/mo)Studio ($99/mo)
MAU / month5,00050,000250,000
Writes / month2,000,00050,000,000500,000,000
Max save size256 KB1 MB4 MB
Write rate (per-player)5 / sec, burst 2020 / sec, burst 6050 / sec, burst 150
Panic bypassNo (free projects go read-only during a panic event)YesYes

There is also a hard global ceiling of 8 MB per request body, enforced before any plan check. Beyond that, a global monthly write budget of 20 million acts as a circuit breaker — if crossed, the panic flag is set automatically and free-tier projects are blocked from writing (the cron backstop auto-recovers when the count drops below 50% of budget).

Reads are free Reads (GET requests) are never rate-limited, never counted toward quota, and work even during a panic event. Only writes are metered and gated.