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.
Auth endpoints not yet deployed The SDK defines auth.sign_in_anonymous(), auth.register_email(), etc. but the backend has no player-auth routes yet. These methods return not_implemented. Use set_player_token() with a pre-issued token for testing.

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. sign_in_anonymous() works end-to-end: it calls POST /v1/players/anonymous, gets back a signed player token, and automatically sets it on the HTTP client for all subsequent requests. Other sign-in methods (register_email, sign_in_email, etc.) return not_implemented — the backend doesn't have those routes yet.

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

not_implemented

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

not_implemented

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

Where provider is e.g. "steam", "google", "discord", "apple".

not_implemented

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

not_implemented

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

not_implemented

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

not_implemented

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 Coming soon

not_implemented — no backend endpoint exists.

func player_entry(board_id: String) -> SlateFireResponse Coming soon

not_implemented — no backend endpoint exists.

func friends(board_id: String, count: int = 50) -> SlateFireResponse Coming soon

not_implemented — no friends/social system exists on the backend.

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 Partial

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.

ParameterTypeDescription
keyStringSave slot identifier (e.g. "profile", "level_7_state").
dataDictionaryJSON-serializable save data.
expected_versionint (default -1)Optimistic concurrency version. Not yet supported by the backend — currently ignored.

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

Errors: save_too_large (413, checked client-side before request), 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)
Backend shape mismatch The backend stores raw bytes with no versioning or timestamps. The SDK wraps the backend response into the spec's SaveSlot shape, but version will be 0 and updated_at will be empty until the backend adds an envelope.
func get(key: String) -> SlateFireResponse Partial

Reads a save slot. 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. Returns ok: true, data: null if the slot doesn't exist? Actually, the backend returns 404 for missing saves, which the SDK propagates as an error. If cached, it returns the cached copy.

Errors: invalid_api_key, 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 Coming soon

not_implemented — no backend endpoint exists to list saves.

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 Coming soon

Fire-and-forget event tracking. Events queue to user://slatefire/analytics/queue.json but are never flushed — the backend has no analytics endpoint yet.

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

Appends an event to the local queue with the current timestamp. No network call. The queue persists across sessions.

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

No-op. Stubbed for future use.

SlateFire.config Local only

Remote feature flags and game balance values. The backend has no config endpoint — config.fetch() returns whatever is cached locally (an empty Dictionary until you populate it).

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 } ] }
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 dashboardYour own trusted system (hand-minted for testing; future auth.sign_in_* methods will issue them automatically)

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.