Saltearse al contenido

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.

  1. Open the app in the Sureva frontend and check Services → Databases.
  2. Confirm DynamoDB-backed Managed KVS is available for the app type: api, web-ssr, or sse.
  3. Create a KVS table from the frontend (for example sessions). Creating the first table activates the service — there is no separate “enable” step.
  4. Read SUREVA_KVS_URL and the table’s token SUREVA_KVS_TOKEN_<NAME> from the Lambda runtime environment.
  5. Call /v1/put, /v1/get, and /v1/delete from the app Lambda with AWS SigV4 plus X-KVS-Authorization: Bearer <table-token>.

Static web apps cannot use managed KVS because browser-delivered tokens would expose backend storage credentials.

ConcernBehavior
Storage scopeValues are isolated by organization, app, and environment.
Public exposureThe data-plane API is not a public browser API. Production calls must come from approved Sureva-managed app Lambdas.
App credentialsApp code uses SUREVA_KVS_URL and a table token SUREVA_KVS_TOKEN_<NAME>; it does not receive DynamoDB permissions.
Rate limitA fixed app-environment RPM bucket protects the service across all KVS methods.
Quota responseWhen the bucket is exhausted, the API returns 429 with Retry-After.

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:

VariableMeaning
SUREVA_KVS_URLManaged KVS data-plane URL, normally https://kvs.sureva.com.
SUREVA_KVS_REGIONAWS 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.

Production requests need two authorization layers:

  1. AWS SigV4 in the standard Authorization header. API Gateway uses this to verify that the caller is an approved Sureva-managed app Lambda role.
  2. 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.

POST /v1/put HTTP/1.1
Host: kvs.sureva.com
Content-Type: application/json
X-KVS-Authorization: Bearer <kvs-token>
Authorization: AWS4-HMAC-SHA256 ...
{
"key": "settings/theme",
"value": {
"mode": "dark"
},
"ttlSeconds": 3600
}

Response:

{
"ok": true
}
{
"key": "settings/theme"
}

Found response:

{
"found": true,
"value": {
"mode": "dark"
},
"expiresAt": "2026-06-21T01:00:00Z"
}

Missing or expired response:

{
"found": false,
"expiresAt": null
}
{
"key": "settings/theme"
}

Response:

{
"deleted": true
}

deleted: true means the delete operation completed. It does not mean the key existed before the request.

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 });
}

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>;
}
// Usage
const 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.

RuleValue
Request body128 KiB
Key512 bytes
JSON value64 KiB
Minimum TTL60 seconds
Maximum TTL365 days
Rate limitFixed app-environment RPM bucket

Common service errors use this shape:

{
"error": "invalid_key"
}
StatusMeaning
400Invalid JSON, key, value, or TTL.
401Missing or invalid KVS bearer token.
403Inactive binding, unsupported app type, or API Gateway caller rejection.
413Request body is too large.
429Rate limit exceeded. Read Retry-After before retrying.
500 / 503Service-side storage, auth, quota, or dependency failure.
  • The app type is api, web-ssr, or sse.
  • At least one KVS table has been created for the app (the first table activates the service).
  • App code reads SUREVA_KVS_URL and a table token SUREVA_KVS_TOKEN_<NAME> from runtime env vars.
  • Requests are SigV4-signed for execute-api using SUREVA_KVS_REGION (the gateway region, not the app’s AWS_REGION).
  • The KVS token is sent with X-KVS-Authorization, not the production Authorization header.
  • 429 responses honor Retry-After before retrying.