- App variables — set by the operator in the dashboard, read-only from the SDK. Encrypted at rest with AES-256-GCM keyed per product.
- User variables — per-account key-value pairs, stored in plaintext. The SDK can read and write them unless the operator marked a variable as
readonly(server-writable only, for things like credit balances or ranks).
Authorization: Bearer <token> header, token body field, or ?token= query on GET). The JS SDK handles this — variable(), listVars(), getVar(), and setVar() need an active session first, otherwise the SDK throws NOT_ACTIVATED before any request is sent:
variable() and getVar() unwrap to the raw value (just the string). listVars() returns the { key, value, readonly } array. setVar() returns the raw response data ({ key, value } — no readonly echo).
Get App Variable
Parameters
Responses
Behavior
- Version gate: if the variable has a
minVersionand the device’sappVersionis older, access is refused withUPDATE_REQUIRED. - Tier gate: if the variable has a
requiredTierthat doesn’t match the license’s tier (case-insensitive), access is refused withTIER_REQUIRED. There is nominVersion/requiredTiercomparison beyond that — exact tier match, nothing hierarchical. - Decryption failure: if the master encryption key was rotated and the ciphertext is unreadable, the server answers 500
Variable unreadable(not 404) — recoverable by the operator re-saving the value.
List User Variables
Requires an AppUser-linked session. License-only sessions are refused with
USER_NOT_FOUND (401).Responses
Get User Variable
Parameters
Responses
readonly too — the list shape and the single-get shape match. There is no key-format validation on these routes — a missing variable and a malformed key give the same 404. No 400 exists here.
Set User Variable
readonly by the operator, the write is refused.
Request
Parameters
Responses
{ key, value } only — no readonly echo. The only 400 is an empty key after slicing — the value is never missing-required, it’s coerced (String(value ?? ""), sliced to 2,048) and upserted.
Behavior
- Readonly check: if the variable exists and
readonlyis true, the write is refused withVARIABLE_READONLY. This is how operators store values the app may read but must not forge (ranks, credit balances, flags). A missing variable is always writable —readonlycan only block, never pre-exist. - No per-user cap: there is no variable-count limit — no
USER_VARIABLE_MAX, noFORBIDDEN. Create freely. - Upsert semantics: if the key doesn’t exist it’s created; if it does, the value is overwritten.
Rate Limits
All four routes share one limiter, plus the usual guards:
No separate read/write limits — GETs and the PUT share the 120/min bucket. The limiter answers
Too many requests, slow down with errorCode: "RATE_LIMITED"; the abuse guard adds retryAfterSeconds.
Signed responses (v2 Ed25519 scheme) and requireSignedRequests freshness behave exactly as on login.
Errors
Operators can reword any message per product — match on
errorCode, never on message text.