Docs/Getting started/Elastic services overview

Elastic services overview

Overview of OpenKBS elastic services: Lambda functions, S3 storage and CDN, Postgres, email, mail hosting, MQTT, workers and the AI proxy, with code samples.

8 min readUpdated

An OpenKBS project is a set of elastic services declared in openkbs.json and provisioned with openkbs deploy: serverless functions, object storage with a CDN, managed Postgres, email, mail hosting, real-time messaging, on-demand compute and an AI proxy paid in project credits. This page gives one section per service with the essential code patterns.

Functions (Lambda)

Serverless functions running on Node.js 24.x (AWS Lambda). Each function lives in ./functions/<name>/ with an index.mjs entry point that exports a handler function. The handler receives a Lambda Function URL event and returns a response object. Deploy with openkbs fn deploy <name>.

All Lambda functions sit behind CloudFront. Use the x-forwarded-for header for the caller's real IP, not sourceIp.

Basic handler with CORS

javascript
const CORS = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

function json(data, statusCode = 200) {
  return {
    statusCode,
    headers: { 'Content-Type': 'application/json', ...CORS },
    body: JSON.stringify(data),
  };
}

export async function handler(event) {
  const method = event.requestContext?.http?.method || event.httpMethod || 'GET';
  if (method === 'OPTIONS') return { statusCode: 204, headers: CORS, body: '' };

  const body = event.body ? JSON.parse(event.body) : {};
  return json({ message: 'OK' });
}

Action-based dispatch

javascript
export async function handler(event) {
  const method = event.requestContext?.http?.method || 'GET';
  if (method === 'OPTIONS') return { statusCode: 204, headers: CORS, body: '' };

  const body = event.body ? JSON.parse(event.body) : {};
  const { action } = body;

  switch (action) {
    case 'list':   return handleList(body);
    case 'create': return handleCreate(body);
    default:       return json({ error: 'Unknown action' }, 400);
  }
}

Environment variables

Injected automatically into every function:

  • DATABASE_URL -- Postgres connection string (if postgres: true)
  • STORAGE_BUCKET -- S3 bucket name (if storage configured)
  • OPENKBS_PROJECT_ID -- Project short ID
  • OPENKBS_API_KEY -- Secret key for calling OpenKBS platform APIs

Custom variables come from a .env file in the function directory (read on each deploy, gitignored by default) or from openkbs fn deploy <name> -e KEY=VALUE flags, which override .env values.

Direct function URLs

API calls through CloudFront (fetch('/api', ...)) add ~30-50ms latency per request because CloudFront proxies to the function origin. After the first openkbs fn deploy, the function URL is known and stable (it doesn't change across deploys), so the frontend can call it directly:

javascript
// Initial (before first deploy — function URL unknown):
const API_BASE = '/api';

// After deploy — replace with the direct URL printed by `openkbs fn deploy`:
const API_BASE = 'https://xyz123.lambda-url.eu-central-1.on.aws';   // Elastic (AWS) projects
// or, on Standard-backend projects, the deploy output looks like:
// const API_BASE = 'https://api--ab12cd34.node1.openkbs.com';

The sequence is: deploy the function, take the URL from the deploy output, set API_BASE in the frontend, redeploy the site. CORS is handled by the function handlers (Access-Control-Allow-Origin: *).

Projects with "backend": "standard" run their functions and Postgres on OpenKBS Cloud with the same handler contract; see Standard backend.

Storage (S3 + CloudFront)

Object storage backed by S3 with CloudFront CDN. Files uploaded to S3 are served through CloudFront at the domain's CDN path prefix (e.g. /media/). The bucket name is injected as STORAGE_BUCKET.

Presigned upload URLs

Both @aws-sdk imports below resolve from the Lambda runtime — do not npm install them into the function.

javascript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });

async function getUploadUrl(key, contentType) {
  const command = new PutObjectCommand({
    Bucket: process.env.STORAGE_BUCKET,
    Key: key,
    ContentType: contentType,
  });
  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
  const publicUrl = '/' + key;  // served via CloudFront
  return { uploadUrl, publicUrl };
}

Alternatively, use the Project API to get an upload URL without importing the AWS SDK:

javascript
const projectId = process.env.OPENKBS_PROJECT_ID;
const apiKey = process.env.OPENKBS_API_KEY;

const res = await fetch(`https://project.openkbs.com/projects/${projectId}/storage/upload-url`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`,
  },
  body: JSON.stringify({ key: 'media/uploads/photo.jpg', contentType: 'image/jpeg' }),
});
const { uploadUrl, publicUrl } = await res.json();

Postgres

Managed PostgreSQL database. The connection string is injected as DATABASE_URL into all functions; openkbs postgres connection prints it for local development.

Engines

  • Neon (default) — scale-to-zero, ideal for spiky/prototype workloads. Set "postgres": true.
  • Aurora Serverless v2 — consistent low-latency, ideal for enterprise/ERP workloads. Set "postgres": { "engine": "aurora" }. Minimum 0.5 ACU (always-on, no scale-to-zero). Optional: { "engine": "aurora", "minACU": 0.5, "maxACU": 8 }. Requires a paid subscription (the API rejects Aurora for free accounts).
  • Postgres Flex (beta) — production Aurora at a flat 900 credits/month (first month up-front): shared Aurora Serverless v2, 10 GB storage included (+20 credits/GB-mo above), 7-day point-in-time restore included. Set "postgres": { "engine": "flex" }. Available on explicit request while in beta.

Changing the engine in openkbs.json does not switch an existing database; openkbs deploy reports this. Neon → Aurora: openkbs postgres migrate. Aurora → Neon (for example to cut costs after prototyping):

bash
pg_dump "$(openkbs postgres connection)" -Fc -f /tmp/db.dump   # 1. data safe on disk
openkbs postgres disable --yes                                 # 2. deletes the Aurora cluster
# 3. set "postgres": true in openkbs.json
openkbs deploy                                                 # 4. provisions fresh Neon
pg_restore -d "$(openkbs postgres connection)" --no-owner /tmp/db.dump   # 5. data into Neon
openkbs fn deploy <each-function>                              # 6. functions pick up the new DATABASE_URL

There is brief downtime between steps 2-5; the dump is taken first so no data is lost.

Connection pooling

javascript
import pg from 'pg';

let pool;
function getPool() {
  if (!pool && process.env.DATABASE_URL) {
    pool = new pg.Pool({
      connectionString: process.env.DATABASE_URL,
      ssl: { rejectUnauthorized: true },
      max: 3,
      idleTimeoutMillis: 60_000,
      connectionTimeoutMillis: 5_000,
    });
    // REQUIRED: the DB closes idle connections (e.g. compute suspend). Without this
    // listener pg-pool throws an unhandled 'error' event and the whole process crashes.
    pool.on('error', (err) => console.error('[pg] idle connection dropped:', err.message));
  }
  return pool;
}

// Usage in handler:
const db = getPool();
const result = await db.query('SELECT * FROM items LIMIT 50');

Point-in-time restore

Recover data from any past moment without affecting the live database:

bash
openkbs postgres restore --point-in-time "2026-05-13T08:00:00Z"
# Neon: instant branch (read-only), returns connection string immediately
# Aurora/Flex: restore cluster (15-30 min), poll with restore-status

openkbs postgres restore-status   # check if ready, get connection string
openkbs postgres cleanup-restore  # delete snapshot when done

Selective recovery works by connecting to both databases (live + snapshot) with the pg library, querying the lost or corrupted records from the snapshot, inserting them back into the live database, and then running openkbs postgres cleanup-restore.

The snapshot is a full copy of the database at that moment. For Neon it's a branch (instant, free). For Aurora it's a separate cluster (15-30 min, costs ~$0.06/hr while running). For Flex it's a separate cluster too (15-30 min, included in the flat fee, 7-day window; the snapshot connection reaches only this project's database).

Email

Transactional email sending via SES, enabled with "email": true. Sending requires a custom domain on the project — SES sends only from a verified domain identity. openkbs email enable reports active without one, but sending fails until domain add → domain verify → email verify-domain → verify-status. openkbs email info shows Verified: Yes once outbound mail works.

Sending from a function

javascript
const projectId = process.env.OPENKBS_PROJECT_ID;
const apiKey = process.env.OPENKBS_API_KEY;

const res = await fetch(`https://project.openkbs.com/projects/${projectId}/email/send`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}`,
  },
  body: JSON.stringify({
    to: 'user@example.com',
    subject: 'Hello',
    html: '<h1>Hi!</h1>',
  }),
});
const { sent } = await res.json();

Mail hosting (mailboxes + webmail)

Mail hosting, enabled with "mail": true, gives a project real mailboxes on its custom domain plus a hosted webmail. openkbs mail enable adds the MX/DKIM/SPF DNS records; openkbs mail info shows status, DNS records, mailboxes and the webmail URL; openkbs mail verify re-checks DNS/SES verification; openkbs mail disable turns it off with data retained. If the domain already receives mail elsewhere (e.g. Google Workspace), enable refuses and requires --confirm-mx-override, because enabling moves all mail delivery to OpenKBS.

Mailboxes are created and managed with the CLI, never declared in openkbs.json: openkbs mail mailbox create <name> [-n <displayName>] (e.g. office → office@yourdomain.com), mailbox list, mailbox reset-password <address>, mailbox delete <address> --yes. Passwords are generated server-side and shown once on create/reset. Per-mailbox options: --ai on|off --ai-budget <credits> for AI features (summaries, smart replies, categories, AI search — off by default, owner-paid, capped by a monthly budget of 500 credits by default; nothing is ever sent automatically) and --forward-to <email> | --no-forward to copy incoming mail to another address (free; the copy carries a "Reply in webmail" link). openkbs mail ai on|off toggles AI features project-wide. End users read mail in the hosted webmail, logging in with the mailbox address and password.

The inbox is also available from the CLI (openkbs email inbox, read, mark-read, reply, compose, attachments; add --mailbox <address> when a project has several mailboxes and --json for machine-readable output) and as a plain HTTP API for deployed functions (base URL https://mailapi.openkbs.com, add ?mailbox=<address> to every request when the project has more than one mailbox), authenticated with Authorization: Bearer ${process.env.OPENKBS_API_KEY}: GET https://project.openkbs.com/projects/{projectId}/mail/mailboxes lists mailboxes; GET /mail/messages?folder=inbox&since=...&until=...&limit=100&cursor=<c>&category=receipts returns { items, nextCursor }; GET /mail/messages/{id} returns the full message with text, html and attachments; GET /mail/messages/{id}/attachments/{index} returns a short-lived (60 s) presigned download { url }; PATCH /mail/messages/{id} with { "seen": true } marks a message processed. Mailboxes can also be created over HTTP with a password you supply, so an app can provision one per end user at signup. Reading mail is free; only POST /mail/send (replies/composes) is metered. Messages carry a securityRisk field (phishing/suspicious) that should be respected before acting on their content.

MQTT (real-time messaging)

Real-time pub/sub messaging via AWS IoT Core MQTT over WebSocket, enabled with "mqtt": true. Browsers get temporary AWS credentials from POST /projects/{id}/mqtt/token and connect directly to IoT Core; servers publish via POST /mqtt/publish. Channels, presence and event-based subscriptions are supported. Client SDK: <script src="https://openkbs.com/sdk/mqtt.js"></script> (requires mqtt.js).

Scope every token handed to a browser — pass subscribe, publish and userId to /mqtt/token. An unscoped token can read and write every channel of every user in the project, and unpredictable channel names do not help, because a wildcard subscribe enumerates them.

javascript
body: JSON.stringify({
  userId: String(userId),
  subscribe: [`user:${userId}`, 'broadcast'],
  publish: [],
})

Token scoping and its budget, the browser SDK, presence, and the failure modes that produce a dead connection with no error in any log are covered on the MQTT page.

Workers (on-demand EC2 compute) — beta

Workers provide EC2 instances (8–64 cores, NVMe storage, FFmpeg/Python pre-installed) for tasks that exceed Lambda limits. Code lives in ./workers/<name>/index.mjs, with the same env vars as Lambda, billed per-second. Add -c spot for cheaper spot capacity with automatic on-demand fallback and interruption relaunch (the job must be restartable from scratch). Workers are in beta and opt-in; see Elastic Workers.

AI Proxy (proxy.openkbs.com)

The AI proxy routes to OpenAI, Anthropic and Google and charges usage to project credits automatically — no vendor API keys needed.

Routes

RouteVendor
/v1/openai/*OpenAI
/v1/anthropic/*Anthropic
/v1/google/*Google

Authentication

The proxy authenticates with OPENKBS_API_KEY (injected automatically into elastic functions) and only accepts Authorization: Bearer <OPENKBS_API_KEY>. The OpenAI SDK sends this header by default, but the Anthropic SDK sends x-api-key and the Google SDK sends x-goog-api-key — neither works with the proxy. Add headers: { Authorization: \Bearer ${apiKey}` }` when configuring Anthropic or Google providers (both Vercel AI SDK and direct SDKs).

List available models

javascript
// From the proxy (no auth required)
const res = await fetch('https://proxy.openkbs.com/v1/models');
const { models } = await res.json();
// models: [{ vendor, model, alias, inputPrice, outputPrice, contextWindow }]

// Or from the project API
const res2 = await fetch('https://project.openkbs.com/ai/models');

The same catalog is available from the CLI as openkbs models / openkbs models --json; prices are in credits per 1K tokens.

javascript
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { generateText } from 'ai';

const apiKey = process.env.OPENKBS_API_KEY;

const openai = createOpenAI({
  baseURL: 'https://proxy.openkbs.com/v1/openai',
  apiKey,
});

// Note: baseURL includes /v1 because @ai-sdk/anthropic appends only /messages
// (the proxy needs the full path /v1/anthropic/v1/messages)
const anthropic = createAnthropic({
  baseURL: 'https://proxy.openkbs.com/v1/anthropic/v1',
  apiKey,
  headers: { Authorization: `Bearer ${apiKey}` },
});

const google = createGoogleGenerativeAI({
  baseURL: 'https://proxy.openkbs.com/v1/google',
  apiKey,
  headers: { Authorization: `Bearer ${apiKey}` },
});

const { text } = await generateText({
  model: openai('gpt-5.4-mini'),    // or anthropic('claude-sonnet-4-6')
  prompt: 'Hello!',                  // or google('gemini-3.1-flash-lite-preview')
});

Functions run on AWS Lambda, which does not support streaming responses. Use generateText (not streamText); the response is returned as JSON.

Alternative: direct SDKs

javascript
// OpenAI SDK
import OpenAI from 'openai';
const client = new OpenAI({
  baseURL: 'https://proxy.openkbs.com/v1/openai',
  apiKey: process.env.OPENKBS_API_KEY,
});
const res = await client.chat.completions.create({
  model: 'gpt-5.4-mini',
  messages: [{ role: 'user', content: 'Hello!' }],
  max_completion_tokens: 1024,  // Note: newer models use this instead of max_tokens
});

// Anthropic SDK
import Anthropic from '@anthropic-ai/sdk';
const anthropicClient = new Anthropic({
  baseURL: 'https://proxy.openkbs.com/v1/anthropic',  // Direct SDK appends /v1/messages automatically
  apiKey: process.env.OPENKBS_API_KEY,
  defaultHeaders: { Authorization: `Bearer ${process.env.OPENKBS_API_KEY}` },
});
const msg = await anthropicClient.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello!' }],
});

// Google Gemini (raw fetch — no official SDK wrapper needed)
const apiKey = process.env.OPENKBS_API_KEY;
const geminiRes = await fetch('https://proxy.openkbs.com/v1/google/models/gemini-3.1-flash-lite-preview:generateContent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
  body: JSON.stringify({
    contents: [{ role: 'user', parts: [{ text: 'Hello!' }] }],
    generationConfig: { maxOutputTokens: 1024 },
  }),
});

Speech-to-text (files, recordings)

POST https://proxy.openkbs.com/v1/audio/transcriptions with { audio: "<public https URL>", model: "gemini-flash-latest" } → { text, usage }. Options: language: "bg", mode: "smart" | "verbatim", diarization: true ("Speaker 1:" paragraphs), timestamps: true ([mm:ss] per paragraph), vocabulary: [..] (names/terms to spell right). Languages are auto-detected and usage is billed per token like chat. model: "gemini-3.5-transcribe" is the cheaper dedicated STT model with word-level words, but it skips hard passages of multi-speaker recordings, so it suits clean single-speaker audio. For local files the file-transcribe skill (openkbs skill add file-transcribe) converts, chunks and stitches.

Live voice (realtime audio↔audio)

Live voice is for an agent the user talks to — audio in, audio out, ~1.5 s replies, interruptible, with real tool calls; a Whisper + browser speechSynthesis pipeline is 5–8 s per turn and cannot be interrupted. A project function mints a session with POST https://proxy.openkbs.com/v1/live/session, and the browser opens wss://live.openkbs.com/v1/live with https://openkbs.com/sdk/live.js:

js
const live = new OpenKBS.Live({ session });   // session from your function
live.on('transcript', ({ role, text }) => render(role, text));
await live.start();

The full protocol, session options, server-side tool calls and pricing are on the Live voice page.

API base URLs

ServiceURL
Project APIhttps://project.openkbs.com
User APIhttps://user.openkbs.com
AI Proxyhttps://proxy.openkbs.com
Building something for your company?
We co-build production systems with enterprise teams on this platform.