DynamoDB-backed Managed KVS
Managed KVS is the DynamoDB-backed database service for Sureva backend apps. It gives apps app-scoped key-value storage without direct DynamoDB access. Managed KVS is regional: each app’s KVS lives in the app’s own region, and your app reaches it at the region-specific URL in SUREVA_KVS_URL (for example https://kvs.sureva.com in eu-central-2 or https://kvs.eu-central-1.sureva.com in eu-central-1). Sureva controls authorization, isolation, rate limits, and metering before any request reaches storage.
Quick path
Section titled “Quick path”- Open the app in the Sureva frontend and check Services → Databases.
- Confirm DynamoDB-backed Managed KVS is available for the app type:
api,web-ssr, orsse. - Create a KVS table from the frontend (for example
sessions). Creating the first table activates the service — there is no separate “enable” step. - Read
SUREVA_KVS_URLand the table’s tokenSUREVA_KVS_TOKEN_<NAME>from the Lambda runtime environment. - Call
/v1/put,/v1/get, and/v1/deletefrom the app Lambda with AWS SigV4 plusX-KVS-Authorization: Bearer <table-token>.
Static web apps cannot use managed KVS because browser-delivered tokens would expose backend storage credentials.
What managed KVS gives you
Section titled “What managed KVS gives you”| Concern | Behavior |
|---|---|
| Storage scope | Values are isolated by organization, app, and environment. |
| Public exposure | The data-plane API is not a public browser API. Production calls must come from approved Sureva-managed app Lambdas. |
| App credentials | App code uses SUREVA_KVS_URL and a table token SUREVA_KVS_TOKEN_<NAME>; it does not receive DynamoDB permissions. |
| Rate limit | A fixed app-environment RPM bucket (default 600 requests/minute) protects the service across all KVS methods. |
| Quota response | When the bucket is exhausted, the API returns 429 with Retry-After. |
How clients discover availability
Section titled “How clients discover availability”Clients discover available databases in the Sureva frontend under the app’s Services → Databases section. Today, the available backend database service is DynamoDB-backed Managed KVS.
The frontend owns provisioning and token delivery. App code should not call control-plane creation endpoints directly; it only consumes the runtime contract after a table is created. Tables can also be managed from the sureva CLI (sureva services kvs tables create, rotate, delete), but that path is not yet fully tested — prefer the frontend for now.
For Lambda-backed app types, Sureva makes these runtime variables available when possible:
| Variable | Meaning |
|---|---|
SUREVA_KVS_URL | Managed KVS data-plane URL for your app’s region — e.g. https://kvs.sureva.com (eu-central-2) or https://kvs.eu-central-1.sureva.com (eu-central-1). Read this value; do not hardcode the host. |
SUREVA_KVS_REGION | AWS region to sign SigV4 for — the KVS gateway region for your app (matches your app’s region, e.g. eu-central-2 or eu-central-1). This can differ from your app’s own AWS_REGION, so always sign with this value. |
SUREVA_KVS_TOKEN_<NAME> | Bearer token for a KVS table. The variable name is the table name uppercased — e.g. SUREVA_KVS_TOKEN_SESSIONS for a table named sessions. Present only when that table exists. Managed KVS is tables-only: there is no default token. |
Call the data plane
Section titled “Call the data plane”Production requests need two authorization layers:
- AWS SigV4 in the standard
Authorizationheader. API Gateway uses this to verify that the caller is an approved Sureva-managed app Lambda role. - KVS bearer token in
X-KVS-Authorization. The proxy uses this token to bind the request to the app’s own organization, app, and environment.
Do not put the KVS token in the standard Authorization header in production. SigV4 owns that header.
Sign with the region from SUREVA_KVS_REGION, not your app’s own AWS_REGION. Managed KVS is regional: your app’s gateway lives in the app’s own region (given by SUREVA_KVS_REGION, for example eu-central-2 or eu-central-1), and your app may run in a different region; a credential scoped to a mismatched region is rejected with 403 (Credential should be scoped to a valid region). The custom domain carries no region hint, so the signer cannot infer it — always take the region from SUREVA_KVS_REGION.
Put a value
Section titled “Put a value”POST /v1/put HTTP/1.1Host: kvs.sureva.comContent-Type: application/jsonX-KVS-Authorization: Bearer <kvs-token>Authorization: AWS4-HMAC-SHA256 ...{ "key": "settings/theme", "value": { "mode": "dark" }, "ttlSeconds": 3600}Response:
{ "ok": true}Get a value
Section titled “Get a value”{ "key": "settings/theme"}Found response:
{ "found": true, "value": { "mode": "dark" }, "expiresAt": "2026-06-21T01:00:00Z"}Missing or expired response:
{ "found": false, "expiresAt": null}Delete a value
Section titled “Delete a value”{ "key": "settings/theme"}Response:
{ "deleted": true}deleted: true means the delete operation completed. It does not mean the key existed before the request.
TypeScript example
Section titled “TypeScript example”This helper signs each request for API Gateway and sends the KVS token in X-KVS-Authorization. Use the equivalent SigV4 signer for your runtime if your app is not TypeScript.
import { Sha256 } from '@aws-crypto/sha256-js';import { defaultProvider } from '@aws-sdk/credential-provider-node';import { HttpRequest } from '@smithy/protocol-http';import { SignatureV4 } from '@smithy/signature-v4';
// Read the region-specific URL and signing region injected by Sureva.// Do not hardcode these — they differ per app region (e.g. eu-central-1 vs eu-central-2).const baseUrl = process.env.SUREVA_KVS_URL;const region = process.env.SUREVA_KVS_REGION;if (!baseUrl || !region) { throw new Error('SUREVA_KVS_URL / SUREVA_KVS_REGION are not set — is managed KVS enabled for this app?');}
// KVS is tables-only: read the token for the table you created.const tableName = 'sessions';const token = process.env[`SUREVA_KVS_TOKEN_${tableName.toUpperCase()}`];
const signer = new SignatureV4({ credentials: defaultProvider(), region, service: 'execute-api', sha256: Sha256,});
async function kvsRequest<T>(path: string, body: Record<string, unknown>): Promise<T> { if (!token) throw new Error(`KVS table "${tableName}" is not enabled for this app`);
const url = new URL(path, baseUrl); const payload = JSON.stringify(body);
const request = new HttpRequest({ protocol: url.protocol, hostname: url.hostname, path: url.pathname, method: 'POST', headers: { host: url.hostname, 'content-type': 'application/json', 'x-kvs-authorization': `Bearer ${token}`, }, body: payload, });
const signed = await signer.sign(request); const response = await fetch(url, { method: 'POST', headers: signed.headers, body: payload, });
if (response.status === 429) { const retryAfter = response.headers.get('retry-after'); throw new Error(`KVS rate limited; retry after ${retryAfter ?? 'a short delay'}`); }
if (!response.ok) { throw new Error(`KVS request failed: ${response.status} ${await response.text()}`); }
return response.json() as Promise<T>;}
export function putKVS(key: string, value: unknown, ttlSeconds?: number) { return kvsRequest<{ ok: true }>('/v1/put', { key, value, ttlSeconds });}
export function getKVS<T>(key: string) { return kvsRequest<{ found: boolean; value?: T; expiresAt: string | null }>('/v1/get', { key });}
export function deleteKVS(key: string) { return kvsRequest<{ deleted: true }>('/v1/delete', { key });}Named table namespaces
Section titled “Named table namespaces”An app can have up to 3 named KVS tables (the default limit). Each table has its own isolated keyspace and token. Use named tables to separate data by purpose — for example, sessions and cache — without interference between them.
When a named table is enabled, Sureva injects SUREVA_KVS_TOKEN_<UPPER(name)> into the Lambda runtime automatically. When the table is deleted, the variable is removed.
To call the data plane for a table, use that table’s token in X-KVS-Authorization. The data-plane URL (SUREVA_KVS_URL) is the same for all tables.
function createTableClient(tableName: string) { const varName = `SUREVA_KVS_TOKEN_${tableName.toUpperCase()}`; const tableToken = process.env[varName]; if (!tableToken) throw new Error(`KVS table '${tableName}' is not enabled for this app`);
return { put: (key: string, value: unknown, ttlSeconds?: number) => kvsRequestWithToken(tableToken, '/v1/put', { key, value, ttlSeconds }), get: <T>(key: string) => kvsRequestWithToken<{ found: boolean; value?: T; expiresAt: string | null }>(tableToken, '/v1/get', { key }), delete: (key: string) => kvsRequestWithToken<{ deleted: true }>(tableToken, '/v1/delete', { key }), };}
// Same as kvsRequest above, but accepts an explicit per-table token.async function kvsRequestWithToken<T>(token: string, path: string, body: Record<string, unknown>): Promise<T> { const url = new URL(path, baseUrl); const payload = JSON.stringify(body); const request = new HttpRequest({ protocol: url.protocol, hostname: url.hostname, path: url.pathname, method: 'POST', headers: { host: url.hostname, 'content-type': 'application/json', 'x-kvs-authorization': `Bearer ${token}` }, body: payload, }); const signed = await signer.sign(request); const response = await fetch(url, { method: 'POST', headers: signed.headers, body: payload }); if (response.status === 429) throw new Error(`KVS rate limited; retry after ${response.headers.get('retry-after') ?? 'a short delay'}`); if (!response.ok) throw new Error(`KVS request failed: ${response.status} ${await response.text()}`); return response.json() as Promise<T>;}
// Usageconst sessions = createTableClient('sessions');await sessions.put('user:123', { role: 'admin' }, 3600);Keys are isolated per table. user:123 in sessions does not conflict with user:123 in another table.
Limits and errors
Section titled “Limits and errors”| Rule | Value |
|---|---|
| Request body | 128 KiB |
| Key | 512 bytes |
| JSON value | 64 KiB |
| Minimum TTL | 60 seconds |
| Maximum TTL | 365 days |
| Rate limit | 600 requests/minute per app environment (default) |
Common service errors use this shape:
{ "error": "invalid_key"}| Status | Meaning |
|---|---|
400 | Invalid JSON, key, value, or TTL. |
401 | Missing or invalid KVS bearer token. |
403 | Inactive binding, unsupported app type, or API Gateway caller rejection. |
413 | Request body is too large. |
429 | Rate limit exceeded. Read Retry-After before retrying. |
500 / 503 | Service-side storage, auth, quota, or dependency failure. |
Checklist
Section titled “Checklist”- The app type is
api,web-ssr, orsse. - At least one KVS table has been created for the app (the first table activates the service).
- App code reads
SUREVA_KVS_URLand a table tokenSUREVA_KVS_TOKEN_<NAME>from runtime env vars. - Requests are SigV4-signed for
execute-apiusingSUREVA_KVS_REGION(the gateway region, not the app’sAWS_REGION). - The KVS token is sent with
X-KVS-Authorization, not the productionAuthorizationheader. -
429responses honorRetry-Afterbefore retrying.