Skip to content

Storage reference

Citadel’s storage engine is a typed collection/key/owner object store — the same domain model backing device saves, unlocks, and any other structured per-player or system data — persisted through the node’s real backend (in-memory, SQLite, or Postgres). It is the same store the storage browser exposes to operators.

Concept Type Notes
Owner Owner::System | Owner::User(UserId) System is server/runtime-owned data with no end user; User belongs to one account.
Collection Collection (validated string) A namespace within an owner. Non-empty, ≤128 bytes, no control characters.
Key Key (validated string) Unique within owner + collection. Same validation as Collection.
Object identity ObjectId { owner, collection, key } The full address of one stored object.
Value StorageValue (JSON object) Must be a top-level JSON object — arrays, strings, and bare numbers are rejected.
Version Version (opaque string) Content-addressed: two writes with identical JSON produce the same version, so a previously read version works as an optimistic-concurrency (If-Match-style) precondition.
Permissions Permissions { read, write } See below.

Read permission codes (mirrors Nakama’s numbering):

Code Name Meaning
0 NoRead Only the runtime-authoritative path may read.
1 OwnerRead The runtime and the owning user may read.
2 PublicRead Anyone, including unauthenticated callers, may read.

Write permission codes:

Code Name Meaning
0 NoWrite Only the runtime-authoritative path may write.
1 OwnerWrite The runtime and the owning user may write.

Console operations run as the runtime accessor (Accessor::Runtime): object permissions never block a console read/write — the bearer token and console role (admin/viewer) are the only gate. Ownership on every route is selected with an optional user_id query parameter; when it is absent, the route addresses the system owner.

Values are JSON objects only — there is no opaque/binary value type in the console API today. A JSON body up to 512 KiB is accepted per object through these routes (bigger than the router-wide console body limit, since storage values can be larger than typical admin payloads).

List every collection with its total object count.

  • Auth: bearer token, any role (admin or viewer).
  • Params: none.
  • Response 200:
{
"collections": [
{ "collection": "saves", "objects": 42 },
{ "collection": "unlocks", "objects": 7 }
]
}
  • Errors: 401 authentication_failed (missing/invalid/expired token).
Terminal window
curl -s http://127.0.0.1:7350/console/v1/storage \
-H "Authorization: Bearer $TOKEN"

Paged object summaries for one collection — key, owner, version, and permission codes, but not the value itself.

  • Auth: bearer token, any role.
  • Path param: collection — the collection name.
  • Query params:
Param Type Default Meaning
user_id string absent = system owner Restrict to one owning user; absent lists across all owners.
limit integer 50 Page size, capped at 200.
cursor string none Opaque resume token from a previous page’s next.
  • Response 200:
{
"collection": "saves",
"items": [
{ "user_id": "u-1", "key": "slot-1", "version": "0a1b2c3d4e5f6789",
"read_permission": 1, "write_permission": 1 }
],
"next": "opaque-cursor-token"
}

next is omitted once there are no more pages.

  • Errors: 400 invalid_request (malformed collection/user_id/query), 401 authentication_failed.
Terminal window
curl -s "http://127.0.0.1:7350/console/v1/storage/saves?user_id=u-1&limit=50" \
-H "Authorization: Bearer $TOKEN"

GET /console/v1/storage/{collection}/{key}

Section titled “GET /console/v1/storage/{collection}/{key}”

Read one full object: value, version, and permission codes.

  • Auth: bearer token, any role.
  • Path params: collection, key.
  • Query params: user_id (optional; absent = system owner).
  • Response 200:
{
"collection": "saves",
"user_id": "u-1",
"key": "slot-1",
"value": { "hp": 87, "level": 4 },
"version": "0a1b2c3d4e5f6789",
"read_permission": 1,
"write_permission": 1
}
  • Errors: 400 invalid_request (malformed identity), 401 authentication_failed, 404 not_found (no object at that address).
Terminal window
curl -s "http://127.0.0.1:7350/console/v1/storage/saves/slot-1?user_id=u-1" \
-H "Authorization: Bearer $TOKEN"

PUT /console/v1/storage/{collection}/{key}

Section titled “PUT /console/v1/storage/{collection}/{key}”

Create or overwrite an object. Admin only.

  • Auth: bearer token, admin role (viewer gets 403).
  • Path params: collection, key.
  • Query params: user_id (optional; absent = system owner).
  • Body:
{
"value": { "hp": 100 },
"read_permission": 1,
"write_permission": 1,
"version": "0a1b2c3d4e5f6789"
}
Field Type Required Meaning
value JSON object yes The value to store. Must be a JSON object (not an array, string, or number).
read_permission integer 0|1|2 no (default 1, owner-private) See the read permission table above.
write_permission integer 0|1 no (default 1, owner-private) See the write permission table above.
version string no Optimistic-concurrency precondition: the write only succeeds if the object’s current version equals this token. Omit for an unconditional upsert.

Unknown body fields are rejected (deny_unknown_fields).

  • Response 200 OK: the full object, same shape as the GET above, with the freshly computed version.
  • Errors: 400 invalid_request (malformed body, bad permission code, value not a JSON object), 401 authentication_failed, 403 forbidden (viewer), 409 conflict (the version precondition did not match the object’s current version).
  • Audit: recorded as storage.write, target {collection}/{key} (user {user_id}) or (system), detail wrote version {version}.
Terminal window
curl -s -X PUT "http://127.0.0.1:7350/console/v1/storage/saves/slot-1?user_id=u-1" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": {"hp": 100}, "read_permission": 1, "write_permission": 1}'

DELETE /console/v1/storage/{collection}/{key}

Section titled “DELETE /console/v1/storage/{collection}/{key}”

Delete an object. Admin only. Idempotent.

  • Auth: bearer token, admin role (viewer gets 403).
  • Path params: collection, key.
  • Query params: user_id (optional; absent = system owner), version (optional precondition — the delete only succeeds if it matches the object’s current version).
  • Response: 204 No Content.
  • Errors: 400 invalid_request (malformed identity), 401 authentication_failed, 403 forbidden (viewer), 409 conflict (version precondition mismatch).
  • Audit: recorded as storage.delete, same target format as the write, detail deleted object.
Terminal window
curl -s -X DELETE "http://127.0.0.1:7350/console/v1/storage/saves/slot-1?user_id=u-1" \
-H "Authorization: Bearer $TOKEN"

The embedded Lua runtime (see Lua runtime API) now exposes user-owned storage from trusted game logic: citadel.storage_read, citadel.storage_write, citadel.storage_delete, citadel.storage_index_query, and citadel.register_storage_index_filter. Searching an operator-declared index is trusted game-logic work, not a generic client endpoint — the console API on this page and the runtime accessors share the same durable objects, versions, and permission model. Python and JavaScript expose the same storage accessors.

Storage routes use the console API’s shared JSON error shape (see Admin console & console API — Errors):

{ "code": "invalid_request", "message": "storage value must be a JSON object" }
Status Code When
400 invalid_request Malformed collection/key/user id, bad permission code, non-object value.
401 authentication_failed Missing, invalid, or expired bearer token.
403 forbidden A viewer attempted PUT/DELETE.
404 not_found GET for an object that does not exist.
409 conflict version precondition did not match the current object version.
  • Console-admin-only. No client-facing storage API exists yet; see the status callout above.
  • Runtime storage is trusted-tier. Lua, Python, and JavaScript reach storage through the accessors in Lua runtime storage access above; there is still no direct game-client storage endpoint.
  • Values are JSON objects only. There is no opaque/binary value type or content-type metadata in the current contract.
  • For the full section walkthrough (roles, login, and how Storage sits among the other console sections), see Admin console & console API.