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. Your app calls the KVS proxy at https://kvs.sureva.com; 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 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.
For Lambda-backed app types, Sureva makes these runtime variables available when possible:
| Variable | Meaning |
|---|---|
SUREVA_KVS_URL | Managed KVS data-plane URL, normally https://kvs.sureva.com. |
SUREVA_KVS_REGION | AWS region to sign SigV4 for — the KVS gateway region (us-east-2). 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. The KVS gateway lives in us-east-2, but your app may run in another region (for example us-east-1); 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.
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';
const baseUrl = process.env.SUREVA_KVS_URL ?? 'https://kvs.sureva.com';const region = process.env.SUREVA_KVS_REGION ?? 'us-east-2';
// 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 2 named KVS tables. Each table has its own isolated keyspace, token, and per-table rate quota. 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 | Fixed app-environment RPM bucket |
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.