> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frostauth.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Variables

> Read and write app-level and user-level variables via the Frost Auth SDK.

Variables let you store configuration, feature flags, or runtime data in the cloud instead of hardcoding it into your binary. Two scopes exist:

* **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).

All routes require a valid SDK session token (`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:

```js theme={null}
await client.init();
await client.login("alex.dev", "••••••••");

const motd = await client.variable("maintenance_message"); // app scope
const vars = await client.listVars();                       // user scope
const theme = await client.getVar("theme");
await client.setVar("theme", "dark");
```

`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

```http theme={null}
GET /api/v1/sdk/vars/app/:key
```

Reads an operator-defined application variable.

### Parameters

| Field | Required | Description |
| - | - | - |
| `:key` | Yes | Variable name in the URL path (sliced to 128 chars server-side; the SDK sends it as-is). |

### Responses

```json 200 Success theme={null}
{
  "success": true,
  "response": {
    "data": {
      "key": "maintenance_message",
      "value": "Scheduled downtime at midnight UTC"
    }
  }
}
```

```json 403 Forbidden theme={null}
{
  "error": true,
  "response": {
    "code": 403,
    "message": "Please update your app to the latest version",
    "errorCode": "UPDATE_REQUIRED"
  }
}
```

```json 404 Not Found theme={null}
{
  "error": true,
  "response": {
    "code": 404,
    "message": "Variable not found",
    "errorCode": "VARIABLE_NOT_FOUND"
  }
}
```

```json 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Variable unreadable"
  }
}
```

### Behavior

* **Version gate**: if the variable has a `minVersion` and the device's `appVersion` is older, access is refused with `UPDATE_REQUIRED`.
* **Tier gate**: if the variable has a `requiredTier` that doesn't match the license's tier (case-insensitive), access is refused with `TIER_REQUIRED`. There is no `minVersion`/`requiredTier` comparison 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

```http theme={null}
GET /api/v1/sdk/vars/user
```

Returns all variables stored for the authenticated end-user account.

> [!NOTE]
>
> Requires an AppUser-linked session. License-only sessions are refused with `USER_NOT_FOUND` (401).

### Responses

```json 200 Success theme={null}
{
  "success": true,
  "response": {
    "data": {
      "variables": [
        { "key": "theme", "value": "dark", "readonly": false },
        { "key": "credits", "value": "150", "readonly": true }
      ]
    }
  }
}
```

```json 401 Unauthorized theme={null}
{
  "error": true,
  "response": {
    "code": 401,
    "message": "User not found",
    "errorCode": "USER_NOT_FOUND"
  }
}
```

```json 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Variable lookup failed"
  }
}
```

***

## Get User Variable

```http theme={null}
GET /api/v1/sdk/vars/user/:key
```

Reads a single user variable by key.

### Parameters

| Field | Required | Description |
| - | - | - |
| `:key` | Yes | Variable name in the URL path (sliced to 128 chars server-side). |

### Responses

```json 200 Success theme={null}
{
  "success": true,
  "response": {
    "data": {
      "key": "theme",
      "value": "dark",
      "readonly": false
    }
  }
}
```

```json 404 Not Found theme={null}
{
  "error": true,
  "response": {
    "code": 404,
    "message": "Variable not found",
    "errorCode": "VARIABLE_NOT_FOUND"
  }
}
```

```json 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Variable lookup failed"
  }
}
```

Note the 200 shape carries `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

```http theme={null}
PUT /api/v1/sdk/vars/user/:key
Content-Type: application/json
```

Creates or updates a user variable. If the variable already exists and is marked `readonly` by the operator, the write is refused.

### Request

```json theme={null}
{
  "value": "dark"
}
```

### Parameters

| Field | Required | Description |
| - | - | - |
| `:key` | Yes | Variable name in the URL path (sliced to 128 chars server-side). |
| `value` | Yes | Variable value. Maximum 2,048 characters. |

### Responses

```json 200 Success theme={null}
{
  "success": true,
  "response": {
    "data": {
      "key": "theme",
      "value": "dark"
    }
  }
}
```

```json 400 Bad Request theme={null}
{
  "error": true,
  "response": {
    "code": 400,
    "message": "Missing required fields",
    "errorCode": "MISSING_FIELDS"
  }
}
```

```json 403 Forbidden theme={null}
{
  "error": true,
  "response": {
    "code": 403,
    "message": "This variable is read-only",
    "errorCode": "VARIABLE_READONLY"
  }
}
```

```json 500 Server Error theme={null}
{
  "error": true,
  "response": {
    "code": 500,
    "message": "Variable write failed"
  }
}
```

The write response is `{ 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 `readonly` is true, the write is refused with `VARIABLE_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 — `readonly` can only block, never pre-exist.
* **No per-user cap**: there is no variable-count limit — no `USER_VARIABLE_MAX`, no `FORBIDDEN`. Create freely.
* **Upsert semantics**: if the key doesn't exist it's created; if it does, the value is overwritten.

> [!WARNING]
>
> User variable values are stored in plaintext, unlike app variables which are encrypted. Don't store secrets here.

***

## Rate Limits

All four routes share one limiter, plus the usual guards:

| Layer | Limit | Keyed by |
| - | - | - |
| Vars limiter | 120 per 1 min | Hashed token prefix + IP |
| Abuse guard | Escalating block after repeated failures | IP |

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

> [!NOTE]
>
> Operators can reword any message per product — match on `errorCode`, never on message text.

| HTTP | errorCode | Message (default) | Cause |
| - | - | - | - |
| `400` | `MISSING_FIELDS` | `Missing required fields` | PUT with an empty key. Only this route has a 400. |
| `401` | `UNAUTHORIZED` | `Unauthorized access` | No token provided. |
| `401` | `INVALID_TOKEN` | `Invalid token` | Token fails verification. |
| `401` | `INVALID_SESSION` | `Invalid or expired session` | Token revoked, or license/device row gone or mismatched. |
| `401` | `USER_NOT_FOUND` | `User not found` | License-only session on a user-scoped route. |
| `403` | `VARIABLE_READONLY` | `This variable is read-only` | Variable is operator-managed. |
| `403` | `UPDATE_REQUIRED` | `Please update your app to the latest version` | Device version below variable's `minVersion`. |
| `403` | `TIER_REQUIRED` | `This requires a higher tier` | License tier doesn't match variable's `requiredTier` (case-insensitive exact match). |
| `403` | (gate code) | (configured message) | Gate failure from the session check (expiry, blacklist, device BLOCKED/RELEASED). |
| `404` | `VARIABLE_NOT_FOUND` | `Variable not found` | Variable doesn't exist. |
| `429` | `RATE_LIMITED` | `Too many requests…` | Rate limit exceeded. |
| `500` | — | Various | `Variable lookup failed` / `Variable write failed` / `Variable unreadable` (app-var decrypt failure). |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.