Player storage is per-player, per-project key-value storage that persists across sessions. Save games, unlocks, and settings live on the server — reachable from GDScript and from a simple HTTP API for your own tools.

From GDScript

Values are any JSON-serializable Variant (Dictionary, Array, String, numbers, bools):

# Save
await SpawnWeaver.storage_set("save", {"level": 3, "gold": 120, "inventory": ["sword"]})

# Load — value is the stored data, or null when the key was never set
var result := await SpawnWeaver.storage_get("save")
if result.ok and result.value != null:
    var save: Dictionary = result.value
    load_level(save["level"])

# Delete — value is true when the key existed
var deleted := await SpawnWeaver.storage_delete("old_save")

# List this player's keys, optionally by prefix
var keys := await SpawnWeaver.storage_list("slot_")   # value: Array[String]

Storage is scoped to the connected player's identity — every player reads and writes only their own data. There is no client-side way to read another player's storage (do cross-player things with state or events, or server-side over HTTP).

You must be connected (await SpawnWeaver.start()) — storage calls ride the realtime connection.

Typical pattern: load-on-boot, save-on-change

var profile := {"name": "", "best_score": 0}

func _ready() -> void:
    await SpawnWeaver.start()
    var result := await SpawnWeaver.storage_get("profile")
    if result.ok and result.value != null:
        profile = result.value

func record_score(score: int) -> void:
    if score > int(profile.get("best_score", 0)):
        profile["best_score"] = score
        SpawnWeaver.storage_set("profile", profile)   # fine to not await for background saves

Over HTTP

The same data is exposed at /api/storage/{projectId}/players/{playerId}/keys/{key} with two accepted bearer credentials:

Credential Access
Project secret key (sk_…) Any player's data in the project — for your servers, tools, and dashboards only. Never in a game client.
Player token (from the realtime welcome) That player's own data only — safe for a game client's HTTP calls
# Write a value (the request body is the raw JSON value):
curl -X PUT https://spawnweaver.dev/api/storage/proj_xxx/players/player_abc/keys/save \
  -H "Authorization: Bearer sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{"level": 3, "gold": 120}'
# 200 -> { "key": "save", "updatedAtUtc": "2026-07-29T10:00:00Z" }

# Read it back:
curl https://spawnweaver.dev/api/storage/proj_xxx/players/player_abc/keys/save \
  -H "Authorization: Bearer sk_your_secret_key"
# 200 -> { "key": "save", "value": "{\"level\": 3, \"gold\": 120}", "updatedAtUtc": "…" }

# List keys / delete a key:
curl https://spawnweaver.dev/api/storage/proj_xxx/players/player_abc/keys \
  -H "Authorization: Bearer sk_your_secret_key"
# 200 -> { "keys": ["save"] }

curl -X DELETE https://spawnweaver.dev/api/storage/proj_xxx/players/player_abc/keys/save \
  -H "Authorization: Bearer sk_your_secret_key"
# 204 (or 404 if the key didn't exist)

With a player token the same calls work, but only when {playerId} in the URL matches the token's own player — anything else is 401. Full method/response details in the HTTP API reference.

Use the HTTP API for: support tooling ("inspect this player's save"), migration scripts, granting items from a companion service, or web dashboards. Everything in-game should go through the GDScript API.

Quotas

Quota Default Error
Value size 64 KB per key storage-value-too-large (HTTP: 413)
Keys per player 100 storage-quota-exceeded (HTTP: 409)
Key length 128 characters, non-empty storage-invalid-key (HTTP: 400)

When you hit the key quota, delete an old key first — quota errors are not retryable.

What belongs in storage (and what doesn't)

Data Put it in
Save games, progression, unlocks Storage
Player settings you want to roam with the identity Storage
A chosen display name Storage (and set_display_name() each boot)
Live positions, health, game phase State sync — storage is per-player and not broadcast
One-shot actions, chat Events
Data other players must read in-game Room state / entities — storage is private to each player
Local-only caches, screenshots, large binaries Godot user:// files — storage values cap at 64 KB

Storage writes are not rate-limited separately, but they share the connection's message budget (20/s) — batch your saves (one profile dictionary, not thirty tiny keys written per second).

Common mistakes

Mistake What happens Fix
Calling storage before start() not-connected Storage rides the realtime connection
Treating a missing key as an error storage_get succeeds with value == null Check result.value != null, not just result.ok
Using storage as a shared leaderboard Players can't read each other's keys Aggregate server-side over HTTP with the secret key
Shipping sk_… in the game to call the HTTP API Anyone can read/write every player's data Clients use the GDScript API or their own player token
Saving every frame Burns the message budget Save on meaningful changes, or on a timer

Next