بنقرة واحدة
openkbs
OpenKBS platform CLI, elastic services, and AI proxy
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
OpenKBS platform CLI, elastic services, and AI proxy
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | openkbs |
| description | OpenKBS platform CLI, elastic services, and AI proxy |
openkbs login # Browser-based login (interactive users)
openkbs auth <token> # Authenticate with project JWT (containers)
openkbs logout # Clear stored credentials
openkbs list # List all projects (alias: ls)
openkbs create [name] -r <region> # Create + scaffold project
openkbs deploy # Build site + deploy all services from openkbs.json
openkbs update # Update CLI binary + download latest skill
openkbs ui # Start web UI for local development (port 3000)
openkbs ui -p 8080 # Custom port
openkbs ui --no-open # Don't auto-open browser
openkbs fn create <name> # Scaffold new function (creates dir + updates openkbs.json)
openkbs fn list # List deployed functions (alias: ls)
openkbs fn deploy <name> # Zip ./functions/<name>/ and deploy
openkbs fn logs <name> # Tail recent logs
openkbs fn invoke <name> -d '{"action":"hello"}' # Invoke with JSON payload
openkbs fn destroy <name> # Delete function
Options for fn deploy:
-s, --schedule <expr> -- Schedule expression, e.g. "rate(1 hour)" or "cron(0 9 * * ? *)"-m, --memory <mb> -- Memory in MB (default from openkbs.json)-t, --timeout <sec> -- Timeout in seconds--no-http -- Disable HTTP access (function URL)openkbs site deploy # Deploy ./build/ to S3 + CloudFront
openkbs storage list [prefix] # List objects (alias: ls)
openkbs storage upload <local> [remote] # Upload a file
openkbs storage download <remote> [local] # Download a file
openkbs storage rm <keys...> # Delete objects
openkbs postgres info # Show host, database, user
openkbs postgres connection # Output full connection string
openkbs mqtt info # Show MQTT status and endpoint
openkbs mqtt enable # Enable MQTT for this project
openkbs mqtt disable # Disable MQTT
openkbs mqtt token [-u userId] # Generate temporary client credentials
openkbs mqtt publish <channel> -d '<json>' # Publish event to channel
openkbs email enable # Enable email sending for this project
openkbs email info # Show email status and usage
openkbs email send <to> -s <subject> -b <body> # Send email
openkbs email disable # Disable email
openkbs email verify-domain <domain> # Start domain verification for custom sender
openkbs email verify-status # Check verification status
openkbs domain add <domain> # Register custom domain (e.g. example.com)
openkbs domain verify # Check DNS records and certificate status
openkbs domain provision # Create CloudFront distribution for domain
openkbs domain info # Show current domain configuration
openkbs domain remove # Remove custom domain
./openkbs.json # Project config (services, region, functions, target)
./package.json # Root package.json (React, Vite)
./vite.config.js # Vite config (dev server, API proxy, build output)
./src/ # React source code (edit here)
index.html # HTML entry point
main.jsx # React entry point
App.jsx # Root component
./build/ # Vite build output (auto-generated, deployed to S3)
./functions/ # Each subfolder = one Lambda function
api/
index.mjs # Entry point (export handler)
package.json # Optional dependencies (bundled on deploy)
The target field in openkbs.json controls where deployments go:
| Local | Cloud | |
|---|---|---|
| Functions | LocalStack Lambda | AWS Lambda |
| Storage | LocalStack S3 | AWS S3 + CloudFront |
| Database | Docker PostgreSQL | Neon PostgreSQL |
| MQTT | Not available | AWS IoT Core |
| Not available | AWS SES | |
| Target | "target": "local" | "target": "cloud" |
openkbs deploy automatically runs npm run build before deploying the site.
{
"projectId": "a0ebcf5d1fa5",
"region": "us-east-1",
"target": "local",
"postgres": true,
"storage": { "cloudfront": "media" },
"mqtt": true,
"email": true,
"functions": [
{ "name": "api", "runtime": "nodejs24.x", "memory": 512, "timeout": 30 },
{ "name": "auth", "runtime": "nodejs24.x", "memory": 512, "timeout": 30 },
{ "name": "cleanup", "schedule": "rate(1 hour)", "timeout": 900 }
],
"site": "./build",
"spa": "/app/index.html"
}
| Field | Description |
|---|---|
projectId | Project short ID (auto-set on create) |
region | AWS region for all resources |
target | "local" for LocalStack, "cloud" for AWS |
postgres | true to provision PostgreSQL |
storage | Object with cloudfront prefix for CDN distribution |
mqtt | true to enable real-time WebSocket messaging (cloud only) |
email | true to enable email sending via SES (cloud only) |
functions | Array of function definitions to deploy |
site | Path to build output directory |
spa | SPA fallback path (all 404s redirect here) |
Serverless functions running on Node.js 24.x (AWS Lambda) or Node.js 20.x (LocalStack).
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.
Creating a new function:
openkbs fn create payments # Creates functions/payments/ with handler template
# Also adds it to openkbs.json functions array
Environment variables injected automatically:
DATABASE_URL -- Postgres connection string (if postgres: true)STORAGE_BUCKET -- S3 bucket name (if storage configured)OPENKBS_PROJECT_ID -- Project short IDOPENKBS_API_KEY -- Secret key for calling OpenKBS platform APIs (cloud only)Deploy: openkbs fn deploy <name>
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/).
Managed PostgreSQL database. Connection string is injected as DATABASE_URL into all functions.
postgresql://postgres:openkbs@localhost:5432/openkbs (Docker container)Use openkbs postgres connection to get the string.
Real-time pub/sub messaging via AWS IoT Core MQTT over WebSocket. Clients get temporary AWS credentials from POST /projects/{id}/mqtt/token, then connect directly to IoT Core. Supports channels, presence, and event-based subscriptions. Client SDK: <script src="https://openkbs.com/sdk/mqtt.js"></script> (requires mqtt.js).
Security: MQTT credentials allow pub/sub on ALL channels within the project. For private/sensitive channels, use unpredictable channel names (e.g., crypto.randomBytes(32).toString('hex')). For sensitive data, publish only from the server via POST /projects/{id}/mqtt/publish.
| Service | URL |
|---|---|
| Project API | https://project.openkbs.com |
| User API | https://user.openkbs.com |
| AI Proxy | https://proxy.openkbs.com |
The project template uses React + Vite with hot module replacement (HMR).
./src/ (React JSX components)./build/ (auto-generated by npm run build)openkbs deploy automatically runs npm run build before deployingAdding new pages/components: Create new .jsx files in ./src/ and import them in App.jsx.
API proxy: In local mode, the Vite dev server automatically proxies requests to function endpoints. For example, if you have a function named api, requests to /api are proxied to the Lambda function running in LocalStack. No configuration needed — the proxy reads openkbs.json dynamically.
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' });
}
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);
}
}
import pg from 'pg';
const db = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 1,
idleTimeoutMillis: 120000,
});
let dbInitialized = false;
async function connectDB() {
if (!dbInitialized) {
dbInitialized = true;
await db.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
`);
}
}
// Usage in handler:
export async function handler(event) {
await connectDB();
const result = await db.query('SELECT * FROM users LIMIT 50');
return json({ users: result.rows });
}
Note: Use max: 1 for Lambda — each invocation gets its own connection. Use CREATE TABLE IF NOT EXISTS for auto-migration on first deploy.
import crypto from 'crypto';
// Register
const privateChannel = crypto.randomBytes(32).toString('hex'); // for private MQTT messaging
const result = await db.query(
'INSERT INTO users (name, email, password, private_channel) VALUES ($1, $2, $3, $4) RETURNING *',
[name, email, password, privateChannel]
);
// Login
const result = await db.query(
'SELECT * FROM users WHERE email = $1 AND password = $2',
[email, password]
);
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(fileName, contentType) {
const bucket = process.env.STORAGE_BUCKET;
const timestamp = Date.now();
const safeName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
const key = `media/uploads/${timestamp}-${safeName}`;
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType || 'image/jpeg',
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
const publicUrl = '/' + key; // served via CloudFront
return { uploadUrl, publicUrl, key };
}
Alternatively, use the Project API to get an upload URL without importing the AWS SDK:
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();
async function uploadImage(file) {
// Get presigned URL from your function
const res = await fetch('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'getUploadUrl', fileName: file.name, contentType: file.type }),
});
const { uploadUrl, publicUrl } = await res.json();
// Upload directly to S3
await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
return publicUrl;
}
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();
MQTT uses AWS IoT Core MQTT over WebSocket. Clients connect directly to IoT Core (no proxy). The platform provides:
POST /projects/{id}/mqtt/token — temporary STS credentials (15 min, scoped to project topics)POST /projects/{id}/mqtt/publish — server-side publish (metered, billed)mqtt.js) — browser SDK with channels, presence, auto-reconnectData flow:
Browser → SigV4-signed WebSocket → AWS IoT Core (managed MQTT broker)
Server → POST /mqtt/publish → Lambda → IoT Core → all subscribers
Presence uses MQTT Last Will and Testament (LWT) for auto-leave on disconnect.
const projectId = process.env.OPENKBS_PROJECT_ID;
const apiKey = process.env.OPENKBS_API_KEY;
const res = await fetch(`https://project.openkbs.com/projects/${projectId}/mqtt/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ userId: String(userId) }),
});
// Returns: { iotEndpoint, region, topicPrefix, clientIdPrefix, credentials: { accessKeyId, secretAccessKey, sessionToken } }
const mqttData = await res.json();
await fetch(`https://project.openkbs.com/projects/${projectId}/mqtt/publish`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({ channel: 'posts', event: 'new_post', data: { id: 1, title: 'Hello' } }),
});
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<script src="https://openkbs.com/sdk/mqtt.js"></script>
const realtime = new MQTT.Realtime({
credentials: mqttData.credentials,
iotEndpoint: mqttData.iotEndpoint,
region: mqttData.region,
topicPrefix: mqttData.topicPrefix,
clientIdPrefix: mqttData.clientIdPrefix,
clientId: 'user-123',
debug: false
});
// Connection events
realtime.connection.on('connected', () => console.log('online'));
realtime.connection.on('disconnected', () => console.log('offline'));
// Channels
const channel = realtime.channels.get('posts');
channel.subscribe('new_post', (msg) => console.log(msg.data));
channel.publish('greeting', { text: 'Hello!' });
// Presence
channel.presence.enter({ name: 'Alice' });
channel.presence.subscribe((members) => console.log(members));
channel.presence.leave();
// Cleanup
realtime.close();
For secure private messaging, each user gets an unpredictable private channel:
// Server: generate private channel on registration
const privateChannel = crypto.randomBytes(32).toString('hex');
await db.query('INSERT INTO users (name, private_channel) VALUES ($1, $2)', [name, privateChannel]);
// Server: send message by publishing to recipient's private channel
const recipient = await db.query('SELECT private_channel FROM users WHERE id = $1', [toUserId]);
await mqttPublish(recipient.rows[0].private_channel, 'new_message', { fromUserId, content: message });
// Client: subscribe to own private channel (only this user knows it)
const privateChannel = realtime.channels.get(user.privateChannel);
privateChannel.subscribe('new_message', (msg) => console.log('New message:', msg.data));
crypto.randomBytes(32).toString('hex')/mqtt/publishFor larger apps, split logic into separate functions:
functions/
auth/ # User registration, login, token management
index.mjs
package.json
posts/ # Content CRUD, image uploads
index.mjs
package.json
payments/ # Payment processing
index.mjs
package.json
Create new functions with openkbs fn create <name>. Each function:
package.json for dependenciesopenkbs fn deploy <name>/<functionName> to the right LambdaFrontend API calls:
// Each function is accessible at /<functionName>
await fetch('/auth', { method: 'POST', body: JSON.stringify({ action: 'login', email, password }) });
await fetch('/posts', { method: 'POST', body: JSON.stringify({ action: 'list' }) });
await fetch('/payments', { method: 'POST', body: JSON.stringify({ action: 'charge', amount: 100 }) });
This section shows how all pieces connect to build a real-time app with auth, data, file uploads, and live updates.
Frontend (React + Vite)
├── /auth → functions/auth/ → Postgres (users table)
├── /posts → functions/posts/ → Postgres (posts, messages tables) + S3 (images)
└── MQTT SDK → AWS IoT Core → Real-time updates (posts, private messages, presence)
Registration:
{ action: 'register', name, email, password } to /authprivate_channel (crypto.randomBytes)/mqtt/token to get temporary MQTT credentials{ user: { id, name, privateChannel, mqtt: { credentials, iotEndpoint, ... } } }localStorage, connects to MQTTLogin (page reload):
localStorage on mount/auth with { action: 'refreshMqtt', userId }posts) and private channel (user.privateChannel)Creating a post:
{ action: 'create', content, userId, userName } to /postsposts tableposts channel with the new post datasetPosts(prev => [newPost, ...prev])Sending a private message:
{ action: 'sendMessage', toUserId, message, fromUserId, fromUserName } to /postsprivate_channel from users tablemessages tablePresence (who's online):
posts channel// functions/auth/index.mjs
import pg from 'pg';
import crypto from 'crypto';
const db = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 1, idleTimeoutMillis: 120000 });
let dbReady = false;
async function initDB() {
if (dbReady) return;
dbReady = true;
await db.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
private_channel VARCHAR(64) UNIQUE NOT NULL,
avatar_color VARCHAR(7) DEFAULT '#007bff',
created_at TIMESTAMP DEFAULT NOW()
)
`);
}
const COLORS = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50', '#ff9800', '#ff5722'];
async function getMqttToken(userId) {
const projectId = process.env.OPENKBS_PROJECT_ID;
const apiKey = process.env.OPENKBS_API_KEY;
if (!projectId || !apiKey) return null;
try {
const res = await fetch(`https://project.openkbs.com/projects/${projectId}/mqtt/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify({ userId: String(userId) }),
});
const data = await res.json();
return data.error ? null : data;
} catch { return null; }
}
const CORS = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type' };
const json = (data, statusCode = 200) => ({ statusCode, headers: CORS, body: JSON.stringify(data) });
export async function handler(event) {
if (event.requestContext?.http?.method === 'OPTIONS') return { statusCode: 200, headers: CORS, body: '' };
await initDB();
const body = JSON.parse(event.body || '{}');
const { action } = body;
if (action === 'register') {
const { name, email, password } = body;
const existing = await db.query('SELECT id FROM users WHERE email = $1', [email]);
if (existing.rows.length > 0) return json({ error: 'Email already registered' }, 400);
const privateChannel = crypto.randomBytes(32).toString('hex');
const avatarColor = COLORS[Math.floor(Math.random() * COLORS.length)];
const result = await db.query(
'INSERT INTO users (name, email, password, private_channel, avatar_color) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, email, private_channel, avatar_color',
[name, email, password, privateChannel, avatarColor]
);
const user = result.rows[0];
const mqtt = await getMqttToken(user.id);
return json({ user: { id: user.id, name: user.name, email: user.email, avatarColor: user.avatar_color, privateChannel: user.private_channel, mqtt } });
}
if (action === 'login') {
const { email, password } = body;
const result = await db.query('SELECT id, name, email, private_channel, avatar_color FROM users WHERE email = $1 AND password = $2', [email, password]);
if (result.rows.length === 0) return json({ error: 'Invalid email or password' }, 401);
const user = result.rows[0];
const mqtt = await getMqttToken(user.id);
return json({ user: { id: user.id, name: user.name, email: user.email, avatarColor: user.avatar_color, privateChannel: user.private_channel, mqtt } });
}
if (action === 'refreshMqtt') {
const { userId } = body;
const check = await db.query('SELECT id FROM users WHERE id = $1', [userId]);
if (check.rows.length === 0) return json({ error: 'Invalid user' }, 403);
const mqtt = await getMqttToken(userId);
return json({ mqtt });
}
// Public user list — NEVER expose email, password, or privateChannel
if (action === 'users') {
const result = await db.query('SELECT id, name, avatar_color FROM users ORDER BY name');
return json({ users: result.rows.map(u => ({ id: u.id, name: u.name, avatarColor: u.avatar_color })) });
}
return json({ error: 'Unknown action', available: ['register', 'login', 'refreshMqtt', 'users'] }, 400);
}
// functions/posts/index.mjs
import pg from 'pg';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const db = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 1, idleTimeoutMillis: 120000 });
const s3 = new S3Client({ region: process.env.AWS_REGION || 'us-east-1' });
let dbReady = false;
async function initDB() {
if (dbReady) return;
dbReady = true;
await db.query(`CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, user_name VARCHAR(255) NOT NULL,
content TEXT, image_url TEXT, created_at TIMESTAMP DEFAULT NOW()
)`);
await db.query(`CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY, from_user_id INTEGER NOT NULL, from_user_name VARCHAR(255) NOT NULL,
to_user_id INTEGER NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW()
)`);
}
async function mqttPublish(channel, event, data) {
const projectId = process.env.OPENKBS_PROJECT_ID;
const apiKey = process.env.OPENKBS_API_KEY;
if (!projectId || !apiKey) return null;
try {
const res = await fetch(`https://project.openkbs.com/projects/${projectId}/mqtt/publish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify({ channel, event, data }),
});
return await res.json();
} catch { return null; }
}
const CORS = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type' };
const json = (data, sc = 200) => ({ statusCode: sc, headers: CORS, body: JSON.stringify(data) });
export async function handler(event) {
if (event.requestContext?.http?.method === 'OPTIONS') return { statusCode: 200, headers: CORS, body: '' };
const body = JSON.parse(event.body || '{}');
const { action } = body;
// S3 upload URL — doesn't need DB
if (action === 'getUploadUrl') {
const bucket = process.env.STORAGE_BUCKET;
if (!bucket) return json({ error: 'Storage not configured' }, 500);
const safeName = (body.fileName || 'file').replace(/[^a-zA-Z0-9.-]/g, '_');
const key = `media/uploads/${Date.now()}-${safeName}`;
const uploadUrl = await getSignedUrl(s3, new PutObjectCommand({
Bucket: bucket, Key: key, ContentType: body.contentType || 'image/jpeg'
}), { expiresIn: 3600 });
return json({ uploadUrl, publicUrl: '/' + key, key });
}
await initDB();
if (action === 'list') {
const result = await db.query('SELECT id, user_id, user_name, content, image_url, created_at FROM posts ORDER BY created_at DESC LIMIT 50');
return json({ posts: result.rows.map(r => ({ id: r.id, userId: r.user_id, userName: r.user_name, content: r.content, imageUrl: r.image_url, createdAt: r.created_at })) });
}
if (action === 'create') {
const { content, imageUrl, userId, userName } = body;
if (!content && !imageUrl) return json({ error: 'Content or image required' }, 400);
if (!userId || !userName) return json({ error: 'Missing user info' }, 400);
const result = await db.query('INSERT INTO posts (user_id, user_name, content, image_url) VALUES ($1, $2, $3, $4) RETURNING id, created_at', [userId, userName, content || '', imageUrl || null]);
const post = { id: result.rows[0].id, userId, userName, content: content || '', imageUrl: imageUrl || null, createdAt: result.rows[0].created_at };
await mqttPublish('posts', 'new_post', { post });
return json({ post });
}
if (action === 'sendMessage') {
const { toUserId, message, fromUserId, fromUserName } = body;
if (!toUserId || !message || !fromUserId || !fromUserName) return json({ error: 'Missing fields' }, 400);
// Look up recipient's SECRET private channel
const recipient = await db.query('SELECT private_channel FROM users WHERE id = $1', [toUserId]);
if (recipient.rows.length === 0) return json({ error: 'Recipient not found' }, 404);
const result = await db.query('INSERT INTO messages (from_user_id, from_user_name, to_user_id, content) VALUES ($1, $2, $3, $4) RETURNING id, created_at', [fromUserId, fromUserName, toUserId, message]);
const msg = { id: result.rows[0].id, fromUserId, fromUserName, toUserId, content: message, createdAt: result.rows[0].created_at };
// Publish to recipient's private channel — only they receive it
await mqttPublish(recipient.rows[0].private_channel, 'new_message', msg);
return json({ message: msg });
}
if (action === 'getMessages') {
const { userId, withUserId } = body;
// Bidirectional query — messages sent in either direction
const result = await db.query(
`SELECT id, from_user_id, from_user_name, to_user_id, content, created_at FROM messages
WHERE (from_user_id = $1 AND to_user_id = $2) OR (from_user_id = $2 AND to_user_id = $1)
ORDER BY created_at ASC LIMIT 100`, [userId, withUserId]
);
return json({ messages: result.rows.map(m => ({ id: m.id, fromUserId: m.from_user_id, fromUserName: m.from_user_name, toUserId: m.to_user_id, content: m.content, createdAt: m.created_at })) });
}
return json({ error: 'Unknown action' }, 400);
}
// src/App.jsx
import React, { useState, useEffect, useRef } from 'react';
export default function App() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [users, setUsers] = useState([]);
const [onlineUsers, setOnlineUsers] = useState(new Set());
const mqttRef = useRef(null);
// Restore user from localStorage on page load
useEffect(() => {
const saved = localStorage.getItem('user');
if (!saved) return;
const u = JSON.parse(saved);
setUser({ ...u, mqtt: null }); // Show UI immediately
// Refresh expired MQTT credentials (STS tokens last 15 min)
fetch('/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'refreshMqtt', userId: u.id }),
})
.then(r => r.json())
.then(d => { if (d.mqtt) setUser(prev => ({ ...prev, mqtt: d.mqtt })); });
}, []);
// Connect to MQTT when credentials are available
useEffect(() => {
if (!user?.mqtt?.credentials) return;
// Load initial data
fetch('/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'list' }) })
.then(r => r.json()).then(d => setPosts(d.posts || []));
fetch('/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'users' }) })
.then(r => r.json()).then(d => setUsers((d.users || []).filter(u => u.id !== user.id)));
// Connect MQTT
const realtime = new MQTT.Realtime({
credentials: user.mqtt.credentials,
iotEndpoint: user.mqtt.iotEndpoint,
region: user.mqtt.region,
topicPrefix: user.mqtt.topicPrefix,
clientIdPrefix: user.mqtt.clientIdPrefix,
clientId: String(user.id),
});
mqttRef.current = realtime;
// Subscribe to posts channel
const postsChannel = realtime.channels.get('posts');
postsChannel.subscribe('new_post', (msg) => {
const post = msg.data?.post || msg.data;
if (post?.id) setPosts(prev => prev.some(p => p.id === post.id) ? prev : [post, ...prev]);
});
// Presence — who's online
postsChannel.presence.enter({ userId: String(user.id), name: user.name });
postsChannel.presence.subscribe((members) => {
const ids = new Set(members.map(m => Number(m.data?.userId || m.clientId)));
ids.add(user.id);
setOnlineUsers(ids);
});
// Subscribe to private channel for direct messages
let privateChannel = null;
if (user.privateChannel) {
privateChannel = realtime.channels.get(user.privateChannel);
privateChannel.subscribe('new_message', (msg) => {
// Handle incoming private message — update your message state
console.log('Private message:', msg.data);
});
}
// Cleanup on unmount or credential change
return () => {
postsChannel.presence.leave();
if (privateChannel) privateChannel.detach();
realtime.close();
};
}, [user?.mqtt?.credentials]);
const handleLogin = (userData) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};
const handleLogout = () => {
mqttRef.current?.close();
setUser(null);
setPosts([]);
localStorage.removeItem('user');
};
if (!user) return <LoginForm onLogin={handleLogin} />;
return (
<div>
<header>
<span>Welcome, {user.name}</span>
<button onClick={handleLogout}>Logout</button>
</header>
<UserList users={users} onlineUsers={onlineUsers} />
<PostsFeed user={user} posts={posts} />
</div>
);
}
function LoginForm({ onLogin }) {
const [isRegister, setIsRegister] = useState(false);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const res = await fetch('/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: isRegister ? 'register' : 'login', name, email, password }),
});
const data = await res.json();
if (data.error) setError(data.error);
else if (data.user) onLogin(data.user);
} catch { setError('Connection error'); }
finally { setLoading(false); }
};
return (
<form onSubmit={handleSubmit}>
<h2>{isRegister ? 'Create Account' : 'Welcome Back'}</h2>
{error && <div style={{ color: 'red' }}>{error}</div>}
{isRegister && <input placeholder="Name" value={name} onChange={e => setName(e.target.value)} required />}
<input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required />
<input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} required />
<button disabled={loading}>{loading ? 'Loading...' : isRegister ? 'Register' : 'Login'}</button>
<p><a onClick={() => setIsRegister(!isRegister)}>{isRegister ? 'Already have an account? Login' : "Don't have an account? Register"}</a></p>
</form>
);
}
function PostsFeed({ user, posts }) {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(false);
const [imageFile, setImageFile] = useState(null);
const [imagePreview, setImagePreview] = useState(null);
const handleImageSelect = (e) => {
const file = e.target.files[0];
if (!file?.type.startsWith('image/')) return;
setImageFile(file);
const reader = new FileReader();
reader.onload = (e) => setImagePreview(e.target.result);
reader.readAsDataURL(file);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!content.trim() && !imageFile) return;
setLoading(true);
try {
let imageUrl = null;
if (imageFile) {
// Get presigned upload URL
const urlRes = await fetch('/posts', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'getUploadUrl', fileName: imageFile.name, contentType: imageFile.type }),
});
const { uploadUrl, publicUrl } = await urlRes.json();
// Upload directly to S3
await fetch(uploadUrl, { method: 'PUT', body: imageFile, headers: { 'Content-Type': imageFile.type } });
imageUrl = publicUrl;
}
await fetch('/posts', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create', content, imageUrl, userId: user.id, userName: user.name }),
});
setContent(''); setImageFile(null); setImagePreview(null);
} finally { setLoading(false); }
};
return (
<div>
<form onSubmit={handleSubmit}>
<textarea placeholder="What's on your mind?" value={content} onChange={e => setContent(e.target.value)} />
<input type="file" accept="image/*" onChange={handleImageSelect} />
{imagePreview && <img src={imagePreview} style={{ maxWidth: 200 }} alt="Preview" />}
<button disabled={loading}>{loading ? 'Posting...' : 'Post'}</button>
</form>
{posts.map(post => (
<div key={post.id}>
<strong>{post.userName}</strong>
<p>{post.content}</p>
{post.imageUrl && <img src={post.imageUrl} style={{ maxWidth: '100%' }} alt="" />}
</div>
))}
</div>
);
}
function UserList({ users, onlineUsers, onSelectUser }) {
return (
<div>
{users.map(u => (
<div key={u.id} onClick={() => onSelectUser?.(u)} style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{
width: 10, height: 10, borderRadius: '50%',
background: onlineUsers.has(u.id) ? '#4CAF50' : '#ccc',
}} />
<span>{u.name}</span>
</div>
))}
</div>
);
}
function ChatView({ user, selectedUser, messages, onSend }) {
const [input, setInput] = useState('');
const endRef = useRef(null);
useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
onSend(input);
setInput('');
};
return (
<div>
<div style={{ overflowY: 'auto', maxHeight: 400 }}>
{messages.map((msg, i) => (
<div key={msg.id || i} style={{ textAlign: msg.fromUserId === user.id ? 'right' : 'left', marginBottom: 8 }}>
<span style={{
display: 'inline-block', padding: '8px 12px', borderRadius: 12,
background: msg.fromUserId === user.id ? '#007bff' : '#e0e0e0',
color: msg.fromUserId === user.id ? 'white' : '#333',
}}>{msg.content}</span>
</div>
))}
<div ref={endRef} />
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: 8 }}>
<input value={input} onChange={e => setInput(e.target.value)} placeholder="Type a message..." style={{ flex: 1 }} />
<button type="submit">Send</button>
</form>
</div>
);
}
// In your App component or message handler:
const sendMessage = async (toUserId, content) => {
const res = await fetch('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'sendMessage', toUserId, message: content, fromUserId: user.id, fromUserName: user.name }),
});
const data = await res.json();
if (data.message) {
// Add to local state immediately (sender sees it right away)
setMessages(prev => ({ ...prev, [toUserId]: [...(prev[toUserId] || []), data.message] }));
}
};
// Loading chat history when selecting a user:
const loadMessages = async (withUserId) => {
const res = await fetch('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'getMessages', userId: user.id, withUserId }),
});
const data = await res.json();
setMessages(prev => ({ ...prev, [withUserId]: data.messages || [] }));
};
The project board is a Trello-like kanban system.
openkbs board # Get full board
openkbs board create <title> [-c <column>] [-t <type>] [-p <priority>] [-d <description>]
openkbs board update <cardId> [--title <t>] [--description <d>] [--priority <p>] [--status <s>]
openkbs board move <cardId> <columnName>
openkbs board comment <cardId> <message>
openkbs board delete <cardId>
Card types: task, bug, feature, paid-task Priorities: low, medium, high, critical Default columns: Backlog, To Do, In Progress, Review, Done
openkbs image "A sunset over mountains" -o site/hero.png
openkbs image "Create a banner with this logo" --ref site/logo.png -o site/banner.png
openkbs image "Wide banner" --aspect-ratio 16:9 --count 4 -o site/banner.png
AI proxy that routes to OpenAI, Anthropic, and Google. Charges to project credits automatically — no vendor API keys needed.
| Route | Vendor |
|---|---|
/v1/openai/* | OpenAI |
/v1/anthropic/* | Anthropic |
/v1/google/* |
Uses OPENKBS_API_KEY (injected automatically into elastic functions).
Important: The proxy only accepts
Authorization: Bearer <OPENKBS_API_KEY>for auth. The Anthropic SDK sendsx-api-keyand the Google SDK sendsx-goog-api-key— neither works. You must addheaders: { Authorization: \Bearer ${apiKey}` }` when configuring those providers.
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,
});
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'),
prompt: 'Hello!',
});
Note: Functions run on AWS Lambda which does not support streaming responses. Use
generateText(notstreamText).
| Vendor | Model | Input (credits/1K) | Output (credits/1K) |
|---|---|---|---|
| openai | gpt-5.4 | 300 | 1800 |
| openai | gpt-5.4-mini | 90 | 540 |
| anthropic | claude-opus-4-6 | 600 | 3000 |
| anthropic | claude-sonnet-4-6 | 360 | 1800 |
| anthropic | claude-haiku-4-5-20251001 | 120 | 600 |
| gemini-3.1-pro-preview | 240 | 1440 | |
| gemini-3.1-flash-lite-preview | 30 | 180 | |
| gemini-3-flash-preview | 60 | 360 |
100,000 credits = 1 EUR. Use GET /ai/models to fetch the latest list programmatically.