# RuneStore API Documentation > Secure JSON Storage. Store, version, and retrieve JSON documents via a REST API with optional public-read raw URLs. This document is the canonical reference for the RuneStore HTTP API. It is designed to be machine-readable so an AI assistant can fetch it directly and produce correct client code. Share this URL with an AI: `https:///docs.md` --- ## 1. Concepts - **Bin** — a single JSON document with metadata (`name`, `slug`, `tags`, `visibility`, `collection_id`) and a content field stored as PostgreSQL JSONB. - **Version** — every write (PUT, PATCH, restore) creates a new immutable `store_version`. Versions are numbered starting at 1. - **Visibility** — `private` (requires API key for read) or `public_read` (anyone may GET it without a key; writes still require a key with the right scope). - **Collection** — optional grouping for stores. - **API key** — bearer credential. Keys are shown ONCE at creation and stored hashed (SHA-256 + salt). Each key has a set of scopes. - **Soft delete** — `DELETE /stores/:id` sets `deleted_at`. The store disappears from listings but rows persist. - **Hard delete** — `DELETE /stores/:id?hard=true` (admin keys only) permanently removes the store and its versions. ### Scopes | Scope | Allows | |---|---| | `stores:read` | Read any store (including private) | | `stores:list` | List stores, list collections | | `stores:create` | Create stores and collections | | `stores:update` | PUT/PATCH/restore a store | | `stores:delete` | Soft-delete (and hard-delete if admin) | --- ## 2. Base URL & authentication Base URL: `https:///api/public/v1` All endpoints accept CORS preflight (`OPTIONS`). All requests and responses are JSON (`application/json`) unless noted. Send the API key in one of these headers: ```http Authorization: Bearer rj_live_xxxxxxxxxxxxxxxxxxxx ``` or ```http X-API-Key: rj_live_xxxxxxxxxxxxxxxxxxxx ``` Public-read stores may be fetched WITHOUT a key via `GET /stores/:id`, `GET /stores/:id/latest`, or `GET /stores/:id/raw`. All other endpoints require a key. ### Error format ```json { "error": "message" } ``` Status codes: `400` bad request, `401` unauthorized, `403` forbidden (missing scope), `404` not found, `415` unsupported media type (PATCH), `429` rate limited, `500` internal. ### Rate limits Token bucket per IP and per API key. Default `120/min` per IP, `600/min` per key. Override via env vars `RATE_LIMIT_PER_MIN_IP`, `RATE_LIMIT_PER_MIN_KEY`. ### JSON safety limits | Limit | Default | Env var | |---|---|---| | Max bytes | 1 MB | `MAX_JSON_BYTES` | | Max depth | 40 | `MAX_JSON_DEPTH` | | Max keys | 5000 | `MAX_JSON_KEYS` | | Versions kept per store | 50 | `MAX_VERSIONS_PER_STORE` | Forbidden keys (rejected anywhere in the document): `__proto__`, `constructor`, `prototype`. --- ## 3. Endpoint reference ### Health ```http GET /health ``` → `200 { "ok": true, "time": "2026-06-07T…" }` ### List stores ```http GET /stores?search=&visibility=&collection_id=&tag=&limit=50&offset=0 ``` Scope: `stores:list`. Response: ```json { "stores": [{ "id": "...", "name": "...", "slug": "...", "visibility": "private", "tags": ["a"], "latest_version_number": 3, "updated_at": "..." }], "total": 42 } ``` ### Create store ```http POST /stores Content-Type: application/json ``` Scope: `stores:create`. Body: ```json { "name": "My Config", "slug": "my-config", // optional "description": "…", // optional "visibility": "private", // or "public_read", default "private" "collection_id": "uuid|null", // optional "tags": ["env:prod"], // optional "content": { "any": "json" } // required } ``` → `201` with full store record (includes `id`, `latest_version_number: 1`). ### Get store (with metadata) ```http GET /stores/:id ``` - Private: scope `stores:read`. - Public-read: no key required. ### Get latest content (compact) ```http GET /stores/:id/latest ``` Response: `{ "version": 3, "content": …, "updated_at": "…" }`. ### Get raw content ```http GET /stores/:id/raw ``` Returns just the JSON body with `Content-Type: application/json`. Ideal for direct consumption (e.g. fetch in a frontend, or a config loader). ### Replace store (full write) ```http PUT /stores/:id Content-Type: application/json ``` Scope: `stores:update`. Body is the new content. Creates a new version. ### Patch store ```http PATCH /stores/:id ``` Scope: `stores:update`. The `Content-Type` header determines patch semantics: #### a) JSON Merge Patch — RFC 7396 ```http Content-Type: application/merge-patch+json ``` Body is a partial object. Fields are merged recursively; `null` deletes a field; arrays are replaced wholesale. Example — current content `{"a":1,"b":{"c":2,"d":3}}`, body `{"b":{"c":9,"d":null},"e":5}` → result `{"a":1,"b":{"c":9},"e":5}`. #### b) JSON Patch — RFC 6902 ```http Content-Type: application/json-patch+json ``` Body is an array of ops: `add`, `remove`, `replace`, `move`, `copy`, `test`. Applied atomically — if any op fails (including `test`), no change is persisted. ```json [ { "op": "replace", "path": "/name", "value": "new" }, { "op": "add", "path": "/tags/-", "value": "x" }, { "op": "test", "path": "/version", "value": 1 } ] ``` Any other `Content-Type` returns `415` with `Accept-Patch: application/merge-patch+json, application/json-patch+json`. After a successful patch a new version is created with `patch_meta` capturing `{ patch_type, content_type }`. Patch bodies are NOT logged. ### Delete store ```http DELETE /stores/:id # soft delete (sets deleted_at) DELETE /stores/:id?hard=true # hard delete (admin keys only) ``` Scope: `stores:delete`. → `{ "ok": true }`. ### List versions ```http GET /stores/:id/versions ``` Scope: `stores:read`. Returns versions newest-first with `version_number`, `created_at`, `update_method`, `patch_meta`. ### Get one version ```http GET /stores/:id/versions/:version ``` ### Restore a version ```http POST /stores/:id/versions/:version/restore ``` Scope: `stores:update`. Creates a new version whose content equals the chosen historical version. ### Collections ```http GET /collections # stores:list → { "collections": [...] } POST /collections # stores:create → 201 collection ``` POST body: `{ "name": "...", "slug": "...", "description": "..." }`. ```http GET /collections/:id # detail (scope stores:list) GET /collections/:id/stores # list stores in a collection (scope stores:list) ``` --- ## 4. Quick recipes ### curl: create then update via merge patch ```bash HOST="https://runestore.thor.solutions" KEY="rj_live_xxx" # Create curl -sX POST "$HOST/api/public/v1/stores" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"name":"feature-flags","visibility":"private","content":{"newUi":false,"beta":[]}}' # Merge patch curl -sX PATCH "$HOST/api/public/v1/stores/" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/merge-patch+json" \ -d '{"newUi":true,"beta":["alice"]}' # Public raw fetch (if visibility=public_read) curl -s "$HOST/api/public/v1/stores//raw" ``` ### JavaScript client ```ts const HOST = "https://runestore.thor.solutions"; const KEY = process.env.RUNESTORE_KEY!; export async function getStore(id: string): Promise { const r = await fetch(`${HOST}/api/public/v1/stores/${id}/raw`, { headers: { "X-API-Key": KEY }, }); if (!r.ok) throw new Error(`RuneStore ${r.status}`); return r.json() as Promise; } export async function patchBin(id: string, patch: object) { const r = await fetch(`${HOST}/api/public/v1/stores/${id}`, { method: "PATCH", headers: { "X-API-Key": KEY, "Content-Type": "application/merge-patch+json", }, body: JSON.stringify(patch), }); if (!r.ok) throw new Error(`RuneStore ${r.status} ${await r.text()}`); return r.json(); } ``` ### Python ```python import os, requests HOST = "https://runestore.thor.solutions" KEY = os.environ["RUNEJSON_KEY"] H = {"Authorization": f"Bearer {KEY}"} def get_raw(store_id): r = requests.get(f"{HOST}/api/public/v1/stores/{store_id}/raw", headers=H, timeout=10) r.raise_for_status() return r.json() def json_patch(store_id, ops): r = requests.patch( f"{HOST}/api/public/v1/stores/{store_id}", headers={**H, "Content-Type": "application/json-patch+json"}, json=ops, timeout=10, ) r.raise_for_status() return r.json() ``` --- ## 5. Dashboard Visit `/stores` after signing in at `/auth`. The first account created becomes admin. Generate keys at `/api-keys` — the raw value is shown ONCE. ## 6. Versioning & changelog - API path is `/api/public/v1/`. Breaking changes will ship under `/v2/`. - This document version: 1.0 (2026-06-07).