openkbs 0.0.64 → 0.0.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/elastic/README.md +1150 -0
- package/elastic/functions.md +328 -0
- package/elastic/postgres.md +287 -0
- package/elastic/pulse.md +386 -0
- package/elastic/storage.md +291 -0
- package/package.json +1 -1
- package/src/actions.js +76 -92
- package/src/index.js +23 -10
- package/src/utils.js +8 -4
- package/templates/.claude/skills/openkbs/SKILL.md +184 -0
- package/templates/.claude/skills/openkbs/metadata.json +1 -0
- package/templates/.claude/skills/openkbs/reference/backend-sdk.md +428 -0
- package/templates/.claude/skills/openkbs/reference/commands.md +370 -0
- package/templates/.claude/skills/openkbs/reference/elastic-services.md +327 -0
- package/templates/.claude/skills/openkbs/reference/frontend-sdk.md +299 -0
- package/version.json +3 -3
- package/templates/.openkbs/knowledge/metadata.json +0 -3
- package/templates/CLAUDE.md +0 -655
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/icon.png +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/instructions.txt +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/app/settings.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/scripts/run_job.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/scripts/utils/agent_client.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/actions.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/handler.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onRequest.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onRequest.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onResponse.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Events/onResponse.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Frontend/contentRender.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-copywriter-agent/src/Frontend/contentRender.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/README.md +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/app/instructions.txt +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/app/settings.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/actions.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onRequest.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onRequest.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onResponse.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Events/onResponse.json +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Frontend/contentRender.js +0 -0
- /package/templates/{.openkbs/knowledge → .claude/skills/openkbs}/examples/ai-marketing-agent/src/Frontend/contentRender.json +0 -0
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
# OpenKBS Elastic Services
|
|
2
|
+
|
|
3
|
+
Deploy full-stack applications with zero infrastructure setup. Elastic Services provide production-ready PostgreSQL, S3 storage, serverless functions, and real-time WebSockets - all with simple CLI commands.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
|
+
- [PostgreSQL Database](#postgresql-database)
|
|
10
|
+
- [S3 Storage & CloudFront CDN](#s3-storage--cloudfront-cdn)
|
|
11
|
+
- [Serverless Functions](#serverless-functions)
|
|
12
|
+
- [Real-time Pulse](#real-time-pulse)
|
|
13
|
+
- [CLI Reference](#cli-reference)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
### What Are Elastic Services?
|
|
20
|
+
|
|
21
|
+
Elastic Services extend your OpenKBS Knowledge Base with cloud infrastructure:
|
|
22
|
+
|
|
23
|
+
| Service | What You Get | Use Case |
|
|
24
|
+
|---------|--------------|----------|
|
|
25
|
+
| **Postgres** | PostgreSQL database (Neon) | Store users, orders, content |
|
|
26
|
+
| **Storage** | S3 bucket + CloudFront CDN | Upload images, files, media |
|
|
27
|
+
| **Functions** | Serverless Lambda functions | Build APIs, webhooks, background jobs |
|
|
28
|
+
| **Pulse** | Real-time WebSocket | Live updates, chat, presence |
|
|
29
|
+
|
|
30
|
+
### Why Elastic?
|
|
31
|
+
|
|
32
|
+
**Before Elastic:**
|
|
33
|
+
- Set up AWS accounts, IAM roles, VPCs
|
|
34
|
+
- Configure databases, connection strings
|
|
35
|
+
- Manage deployments, CI/CD pipelines
|
|
36
|
+
- Handle SSL certificates, domains
|
|
37
|
+
- Debug networking issues
|
|
38
|
+
|
|
39
|
+
**With Elastic:**
|
|
40
|
+
```bash
|
|
41
|
+
openkbs postgres enable # Database ready in 10 seconds
|
|
42
|
+
openkbs storage enable # S3 bucket with CDN
|
|
43
|
+
openkbs fn push api # Deploy your API
|
|
44
|
+
openkbs deploy # Ship everything
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Your app is live at `https://yourdomain.com/`.
|
|
48
|
+
|
|
49
|
+
### Environment Variables
|
|
50
|
+
|
|
51
|
+
When you enable Elastic services, environment variables are automatically injected into your functions:
|
|
52
|
+
|
|
53
|
+
| Variable | Service | Example |
|
|
54
|
+
|----------|---------|---------|
|
|
55
|
+
| `DATABASE_URL` | Postgres | `postgresql://user:pass@host/db` |
|
|
56
|
+
| `STORAGE_BUCKET` | Storage | `openkbs-elastic-xyz123` |
|
|
57
|
+
| `STORAGE_REGION` | Storage | `us-east-1` |
|
|
58
|
+
| `OPENKBS_KB_ID` | All | `abc123xyz` |
|
|
59
|
+
| `OPENKBS_API_KEY` | All | Your API key |
|
|
60
|
+
|
|
61
|
+
No manual configuration needed. Just use `process.env.DATABASE_URL` in your code.
|
|
62
|
+
|
|
63
|
+
### Regions
|
|
64
|
+
|
|
65
|
+
Available in 3 regions:
|
|
66
|
+
|
|
67
|
+
| Region | Location | Latency Target |
|
|
68
|
+
|--------|----------|----------------|
|
|
69
|
+
| `us-east-1` | N. Virginia (default) | Americas |
|
|
70
|
+
| `eu-central-1` | Frankfurt | Europe |
|
|
71
|
+
| `ap-southeast-1` | Singapore | Asia-Pacific |
|
|
72
|
+
|
|
73
|
+
Specify region in `openkbs.json` or with `--region` flag.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
### 1. Create Project
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mkdir my-app && cd my-app
|
|
83
|
+
openkbs init
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Configure Services
|
|
87
|
+
|
|
88
|
+
Create `openkbs.json`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"name": "my-app",
|
|
93
|
+
"region": "us-east-1",
|
|
94
|
+
"elastic": {
|
|
95
|
+
"postgres": true,
|
|
96
|
+
"storage": {
|
|
97
|
+
"cloudfront": "media"
|
|
98
|
+
},
|
|
99
|
+
"pulse": true
|
|
100
|
+
},
|
|
101
|
+
"functions": ["api"],
|
|
102
|
+
"site": "./site"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Create API Function
|
|
107
|
+
|
|
108
|
+
Create `functions/api/index.mjs`:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
import pg from 'pg';
|
|
112
|
+
|
|
113
|
+
const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
114
|
+
let connected = false;
|
|
115
|
+
|
|
116
|
+
export async function handler(event) {
|
|
117
|
+
if (!connected) { await db.connect(); connected = true; }
|
|
118
|
+
|
|
119
|
+
const { action } = JSON.parse(event.body || '{}');
|
|
120
|
+
|
|
121
|
+
if (action === 'list') {
|
|
122
|
+
const { rows } = await db.query('SELECT * FROM items');
|
|
123
|
+
return { statusCode: 200, body: JSON.stringify(rows) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'Unknown action' }) };
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Create `functions/api/package.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"type": "module",
|
|
135
|
+
"dependencies": {
|
|
136
|
+
"pg": "^8.11.3"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 4. Deploy
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
cd functions/api && npm install && cd ../..
|
|
145
|
+
openkbs deploy
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Done. Your API is live at `https://yourdomain.com/api`.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## PostgreSQL Database
|
|
153
|
+
|
|
154
|
+
Add a PostgreSQL database to your project with one command. Elastic Postgres uses Neon - a serverless PostgreSQL that scales automatically.
|
|
155
|
+
|
|
156
|
+
### Enable Postgres
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
openkbs postgres enable
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
That's it. Your database is ready.
|
|
163
|
+
|
|
164
|
+
### Check Status
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
openkbs postgres status
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Output:
|
|
171
|
+
```
|
|
172
|
+
Postgres Status:
|
|
173
|
+
Enabled: true
|
|
174
|
+
Host: ep-xyz-123456.us-east-1.aws.neon.tech
|
|
175
|
+
Database: neondb
|
|
176
|
+
Region: us-east-1
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Get Connection String
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
openkbs postgres connection
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Output:
|
|
186
|
+
```
|
|
187
|
+
postgresql://user:password@ep-xyz-123456.us-east-1.aws.neon.tech/neondb?sslmode=require
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Use in Node.js
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
import pg from 'pg';
|
|
194
|
+
|
|
195
|
+
const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
196
|
+
let connected = false;
|
|
197
|
+
|
|
198
|
+
export async function handler(event) {
|
|
199
|
+
// Connect once, reuse across invocations
|
|
200
|
+
if (!connected) {
|
|
201
|
+
await db.connect();
|
|
202
|
+
connected = true;
|
|
203
|
+
|
|
204
|
+
// Create tables on first run
|
|
205
|
+
await db.query(`
|
|
206
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
207
|
+
id SERIAL PRIMARY KEY,
|
|
208
|
+
name TEXT NOT NULL,
|
|
209
|
+
email TEXT UNIQUE NOT NULL,
|
|
210
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
211
|
+
)
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
216
|
+
|
|
217
|
+
switch (action) {
|
|
218
|
+
case 'list':
|
|
219
|
+
const { rows } = await db.query('SELECT * FROM users ORDER BY created_at DESC');
|
|
220
|
+
return { statusCode: 200, body: JSON.stringify(rows) };
|
|
221
|
+
|
|
222
|
+
case 'create':
|
|
223
|
+
const result = await db.query(
|
|
224
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
|
|
225
|
+
[data.name, data.email]
|
|
226
|
+
);
|
|
227
|
+
return { statusCode: 200, body: JSON.stringify(result.rows[0]) };
|
|
228
|
+
|
|
229
|
+
case 'delete':
|
|
230
|
+
await db.query('DELETE FROM users WHERE id = $1', [data.id]);
|
|
231
|
+
return { statusCode: 200, body: JSON.stringify({ deleted: true }) };
|
|
232
|
+
|
|
233
|
+
default:
|
|
234
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'Unknown action' }) };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Use in Python
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
import json
|
|
243
|
+
import os
|
|
244
|
+
import psycopg2
|
|
245
|
+
|
|
246
|
+
conn = None
|
|
247
|
+
|
|
248
|
+
def handler(event, context):
|
|
249
|
+
global conn
|
|
250
|
+
if conn is None:
|
|
251
|
+
conn = psycopg2.connect(os.environ['DATABASE_URL'])
|
|
252
|
+
with conn.cursor() as cur:
|
|
253
|
+
cur.execute('''
|
|
254
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
255
|
+
id SERIAL PRIMARY KEY,
|
|
256
|
+
name TEXT NOT NULL
|
|
257
|
+
)
|
|
258
|
+
''')
|
|
259
|
+
conn.commit()
|
|
260
|
+
|
|
261
|
+
body = json.loads(event.get('body', '{}'))
|
|
262
|
+
action = body.get('action')
|
|
263
|
+
|
|
264
|
+
with conn.cursor() as cur:
|
|
265
|
+
if action == 'list':
|
|
266
|
+
cur.execute('SELECT * FROM items')
|
|
267
|
+
rows = cur.fetchall()
|
|
268
|
+
return {'statusCode': 200, 'body': json.dumps(rows)}
|
|
269
|
+
|
|
270
|
+
elif action == 'create':
|
|
271
|
+
cur.execute('INSERT INTO items (name) VALUES (%s) RETURNING id', (body['name'],))
|
|
272
|
+
conn.commit()
|
|
273
|
+
return {'statusCode': 200, 'body': json.dumps({'id': cur.fetchone()[0]})}
|
|
274
|
+
|
|
275
|
+
return {'statusCode': 400, 'body': json.dumps({'error': 'Unknown action'})}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Use in Java
|
|
279
|
+
|
|
280
|
+
```java
|
|
281
|
+
package com.example;
|
|
282
|
+
|
|
283
|
+
import com.amazonaws.services.lambda.runtime.Context;
|
|
284
|
+
import com.amazonaws.services.lambda.runtime.RequestHandler;
|
|
285
|
+
import com.google.gson.Gson;
|
|
286
|
+
import java.sql.*;
|
|
287
|
+
import java.util.*;
|
|
288
|
+
|
|
289
|
+
public class Handler implements RequestHandler<Map<String, Object>, Map<String, Object>> {
|
|
290
|
+
private static Connection conn;
|
|
291
|
+
private static final Gson gson = new Gson();
|
|
292
|
+
|
|
293
|
+
@Override
|
|
294
|
+
public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
|
|
295
|
+
try {
|
|
296
|
+
if (conn == null || conn.isClosed()) {
|
|
297
|
+
conn = DriverManager.getConnection(System.getenv("DATABASE_URL"));
|
|
298
|
+
try (Statement stmt = conn.createStatement()) {
|
|
299
|
+
stmt.execute("CREATE TABLE IF NOT EXISTS items (id SERIAL PRIMARY KEY, name TEXT)");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
String body = (String) event.get("body");
|
|
304
|
+
Map<String, Object> request = gson.fromJson(body, Map.class);
|
|
305
|
+
String action = (String) request.get("action");
|
|
306
|
+
|
|
307
|
+
if ("list".equals(action)) {
|
|
308
|
+
List<Map<String, Object>> items = new ArrayList<>();
|
|
309
|
+
try (ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM items")) {
|
|
310
|
+
while (rs.next()) {
|
|
311
|
+
items.add(Map.of("id", rs.getInt("id"), "name", rs.getString("name")));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return Map.of("statusCode", 200, "body", gson.toJson(items));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return Map.of("statusCode", 400, "body", "{\"error\":\"Unknown action\"}");
|
|
318
|
+
} catch (Exception e) {
|
|
319
|
+
return Map.of("statusCode", 500, "body", "{\"error\":\"" + e.getMessage() + "\"}");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Common Patterns
|
|
326
|
+
|
|
327
|
+
#### Parameterized Queries (Prevent SQL Injection)
|
|
328
|
+
|
|
329
|
+
```javascript
|
|
330
|
+
// GOOD - parameterized
|
|
331
|
+
await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
332
|
+
|
|
333
|
+
// BAD - string concatenation (NEVER DO THIS)
|
|
334
|
+
await db.query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Pagination
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const page = data.page || 1;
|
|
341
|
+
const limit = data.limit || 20;
|
|
342
|
+
const offset = (page - 1) * limit;
|
|
343
|
+
|
|
344
|
+
const { rows } = await db.query(
|
|
345
|
+
'SELECT * FROM items ORDER BY created_at DESC LIMIT $1 OFFSET $2',
|
|
346
|
+
[limit, offset]
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const { rows: [{ count }] } = await db.query('SELECT COUNT(*) FROM items');
|
|
350
|
+
|
|
351
|
+
return { items: rows, total: parseInt(count), page, limit };
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Search
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
const { rows } = await db.query(
|
|
358
|
+
'SELECT * FROM items WHERE name ILIKE $1',
|
|
359
|
+
[`%${data.query}%`]
|
|
360
|
+
);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### JSON Columns
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
// Create table with JSONB column
|
|
367
|
+
await db.query(`
|
|
368
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
369
|
+
id SERIAL PRIMARY KEY,
|
|
370
|
+
name TEXT,
|
|
371
|
+
metadata JSONB DEFAULT '{}'
|
|
372
|
+
)
|
|
373
|
+
`);
|
|
374
|
+
|
|
375
|
+
// Query JSON
|
|
376
|
+
const { rows } = await db.query(
|
|
377
|
+
"SELECT * FROM products WHERE metadata->>'category' = $1",
|
|
378
|
+
['electronics']
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Update JSON field
|
|
382
|
+
await db.query(
|
|
383
|
+
"UPDATE products SET metadata = jsonb_set(metadata, '{stock}', $1) WHERE id = $2",
|
|
384
|
+
[JSON.stringify(100), productId]
|
|
385
|
+
);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Transactions
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
try {
|
|
392
|
+
await db.query('BEGIN');
|
|
393
|
+
await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
|
|
394
|
+
await db.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
|
|
395
|
+
await db.query('COMMIT');
|
|
396
|
+
} catch (e) {
|
|
397
|
+
await db.query('ROLLBACK');
|
|
398
|
+
throw e;
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Postgres CLI Commands
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
openkbs postgres enable # Enable database
|
|
406
|
+
openkbs postgres status # Check status
|
|
407
|
+
openkbs postgres connection # Get connection string
|
|
408
|
+
openkbs postgres disable # Disable (WARNING: deletes data)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## S3 Storage & CloudFront CDN
|
|
414
|
+
|
|
415
|
+
Upload files, serve images, and host media with S3 storage and CloudFront CDN. Get presigned URLs for secure browser uploads.
|
|
416
|
+
|
|
417
|
+
### Enable Storage
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
openkbs storage enable
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Your S3 bucket is ready.
|
|
424
|
+
|
|
425
|
+
### Check Status
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
openkbs storage status
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Output:
|
|
432
|
+
```
|
|
433
|
+
Storage Status:
|
|
434
|
+
Enabled: true
|
|
435
|
+
Bucket: openkbs-elastic-abc123
|
|
436
|
+
Region: us-east-1
|
|
437
|
+
Public: false
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Add CloudFront CDN
|
|
441
|
+
|
|
442
|
+
Serve files from your domain with edge caching:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
openkbs storage cloudfront media
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
This maps:
|
|
449
|
+
- S3 path `media/*` to URL `yourdomain.com/media/*`
|
|
450
|
+
|
|
451
|
+
| S3 Key | Public URL |
|
|
452
|
+
|--------|------------|
|
|
453
|
+
| `media/photo.jpg` | `yourdomain.com/media/photo.jpg` |
|
|
454
|
+
| `media/uploads/image.png` | `yourdomain.com/media/uploads/image.png` |
|
|
455
|
+
|
|
456
|
+
### Upload Files from CLI
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
openkbs storage put ./photo.jpg media/photo.jpg
|
|
460
|
+
openkbs storage put ./document.pdf docs/document.pdf
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Generate Presigned Upload URL (Node.js)
|
|
464
|
+
|
|
465
|
+
```javascript
|
|
466
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
467
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
468
|
+
|
|
469
|
+
const s3 = new S3Client({ region: process.env.STORAGE_REGION });
|
|
470
|
+
|
|
471
|
+
export async function handler(event) {
|
|
472
|
+
const { action, fileName, contentType } = JSON.parse(event.body || '{}');
|
|
473
|
+
|
|
474
|
+
if (action === 'getUploadUrl') {
|
|
475
|
+
const bucket = process.env.STORAGE_BUCKET;
|
|
476
|
+
|
|
477
|
+
// Key must match CloudFront path prefix
|
|
478
|
+
const key = `media/uploads/${Date.now()}-${fileName}`;
|
|
479
|
+
|
|
480
|
+
const command = new PutObjectCommand({
|
|
481
|
+
Bucket: bucket,
|
|
482
|
+
Key: key,
|
|
483
|
+
ContentType: contentType || 'application/octet-stream'
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
|
|
487
|
+
|
|
488
|
+
// Return relative URL for CloudFront
|
|
489
|
+
const publicUrl = `/${key}`;
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
statusCode: 200,
|
|
493
|
+
body: JSON.stringify({ uploadUrl, publicUrl, key })
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Generate Presigned Upload URL (Python)
|
|
500
|
+
|
|
501
|
+
```python
|
|
502
|
+
import json
|
|
503
|
+
import os
|
|
504
|
+
import boto3
|
|
505
|
+
from botocore.config import Config
|
|
506
|
+
|
|
507
|
+
s3 = boto3.client('s3',
|
|
508
|
+
region_name=os.environ.get('STORAGE_REGION', 'us-east-1'),
|
|
509
|
+
config=Config(signature_version='s3v4')
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def handler(event, context):
|
|
513
|
+
body = json.loads(event.get('body', '{}'))
|
|
514
|
+
action = body.get('action')
|
|
515
|
+
bucket = os.environ['STORAGE_BUCKET']
|
|
516
|
+
|
|
517
|
+
if action == 'getUploadUrl':
|
|
518
|
+
import time
|
|
519
|
+
key = f"media/uploads/{int(time.time())}-{body['fileName']}"
|
|
520
|
+
|
|
521
|
+
url = s3.generate_presigned_url('put_object',
|
|
522
|
+
Params={'Bucket': bucket, 'Key': key, 'ContentType': body.get('contentType', 'application/octet-stream')},
|
|
523
|
+
ExpiresIn=3600
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
'statusCode': 200,
|
|
528
|
+
'body': json.dumps({'uploadUrl': url, 'publicUrl': f'/{key}', 'key': key})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {'statusCode': 400, 'body': json.dumps({'error': 'Unknown action'})}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Browser Upload
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
async function uploadFile(file) {
|
|
538
|
+
// 1. Get presigned URL from your API
|
|
539
|
+
const response = await fetch('/api', {
|
|
540
|
+
method: 'POST',
|
|
541
|
+
headers: { 'Content-Type': 'application/json' },
|
|
542
|
+
body: JSON.stringify({
|
|
543
|
+
action: 'getUploadUrl',
|
|
544
|
+
fileName: file.name,
|
|
545
|
+
contentType: file.type
|
|
546
|
+
})
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const { uploadUrl, publicUrl } = await response.json();
|
|
550
|
+
|
|
551
|
+
// 2. Upload directly to S3
|
|
552
|
+
await fetch(uploadUrl, {
|
|
553
|
+
method: 'PUT',
|
|
554
|
+
body: file,
|
|
555
|
+
headers: { 'Content-Type': file.type }
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// 3. Return the public URL
|
|
559
|
+
return publicUrl; // e.g., /media/uploads/1234567890-photo.jpg
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Usage
|
|
563
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
564
|
+
fileInput.addEventListener('change', async (e) => {
|
|
565
|
+
const file = e.target.files[0];
|
|
566
|
+
const url = await uploadFile(file);
|
|
567
|
+
console.log('Uploaded to:', url);
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Upload with Progress
|
|
572
|
+
|
|
573
|
+
```javascript
|
|
574
|
+
function uploadWithProgress(file, onProgress) {
|
|
575
|
+
return new Promise(async (resolve, reject) => {
|
|
576
|
+
const { uploadUrl, publicUrl } = await fetch('/api', {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: { 'Content-Type': 'application/json' },
|
|
579
|
+
body: JSON.stringify({
|
|
580
|
+
action: 'getUploadUrl',
|
|
581
|
+
fileName: file.name,
|
|
582
|
+
contentType: file.type
|
|
583
|
+
})
|
|
584
|
+
}).then(r => r.json());
|
|
585
|
+
|
|
586
|
+
const xhr = new XMLHttpRequest();
|
|
587
|
+
|
|
588
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
589
|
+
if (e.lengthComputable) {
|
|
590
|
+
onProgress(Math.round((e.loaded / e.total) * 100));
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
xhr.addEventListener('load', () => {
|
|
595
|
+
if (xhr.status === 200) resolve(publicUrl);
|
|
596
|
+
else reject(new Error('Upload failed'));
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
xhr.addEventListener('error', reject);
|
|
600
|
+
xhr.open('PUT', uploadUrl);
|
|
601
|
+
xhr.setRequestHeader('Content-Type', file.type);
|
|
602
|
+
xhr.send(file);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Usage
|
|
607
|
+
const url = await uploadWithProgress(file, (percent) => {
|
|
608
|
+
console.log(`Upload: ${percent}%`);
|
|
609
|
+
});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Storage CLI Commands
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
openkbs storage enable # Enable storage
|
|
616
|
+
openkbs storage status # Check status
|
|
617
|
+
openkbs storage public true|false # Set public access
|
|
618
|
+
openkbs storage cloudfront <path> # Add CloudFront path
|
|
619
|
+
openkbs storage cloudfront remove <path> # Remove CloudFront path
|
|
620
|
+
openkbs storage ls [prefix] # List files
|
|
621
|
+
openkbs storage put <file> [key] # Upload file
|
|
622
|
+
openkbs storage get <key> [file] # Download file
|
|
623
|
+
openkbs storage rm <key> # Delete file
|
|
624
|
+
openkbs storage disable --force # Disable (DANGEROUS)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Serverless Functions
|
|
630
|
+
|
|
631
|
+
Deploy serverless APIs with AWS Lambda. Write your code in Node.js, Python, or Java. Get HTTPS endpoints automatically.
|
|
632
|
+
|
|
633
|
+
### Project Structure
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
my-app/
|
|
637
|
+
├── openkbs.json
|
|
638
|
+
├── functions/
|
|
639
|
+
│ └── api/
|
|
640
|
+
│ ├── index.mjs # Node.js handler
|
|
641
|
+
│ └── package.json # Dependencies
|
|
642
|
+
└── site/
|
|
643
|
+
└── index.html
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Node.js Function
|
|
647
|
+
|
|
648
|
+
Create `functions/api/index.mjs`:
|
|
649
|
+
|
|
650
|
+
```javascript
|
|
651
|
+
export async function handler(event) {
|
|
652
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
653
|
+
|
|
654
|
+
switch (action) {
|
|
655
|
+
case 'hello':
|
|
656
|
+
return {
|
|
657
|
+
statusCode: 200,
|
|
658
|
+
headers: { 'Content-Type': 'application/json' },
|
|
659
|
+
body: JSON.stringify({ message: `Hello, ${data.name || 'World'}!` })
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
case 'echo':
|
|
663
|
+
return {
|
|
664
|
+
statusCode: 200,
|
|
665
|
+
body: JSON.stringify({ received: data })
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
default:
|
|
669
|
+
return {
|
|
670
|
+
statusCode: 400,
|
|
671
|
+
body: JSON.stringify({ error: 'Unknown action' })
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Create `functions/api/package.json`:
|
|
678
|
+
|
|
679
|
+
```json
|
|
680
|
+
{
|
|
681
|
+
"type": "module",
|
|
682
|
+
"dependencies": {}
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### Python Function
|
|
687
|
+
|
|
688
|
+
Create `functions/api/handler.py`:
|
|
689
|
+
|
|
690
|
+
```python
|
|
691
|
+
import json
|
|
692
|
+
|
|
693
|
+
def handler(event, context):
|
|
694
|
+
body = json.loads(event.get('body', '{}'))
|
|
695
|
+
action = body.get('action')
|
|
696
|
+
|
|
697
|
+
if action == 'hello':
|
|
698
|
+
name = body.get('name', 'World')
|
|
699
|
+
return {
|
|
700
|
+
'statusCode': 200,
|
|
701
|
+
'body': json.dumps({'message': f'Hello, {name}!'})
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
'statusCode': 400,
|
|
706
|
+
'body': json.dumps({'error': 'Unknown action'})
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### Java Function
|
|
711
|
+
|
|
712
|
+
Create `functions/api/src/main/java/com/example/Handler.java`:
|
|
713
|
+
|
|
714
|
+
```java
|
|
715
|
+
package com.example;
|
|
716
|
+
|
|
717
|
+
import com.amazonaws.services.lambda.runtime.Context;
|
|
718
|
+
import com.amazonaws.services.lambda.runtime.RequestHandler;
|
|
719
|
+
import com.google.gson.Gson;
|
|
720
|
+
import java.util.Map;
|
|
721
|
+
|
|
722
|
+
public class Handler implements RequestHandler<Map<String, Object>, Map<String, Object>> {
|
|
723
|
+
private static final Gson gson = new Gson();
|
|
724
|
+
|
|
725
|
+
@Override
|
|
726
|
+
public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
|
|
727
|
+
String body = (String) event.get("body");
|
|
728
|
+
Map<String, Object> request = gson.fromJson(body, Map.class);
|
|
729
|
+
String action = (String) request.get("action");
|
|
730
|
+
|
|
731
|
+
if ("hello".equals(action)) {
|
|
732
|
+
String name = (String) request.getOrDefault("name", "World");
|
|
733
|
+
return Map.of(
|
|
734
|
+
"statusCode", 200,
|
|
735
|
+
"body", gson.toJson(Map.of("message", "Hello, " + name + "!"))
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return Map.of("statusCode", 400, "body", "{\"error\":\"Unknown action\"}");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Deploy Function
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
# Install dependencies
|
|
748
|
+
cd functions/api && npm install && cd ../..
|
|
749
|
+
|
|
750
|
+
# Deploy function
|
|
751
|
+
openkbs fn push api
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Your function is live at `https://yourdomain.com/api`.
|
|
755
|
+
|
|
756
|
+
### Test Function
|
|
757
|
+
|
|
758
|
+
```bash
|
|
759
|
+
# From CLI
|
|
760
|
+
openkbs fn invoke api '{"action":"hello","name":"OpenKBS"}'
|
|
761
|
+
|
|
762
|
+
# With curl
|
|
763
|
+
curl -X POST https://yourdomain.com/api \
|
|
764
|
+
-H "Content-Type: application/json" \
|
|
765
|
+
-d '{"action":"hello","name":"OpenKBS"}'
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### Configure Memory & Timeout
|
|
769
|
+
|
|
770
|
+
```bash
|
|
771
|
+
# Default: 128MB memory, 30s timeout
|
|
772
|
+
openkbs fn push api
|
|
773
|
+
|
|
774
|
+
# More memory (= more CPU)
|
|
775
|
+
openkbs fn push api --memory 512
|
|
776
|
+
|
|
777
|
+
# Longer timeout (max 900s)
|
|
778
|
+
openkbs fn push api --timeout 60
|
|
779
|
+
|
|
780
|
+
# Both
|
|
781
|
+
openkbs fn push api --memory 1024 --timeout 120
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Memory options: 128, 256, 512, 1024, 2048, 3008 MB
|
|
785
|
+
|
|
786
|
+
### Environment Variables
|
|
787
|
+
|
|
788
|
+
```bash
|
|
789
|
+
# View environment
|
|
790
|
+
openkbs fn env api
|
|
791
|
+
|
|
792
|
+
# Set custom variables
|
|
793
|
+
openkbs fn env api STRIPE_KEY=sk_live_xxx
|
|
794
|
+
openkbs fn env api DEBUG=true
|
|
795
|
+
|
|
796
|
+
# Remove variable
|
|
797
|
+
openkbs fn env api STRIPE_KEY=
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### CORS Headers
|
|
801
|
+
|
|
802
|
+
For browser access, add CORS headers:
|
|
803
|
+
|
|
804
|
+
```javascript
|
|
805
|
+
export async function handler(event) {
|
|
806
|
+
const headers = {
|
|
807
|
+
'Content-Type': 'application/json',
|
|
808
|
+
'Access-Control-Allow-Origin': '*',
|
|
809
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Handle OPTIONS preflight
|
|
813
|
+
if (event.requestContext?.http?.method === 'OPTIONS') {
|
|
814
|
+
return { statusCode: 200, headers, body: '' };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Your logic here...
|
|
818
|
+
|
|
819
|
+
return { statusCode: 200, headers, body: JSON.stringify({ success: true }) };
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Multiple Functions
|
|
824
|
+
|
|
825
|
+
```
|
|
826
|
+
functions/
|
|
827
|
+
├── auth/
|
|
828
|
+
│ ├── index.mjs
|
|
829
|
+
│ └── package.json
|
|
830
|
+
├── posts/
|
|
831
|
+
│ ├── index.mjs
|
|
832
|
+
│ └── package.json
|
|
833
|
+
└── payments/
|
|
834
|
+
├── index.mjs
|
|
835
|
+
└── package.json
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
`openkbs.json`:
|
|
839
|
+
```json
|
|
840
|
+
{
|
|
841
|
+
"name": "my-app",
|
|
842
|
+
"elastic": { "postgres": true },
|
|
843
|
+
"functions": ["auth", "posts", "payments"]
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Deploy all at once:
|
|
848
|
+
```bash
|
|
849
|
+
openkbs deploy
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Or individually:
|
|
853
|
+
```bash
|
|
854
|
+
openkbs fn push auth
|
|
855
|
+
openkbs fn push posts
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### Functions CLI Commands
|
|
859
|
+
|
|
860
|
+
```bash
|
|
861
|
+
openkbs fn list # List functions
|
|
862
|
+
openkbs fn push <name> # Deploy function
|
|
863
|
+
openkbs fn push <name> --memory 512 # With memory
|
|
864
|
+
openkbs fn push <name> --timeout 60 # With timeout
|
|
865
|
+
openkbs fn logs <name> # View logs
|
|
866
|
+
openkbs fn logs <name> --follow # Stream logs
|
|
867
|
+
openkbs fn env <name> # View env vars
|
|
868
|
+
openkbs fn env <name> KEY=value # Set env var
|
|
869
|
+
openkbs fn invoke <name> '{}' # Test invoke
|
|
870
|
+
openkbs fn delete <name> # Delete function
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Real-time Pulse
|
|
876
|
+
|
|
877
|
+
Add real-time features to your app with Pulse WebSocket messaging. Build live updates, chat, presence tracking, and collaborative features.
|
|
878
|
+
|
|
879
|
+
### Enable Pulse
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
openkbs pulse enable
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### How It Works
|
|
886
|
+
|
|
887
|
+
```
|
|
888
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
889
|
+
│ Browser 1 │ │ Pulse │ │ Browser 2 │
|
|
890
|
+
│ │────▶│ Server │────▶│ │
|
|
891
|
+
│ Subscribe │ │ │ │ Receive │
|
|
892
|
+
│ to 'posts' │ │ │ │ message │
|
|
893
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
894
|
+
▲
|
|
895
|
+
│ Publish
|
|
896
|
+
┌──────────────┐
|
|
897
|
+
│ Lambda │
|
|
898
|
+
│ Function │
|
|
899
|
+
└──────────────┘
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
1. Clients connect via WebSocket and subscribe to channels
|
|
903
|
+
2. Your Lambda publishes events to channels
|
|
904
|
+
3. All subscribers receive the message instantly
|
|
905
|
+
|
|
906
|
+
### Client SDK (Browser)
|
|
907
|
+
|
|
908
|
+
Install:
|
|
909
|
+
|
|
910
|
+
```html
|
|
911
|
+
<script src="https://unpkg.com/openkbs-pulse@2.0.1/pulse.js"></script>
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Or with npm:
|
|
915
|
+
```bash
|
|
916
|
+
npm install openkbs-pulse
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
Connect:
|
|
920
|
+
|
|
921
|
+
```javascript
|
|
922
|
+
// Get token from your API (see Server SDK below)
|
|
923
|
+
const { token, endpoint, kbId } = await fetch('/auth', {
|
|
924
|
+
method: 'POST',
|
|
925
|
+
body: JSON.stringify({ action: 'getPulseToken', userId: 'user123' })
|
|
926
|
+
}).then(r => r.json());
|
|
927
|
+
|
|
928
|
+
// Connect to Pulse
|
|
929
|
+
const realtime = new Pulse.Realtime({
|
|
930
|
+
kbId,
|
|
931
|
+
token,
|
|
932
|
+
endpoint,
|
|
933
|
+
clientId: 'user123'
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// Connection events
|
|
937
|
+
realtime.connection.on('connected', () => console.log('Connected!'));
|
|
938
|
+
realtime.connection.on('disconnected', () => console.log('Disconnected'));
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
Subscribe to channel:
|
|
942
|
+
|
|
943
|
+
```javascript
|
|
944
|
+
const channel = realtime.channels.get('posts');
|
|
945
|
+
|
|
946
|
+
// Subscribe to specific event
|
|
947
|
+
channel.subscribe('new_post', (message) => {
|
|
948
|
+
console.log('New post:', message.data);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Subscribe to all events
|
|
952
|
+
channel.subscribe((message) => {
|
|
953
|
+
console.log('Event:', message.name, message.data);
|
|
954
|
+
});
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
Presence (who's online):
|
|
958
|
+
|
|
959
|
+
```javascript
|
|
960
|
+
const channel = realtime.channels.get('posts');
|
|
961
|
+
|
|
962
|
+
// Enter presence with data
|
|
963
|
+
channel.presence.enter({ name: 'Alice', avatar: '/alice.jpg' });
|
|
964
|
+
|
|
965
|
+
// Get current members
|
|
966
|
+
channel.presence.get((members) => {
|
|
967
|
+
console.log('Online:', members.length);
|
|
968
|
+
members.forEach(m => console.log(m.data.name));
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// Subscribe to presence changes
|
|
972
|
+
channel.presence.subscribe((members) => {
|
|
973
|
+
console.log('Members updated:', members);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// Specific events
|
|
977
|
+
channel.presence.subscribe('enter', (member) => {
|
|
978
|
+
console.log(`${member.data.name} joined`);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
channel.presence.subscribe('leave', (member) => {
|
|
982
|
+
console.log(`${member.data.name} left`);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Leave when done
|
|
986
|
+
channel.presence.leave();
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Server SDK (Lambda)
|
|
990
|
+
|
|
991
|
+
Generate token:
|
|
992
|
+
|
|
993
|
+
```javascript
|
|
994
|
+
import pulse from 'openkbs-pulse/server';
|
|
995
|
+
|
|
996
|
+
export async function handler(event) {
|
|
997
|
+
const { action, userId } = JSON.parse(event.body || '{}');
|
|
998
|
+
const kbId = process.env.OPENKBS_KB_ID;
|
|
999
|
+
const apiKey = process.env.OPENKBS_API_KEY;
|
|
1000
|
+
|
|
1001
|
+
if (action === 'getPulseToken') {
|
|
1002
|
+
const tokenData = await pulse.getToken(kbId, apiKey, userId);
|
|
1003
|
+
return {
|
|
1004
|
+
statusCode: 200,
|
|
1005
|
+
body: JSON.stringify({
|
|
1006
|
+
token: tokenData.token,
|
|
1007
|
+
endpoint: tokenData.endpoint,
|
|
1008
|
+
kbId
|
|
1009
|
+
})
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
Publish events:
|
|
1016
|
+
|
|
1017
|
+
```javascript
|
|
1018
|
+
import pulse from 'openkbs-pulse/server';
|
|
1019
|
+
|
|
1020
|
+
export async function handler(event) {
|
|
1021
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
1022
|
+
const kbId = process.env.OPENKBS_KB_ID;
|
|
1023
|
+
const apiKey = process.env.OPENKBS_API_KEY;
|
|
1024
|
+
|
|
1025
|
+
if (action === 'createPost') {
|
|
1026
|
+
// Save to database...
|
|
1027
|
+
const post = { id: 1, title: data.title, content: data.content };
|
|
1028
|
+
|
|
1029
|
+
// Publish to all subscribers
|
|
1030
|
+
await pulse.publish('posts', 'new_post', { post }, { kbId, apiKey });
|
|
1031
|
+
|
|
1032
|
+
return { statusCode: 200, body: JSON.stringify({ post }) };
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
Get presence:
|
|
1038
|
+
|
|
1039
|
+
```javascript
|
|
1040
|
+
const presence = await pulse.presence('posts', { kbId, apiKey });
|
|
1041
|
+
console.log('Online count:', presence.count);
|
|
1042
|
+
console.log('Members:', presence.members);
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### Private Channels
|
|
1046
|
+
|
|
1047
|
+
For private messaging, use secret channel IDs:
|
|
1048
|
+
|
|
1049
|
+
```javascript
|
|
1050
|
+
// Generate unique channel ID per user (store in database)
|
|
1051
|
+
const privateChannel = crypto.randomBytes(32).toString('hex');
|
|
1052
|
+
|
|
1053
|
+
// User subscribes to their own private channel
|
|
1054
|
+
const myChannel = realtime.channels.get(user.privateChannel);
|
|
1055
|
+
myChannel.subscribe('message', (msg) => {
|
|
1056
|
+
console.log('Private message:', msg.data);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Server publishes to recipient's channel
|
|
1060
|
+
await pulse.publish(recipient.privateChannel, 'message', {
|
|
1061
|
+
from: sender.name,
|
|
1062
|
+
text: 'Hello!'
|
|
1063
|
+
}, { kbId, apiKey });
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
### Pulse CLI Commands
|
|
1067
|
+
|
|
1068
|
+
```bash
|
|
1069
|
+
openkbs pulse enable # Enable Pulse
|
|
1070
|
+
openkbs pulse status # Check status
|
|
1071
|
+
openkbs pulse channels # List active channels
|
|
1072
|
+
openkbs pulse presence <channel> # View presence
|
|
1073
|
+
openkbs pulse publish <ch> "msg" # Publish message
|
|
1074
|
+
openkbs pulse disable # Disable
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
## CLI Reference
|
|
1080
|
+
|
|
1081
|
+
### General Commands
|
|
1082
|
+
|
|
1083
|
+
```bash
|
|
1084
|
+
openkbs init # Initialize project
|
|
1085
|
+
openkbs deploy # Deploy everything
|
|
1086
|
+
openkbs ls # List your KBs
|
|
1087
|
+
openkbs status # Show project status
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Postgres Commands
|
|
1091
|
+
|
|
1092
|
+
```bash
|
|
1093
|
+
openkbs postgres enable # Enable database
|
|
1094
|
+
openkbs postgres status # Check status
|
|
1095
|
+
openkbs postgres connection # Get connection string
|
|
1096
|
+
openkbs postgres disable # Disable (deletes data!)
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### Storage Commands
|
|
1100
|
+
|
|
1101
|
+
```bash
|
|
1102
|
+
openkbs storage enable # Enable storage
|
|
1103
|
+
openkbs storage status # Check status
|
|
1104
|
+
openkbs storage public true|false # Set public access
|
|
1105
|
+
openkbs storage cloudfront <path> # Add CloudFront path
|
|
1106
|
+
openkbs storage cloudfront remove <path> # Remove CloudFront path
|
|
1107
|
+
openkbs storage ls [prefix] # List files
|
|
1108
|
+
openkbs storage put <file> [key] # Upload file
|
|
1109
|
+
openkbs storage get <key> [file] # Download file
|
|
1110
|
+
openkbs storage rm <key> # Delete file
|
|
1111
|
+
openkbs storage disable --force # Disable (dangerous!)
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
### Function Commands
|
|
1115
|
+
|
|
1116
|
+
```bash
|
|
1117
|
+
openkbs fn list # List functions
|
|
1118
|
+
openkbs fn push <name> # Deploy function
|
|
1119
|
+
openkbs fn push <name> --memory 512 # With memory setting
|
|
1120
|
+
openkbs fn push <name> --timeout 60 # With timeout setting
|
|
1121
|
+
openkbs fn logs <name> # View logs
|
|
1122
|
+
openkbs fn logs <name> --follow # Stream logs live
|
|
1123
|
+
openkbs fn logs <name> --limit 100 # Limit log entries
|
|
1124
|
+
openkbs fn env <name> # View environment variables
|
|
1125
|
+
openkbs fn env <name> KEY=value # Set environment variable
|
|
1126
|
+
openkbs fn env <name> KEY= # Remove environment variable
|
|
1127
|
+
openkbs fn invoke <name> '{json}' # Test invoke function
|
|
1128
|
+
openkbs fn delete <name> # Delete function
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
### Pulse Commands
|
|
1132
|
+
|
|
1133
|
+
```bash
|
|
1134
|
+
openkbs pulse enable # Enable Pulse
|
|
1135
|
+
openkbs pulse status # Check status
|
|
1136
|
+
openkbs pulse channels # List active channels
|
|
1137
|
+
openkbs pulse presence <channel> # View channel presence
|
|
1138
|
+
openkbs pulse publish <ch> "msg" # Publish message to channel
|
|
1139
|
+
openkbs pulse disable # Disable Pulse
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
|
|
1144
|
+
## Example Apps
|
|
1145
|
+
|
|
1146
|
+
For complete working examples, see the tutorials:
|
|
1147
|
+
|
|
1148
|
+
- **[Node.js Full-Stack App](/tutorials/nodejs-fullstack/)** - Social app with posts, real-time chat, image uploads, and presence
|
|
1149
|
+
- **[Java REST API](/tutorials/java-api/)** - CRUD API with PostgreSQL
|
|
1150
|
+
- **[Python API](/tutorials/python-api/)** - REST API with S3 file uploads
|