@vibes.diy/api-sql
v2.0.9
Published
WebSocket-based API for vibes.diy chat persistence and app deployment. Supports both HTTP POST and WebSocket connections using the same handler via the Evento event-driven framework.
Downloads
1,833
Readme
vibes.diy API
WebSocket-based API for vibes.diy chat persistence and app deployment. Supports both HTTP POST and WebSocket connections using the same handler via the Evento event-driven framework.
Package Structure
api/
├── types/ @vibes.diy/api-types SHARED (client + server)
│ ├── msg-types.ts Request/response types, VibeFile, MsgBase envelope, arktype schemas
│ ├── types.ts FileSystemItem with transform support
│ └── vibes-diy-serv-ctx.ts Server context for wrapper/iframe rendering
│
├── pkg/ @vibes.diy/api-pkg SHARED (client + server)
│ ├── api.ts VibesDiyApiIface interface (3 methods)
│ ├── encoder.ts Evento encoders: ReqRes, W3CWebSocket, Combined
│ ├── index.ts Re-exports
│ └── react/ React components (client only in practice)
│
├── impl/ @vibes.diy/api-impl CLIENT
│ └── index.ts VibeDiyApi class (WebSocket client, WIP - no reconnection)
│
├── svc/ @vibes.diy/api-svc SERVER (Cloudflare Workers)
│ ├── cf-serve.ts Cloudflare Worker entry point (HTTP + WebSocket)
│ ├── create-handler.ts Evento pipeline setup with all handlers
│ ├── check-auth.ts Auth middleware (clerk, device-id verification)
│ ├── unwrap-msg-base.ts MsgBase envelope unwrapping for validation
│ ├── api.ts VibesApiSQLCtx type + VibesFPApiParameters
│ ├── entry-point-utils.ts URL construction for deployed apps
│ │
│ ├── public/ Public API handlers (Evento handlers)
│ │ ├── ensure-app-slug-item.ts App deployment with CID-based storage
│ │ ├── ensure-chat-context.ts Chat session creation/retrieval
│ │ ├── append-chat-section.ts Message persistence with seq numbers
│ │ └── serv-entry-point.ts Serves deployed app HTML
│ │
│ ├── intern/ Internal helpers
│ │ ├── ensure-slug-binding.ts User/app slug management (random-words)
│ │ ├── ensure-storage.ts CID calculation (SHA-256 + base58btc)
│ │ ├── import-map.ts ESM import map generation (esm.sh)
│ │ ├── render-vibes.tsx HTML rendering with import map injection
│ │ └── write-apps.ts App record creation with transforms (sucrase, acorn)
│ │
│ └── sql/
│ └── vibes-diy-api-schema.ts Drizzle schema (D1)
│
└── tests/ @vibes.diy/api-test
└── api.test.tsPackage Summary
| Package | Runs On | Purpose |
| ---------------------- | ------- | --------------------------------------- |
| @vibes.diy/api-types | Both | arktype schemas, request/response types |
| @vibes.diy/api-pkg | Both | Interface + encoders (shared contract) |
| @vibes.diy/api-impl | Client | VibeDiyApi WebSocket client class |
| @vibes.diy/api-svc | Server | Cloudflare Worker handlers, D1/Drizzle |
Architecture
Message Envelope
All requests are wrapped in a MsgBase envelope:
interface MsgBase {
tid: string; // Transaction ID for request/response correlation
src: string; // Source identifier
dst: string; // Destination identifier
ttl: number; // Time-to-live
payload: unknown; // The actual request/response
}Evento Pipeline
The server uses Evento (from @adviser/cement) for request routing. Handlers are registered in order:
- CORS preflight - Handles OPTIONS requests
- servEntryPoint - Serves deployed app HTML (hostname routing:
{appSlug}--{userSlug}.{host}/~{fsId}~/) - Request logging - Logs incoming POST/PUT requests, rejects other methods with 503
- ensureAppSlugItem - App deployment handler
- ensureChatContext - Chat session handler
- appendChatSection - Message persistence handler
- Not-found handler - Returns 501 for unmatched requests (semantically "Not Implemented")
- Error handler - Catches and formats errors as 500
Each handler has validate (arktype schema check) and handle (business logic) phases. Some handlers also have a post phase for cleanup/logging.
Data Flow
Client (browser) Server (CF Worker)
───────────────── ──────────────────
VibeDiyApi.ensureAppSlug() → cf-serve.ts
↓ ↓
WebSocket.send(MsgBox<Req>) → WebSocketPair upgrade
↓
CombinedEventoEnDecoder.encode()
↓
Evento.trigger() with handlers
↓
unwrapMsgBase() → extract payload
↓
checkAuth() → verify token
↓
handler.handle() → business logic
↓
D1 queries (Drizzle ORM)
↓
MsgBox<Res> ← SendResponseProvider.send()API Methods
ensureAppSlug()
Deploy an app with a filesystem. Process:
- Verify auth token (clerk/device-id)
- Create or retrieve user slug binding
- Create or retrieve app slug binding
- Calculate CIDs for all file contents
- Store assets in D1
Assetstable - Apply transforms (jsx-to-js, import map generation)
- Create
Appsrecord with filesystem manifest - Return
entryPointUrlfor iframe loading
ensureChatContext()
Create or retrieve a chat session:
- If
contextIdprovided, verify it exists and belongs to user - Otherwise, generate new contextId (12-char ID)
- Insert into
ChatContextstable - Return
contextId
appendChatSection()
Add messages to a chat context:
- Verify context exists and belongs to user
- Get max
seqfor context, increment - Insert into
ChatSectionswithorigin('user' or 'llm') - Blocks use
BlockMsgs | PromptMsgtypes from@vibes.diy/call-ai-v2 - Return
seqnumber
CI/CD
The API is bundled into the main vibes.diy Cloudflare Worker (not a separate deployment).
Workflow
.github/workflows/vibes-diy-deploy.yaml
Triggers:
- Push to
vibes.diy/**/*paths - Tags:
vibes-diy@* - Manual dispatch
Environments
| Trigger | GH Environment | CF Env | Domain |
| ----------------------- | -------------- | ------ | ------------------------ |
| Tag vibes-diy@p* | prodv2 | prod | *.prod-v2.vibesdiy.net |
| Tag vibes-diy@c* | cli | cli | *.cli-v2.vibesdiy.net |
| Tag vibes-diy@s* | staging | dev | *.dev-v2.vibesdiy.net |
| Path push or other tags | dev | dev | *.dev-v2.vibesdiy.net |
CLI shares prodv2's D1 database and Neon DB but has its own worker, queue, and routes.
Secret Rotation
To rotate session tokens and CA certs for prodv2/cli:
./vibes.diy/actions/deploy/gen-prod-secrets.shThis regenerates keys, updates .prod.vars, and sets GH secrets — without exposing values in terminal output.
For the remaining manual secrets (LLM_BACKEND_API_KEY, RESEND_API_KEY, NEON_DATABASE_URL), edit .prod.vars directly and run gh secret set from the repo root.
Deploy Steps
Deployment is handled by the GitHub Action (vibes.diy/actions/deploy). The action runs:
pnpm run build # Build React app + bundle worker
pnpm run drizzle:d1-remote # Run D1 migrations
core-cli writeEnv | wrangler secret bulk # Push secrets
pnpm run deploy:${ENV} # wrangler deployDo not run these manually - push a tag or merge to trigger the workflow.
Infrastructure
| Resource | Binding | Purpose |
| ------------- | -------- | ----------------------------- |
| D1 Database | DB | Chat contexts, sections, apps |
| Static Assets | ASSETS | React app build |
Server Context (VibesApiSQLCtx)
All handlers receive a shared context via ctx.ctx.getOrThrow<VibesApiSQLCtx>("vibesApiCtx"):
interface VibesApiSQLCtx {
sthis: SuperThis; // Fireproof utilities (logger, nextId, env)
db: VibesSqlite; // Drizzle DB instance
tokenApi: Record<string, FPApiToken>; // Auth verifiers by type
deviceCA: DeviceIdCAIf; // Device ID certificate authority
logger: Logger; // Structured logger
params: VibesFPApiParameters; // Service configuration
cache: CfCacheIf; // Cloudflare cache API
fetchPkgVersion(pkg): Promise<string | undefined>; // npm registry lookup
waitUntil<T>(promise): void; // CF worker lifecycle
ensureStorage(...items): Promise<Result<StorageResult[]>>; // Asset storage
}D1 Schema
Defined in svc/sql/vibes-diy-api-schema.ts using Drizzle ORM.
Assets - Binary content storage (could move to R2)
| Column | Type | Description |
|--------|------|-------------|
| assetId | text PK | CID of content |
| content | blob | Actual content |
| created | text | ISO timestamp |
UserSlugBindings - Maps users to their human-friendly slugs
| Column | Type | Description |
|--------|------|-------------|
| userId | text | User identifier |
| userSlug | text | Human-friendly user slug (unique) |
| created | text | ISO timestamp |
Primary key: (userSlug, userId). Unique index on userSlug.
AppSlugBindings - Maps apps to slugs within a user's namespace
| Column | Type | Description |
|--------|------|-------------|
| userSlug | text FK | References UserSlugBindings.userSlug |
| appSlug | text | Human-friendly app slug |
| created | text | ISO timestamp |
Primary key: (appSlug, userSlug).
Apps - Deployed app versions with filesystem and environment
| Column | Type | Description |
|--------|------|-------------|
| appSlug | text | App identifier |
| userId | text | Owner |
| userSlug | text | Owner's slug |
| releaseSeq | int | Incremented on each deployment |
| fsId | text | CID of filesystem manifest |
| env | json | Environment variables (VibesEnv) |
| fileSystem | json | Array of FileSystemItem with transforms |
| mode | text | 'production' or 'dev' |
| created | text | ISO timestamp |
Primary key: (appSlug, userId, releaseSeq). Indexes on fsId and created.
ChatContexts - Chat session containers
| Column | Type | Description |
|--------|------|-------------|
| contextId | text PK | UUID v4 |
| userId | text | Owner |
| created | text | ISO timestamp |
ChatSections - Individual messages/blocks within a chat
| Column | Type | Description |
|--------|------|-------------|
| contextId | text FK | References ChatContexts.contextId |
| seq | int | Section sequence number (0-indexed, auto-incremented) |
| origin | text | 'user' or 'llm' |
| blocks | json | Array of BlockMsgs or PromptMsg from call-ai-v2 |
| created | text | ISO timestamp |
Primary key: (seq, contextId).
Environment Variables
Required:
| Variable | Purpose |
|----------|---------|
| CLOUD_SESSION_TOKEN_PUBLIC | Public key for cloud session tokens |
| CLERK_PUBLISHABLE_KEY | Clerk auth verification |
| DEVICE_ID_CA_PRIV_KEY | Device ID certificate authority private key |
| DEVICE_ID_CA_CERT | Device ID certificate authority cert |
| FP_VERSION | Fireproof version for import maps |
| VIBES_SVC_HOSTNAME_BASE | Base hostname for deployed apps (e.g., vibes.app) |
Optional (with defaults):
| Variable | Default | Purpose |
|----------|---------|---------|
| VIBES_SVC_PROTOCOL | https | Protocol for deployed apps |
| MAX_APP_SLUG_PER_USER_ID | 10 | Max app slugs per user |
| MAX_USER_SLUG_PER_USER_ID | 10 | Max user slugs per user |
| MAX_APPS_PER_USER_ID | 50 | Max app deployments per user |
Message Types
VibeFile (for deployment)
Six variants for different content types:
| Type | Content | Use Case |
| ------------------- | ------------------------------------ | ------------------------------ |
| code-block | Inline string, lang: 'jsx' \| 'js' | JSX/JS source code |
| code-ref | refId reference | Code stored elsewhere |
| str-asset-block | Inline string | CSS, JSON, text files |
| str-asset-ref | refId reference | String assets stored elsewhere |
| uint8-asset-block | Uint8Array | Images, fonts, binaries |
| uint8-asset-ref | refId reference | Binary assets stored elsewhere |
All file types share base properties:
{
filename: string; // Must start with /, no //, /../, /./
entryPoint?: boolean; // Last one wins, marks app entry
mimetype?: string; // Derived from filename if not set
}FileSystemItem (stored result)
After processing, files become FileSystemItem:
{
fileName: string;
mimeType: string;
assetId: string; // CID of content
assetURI: string; // sql://Assets.assetId, s3://..., r2://...
entryPoint?: boolean;
size: number;
transform?: {
type: 'jsx-to-js' | 'imports' | 'import-map' | 'transformed';
// ... transform-specific fields
}
}Auth Types
type DashAuthType = {
type: "clerk" | "device-id";
token: string;
};Auth is verified via tokenApi which supports:
- clerk - Clerk JWT verification using
CLERK_PUB_JWT_KEY/CLERK_PUB_JWT_URLenv vars - device-id - Device certificate verification using
DEVICE_ID_CA_*keys
Client Usage
[!WARNING] The client (
@vibes.diy/api-impl) is currently a work-in-progress. While it can send requests over WebSocket, the response correlation logic (matching server responses to requests viatid) is not yet fully implemented. It is currently primarily used for testing with direct handler calls.
The client is currently used in tests. The main vibes.diy app doesn't use it yet - integration is in progress.
Creating a Client
import { VibeDiyApi } from "@vibes.diy/api-impl";
import { Result } from "@adviser/cement";
const api = new VibeDiyApi({
apiUrl: "wss://api.vibes.diy/v1/ws", // Default if omitted
getToken: async () => {
// Return auth token (Clerk or device-id)
return Result.Ok({ type: "clerk", token: clerkToken });
},
timeoutMs: 10000, // Default: 10s request timeout
});The client uses KeyedResolvOnce for connection pooling - multiple VibeDiyApi instances with the same apiUrl share a WebSocket connection. (See impl/index.ts for custom message routing options via the msg config.)
Deploying an App
const res = await api.ensureAppSlug({
mode: "dev", // 'dev' or 'production'
appSlug: "my-app", // optional, server generates 3-word slug
userSlug: "my-user", // optional, server generates 3-word slug
env: { API_KEY: "..." }, // optional, passed to app runtime
fileSystem: [
{
type: "code-block",
lang: "jsx",
filename: "/App.jsx", // Must start with /, becomes entry point
entryPoint: true,
content: "export default function App() { return <div>Hello</div>; }",
},
],
});
if (res.isOk()) {
const { entryPointUrl, fsId, userSlug, appSlug, fileSystem } = res.Ok();
// entryPointUrl -> https://{appSlug}--{userSlug}.{hostnameBase}/~{fsId}~/
// e.g. https://my-app--my-user.vibes.app/~zABC123~/
// fsId -> CID of filesystem manifest
// fileSystem -> Array<FileSystemItem> with assetIds and transforms
}Response includes:
entryPointUrl- URL to load in iframe (constructed fromVIBES_SVC_*config)wrapperUrl- URL for wrapper page with auth handofffsId- CID of the filesystem manifestfileSystem- ProcessedFileSystemItem[]with CIDs and transforms applied
Chat Context Management
// Create a new chat context (server generates contextId)
const ctx = await api.ensureChatContext({});
// Or retrieve/create with a specific contextId
const ctx = await api.ensureChatContext({ contextId: "my-context-id" });
if (ctx.isOk()) {
const { contextId } = ctx.Ok();
// Append a user message
const userRes = await api.appendChatSection({
contextId,
origin: "user", // 'user' | 'llm'
blocks: [
// BlockMsgs or PromptMsg from @vibes.diy/call-ai-v2
{
type: "prompt.txt",
streamId: "stream-1",
request: { model: "gpt-4", messages: [{ role: "user", content: "Hello" }] },
timestamp: new Date(),
},
],
});
// userRes.Ok().seq -> 0 (first section)
// Append LLM response
const llmRes = await api.appendChatSection({
contextId,
origin: "llm",
blocks: sectionBlocks, // BlockMsgs from call-ai sections stream
});
// llmRes.Ok().seq -> 1 (second section)
}Note: Context ownership is enforced - you can only append to contexts created by your userId.
Testing
Tests use a local handler directly rather than WebSocket. See api/tests/api.test.ts for the full setup including:
- Creating a local D1/libsql database
- Setting up test device-id auth
- Calling the handler directly
Note: The test passes an ad-hoc fetch property via TypeScript structural typing, but this isn't part of the official VibeDiyApiParam interface. A proper fetch override may be added in the future.
Running Tests
cd vibes.diy/api/tests
pnpm testImplementation Details
URL Structure
Deployed apps are served via hostname-based routing:
https://{appSlug}--{userSlug}.{hostnameBase}/~{fsId}~/
└────────────────────┘ └──────────┘ └──────┘
hostname base domain version pathExample: https://my-app--fuzzy-purple-elephant.vibes.app/~z4PhNX7vuL~/
The extractHostToBindings() function parses:
- Hostname pattern:
{appSlug}--{userSlug}.{rest} - Path pattern:
/~{fsId}~/(fsId starts withz, 8+ chars) - If no fsId in path, serves latest production release
Slug Generation
When appSlug or userSlug are not provided, the server generates 3-word slugs using the random-words package:
generate({ exactly: 1, wordsPerString: 3, separator: "-" });
// → "fuzzy-purple-elephant"Up to 5 attempts are made to find a unique slug before failing.
Transform Pipeline
The write-apps.ts module applies transforms to uploaded files:
JSX → JS (
sucrasewith automatic JSX runtime)- Input:
code-blockwithlang: "jsx" - Output:
/~~transformed~~/{cid}withtransform: { type: "transformed", action: "jsx-to-js" }
- Input:
Import Extraction (
acornparser)- Parses all JS files for
import/exportstatements - Extracts bare specifiers (e.g.,
react,@fireproof/core)
- Parses all JS files for
Import Map Generation
- Unknown packages → npm registry lookup for version →
esm.shURL - Predefined mappings for React 19.2.1, Fireproof, Clerk, etc.
- Output:
/~~calculated~~/import-map.jsonwithtransform: { type: "import-map" }
- Unknown packages → npm registry lookup for version →
fsId Calculation
The fsId is a deterministic CID computed from:
hash(sortedFilenames + mimetypes + contentCIDs + sortedEnvJSON);Same files + same env = same fsId (enables deduplication).
Asset Caching
serv-entry-point.ts uses two-tier Cloudflare caching:
- Global cache by CID:
assetCacheUrl/{assetId} - Path cache by request URL
On cache miss: D1 query → populate both caches via waitUntil().
App Eviction
When a user exceeds MAX_APPS_PER_USER_ID:
- Oldest
devmode apps are evicted first (10% of total + 1) productionapps are preserved
