SDK
TypeScript SDK for Backflow API with auto-discovery and middleware support.
Installation
npm install @backflow/sdkLocal Development
Link the SDK locally for development:
# In backflow/apps/sdk
npm link
# In your project
npm link @backflow/sdkOr use file reference in package.json:
{
"dependencies": {
"@backflow/sdk": "file:../backflow/apps/sdk"
}
}Local Server
Run Backflow locally:
# Start local server
npm run dev
# Use local endpoint
const bf = createBackflow({
tenantId: 'my-tenant',
apiKey: 'your-jwt-token',
endpoint: 'http://localhost:3000'
});CLI & AI Agent Setup
With Claude CLI (MCP)
Configure in ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"backflow": {
"command": "npx",
"args": ["-y", "@backflow/mcp-server"],
"env": {
"BACKFLOW_API_KEY": "your-api-key",
"BACKFLOW_TENANT_ID": "your-tenant-id",
"BACKFLOW_ENDPOINT": "http://localhost:3000"
}
}
}
}With Any AI CLI
Create a script for AI agents:
// scripts/bf-cli.ts
import { createBackflow } from '@backflow/sdk';
const bf = createBackflow({
tenantId: process.env.BACKFLOW_TENANT_ID!,
apiKey: process.env.BACKFLOW_API_KEY!,
endpoint: process.env.BACKFLOW_ENDPOINT || 'http://localhost:3000'
});
const [,, command, ...args] = process.argv;
const commands: Record<string, () => Promise<unknown>> = {
'projects:list': () => bf.projects.list(),
'projects:get': () => bf.projects.get(args[0]),
'projects:create': () => bf.projects.create(JSON.parse(args[0])),
'workflows:run': () => bf.workflows.execute(JSON.parse(args[0])),
'llm:chat': () => bf.llm.complete(args.join(' ')),
};
commands[command]?.().then(console.log).catch(console.error);Run with:
# Set env vars
export BACKFLOW_API_KEY="your-key"
export BACKFLOW_TENANT_ID="your-tenant"
export BACKFLOW_ENDPOINT="http://localhost:3000"
# Execute commands
npx tsx scripts/bf-cli.ts projects:list
npx tsx scripts/bf-cli.ts llm:chat "Explain this code"Environment Variables
| Variable | Description |
|---|---|
BACKFLOW_API_KEY | JWT or API key for auth |
BACKFLOW_TENANT_ID | Your tenant identifier |
BACKFLOW_ENDPOINT | API endpoint (default: https://api.backflow.dev) |
Quick Start
import { createBackflow } from '@backflow/sdk';
const bf = createBackflow({
apiKey: 'bf_live_xxx',
tenantId: 'my-tenant-id',
});
// Typed resources
await bf.files.upload(file, { entityType: 'project', entityId: 'proj-123' });
await bf.workflows.execute({ steps: [...] });
await bf.tenant.secrets.set('API_KEY', 'secret-value');
// Dynamic resources (tenant-defined routes)
await bf.projects.list();
await bf.projects.create({ name: 'New Project' });
await bf.projects('proj-123').versions.list();Configuration
interface BackflowConfig {
apiKey?: string; // API key auth (x-api-key header)
clientKey?: string; // Alias for apiKey
getAuthToken?: () => Promise<string>; // Token auth (Authorization header)
clientId: string; // Required app identifier
endpoint?: string; // Defaults to https://api.backflow.dev
middleware?: BackflowMiddleware; // Request/response hooks
debug?: boolean; // Log requests/responses
}Authentication
API Key Auth (Server-to-Server)
const bf = createBackflow({
clientId: 'my-app',
apiKey: '<your-api-key>', // Sent as x-api-key header
});- Tenant derived automatically from the key
- No separate tenant ID needed - the key IS the auth
- Best for: backend services, scripts, CI/CD
Token Auth (User Sessions)
import { createBackflow } from '@backflow/sdk';
import { getAuth } from 'firebase/auth';
const bf = createBackflow({
clientId: 'my-app',
getAuthToken: async () => {
const user = getAuth().currentUser;
return user?.getIdToken() || '';
}
});- Token must contain
tenant_idclaim - Sent as
Authorization: Bearerheader - Best for: browser apps, user-specific actions
Combined Auth
Both can be used together - API key for tenant identity, token for user identity:
const bf = createBackflow({
clientId: 'my-app',
apiKey: '<your-api-key>',
getAuthToken: () => auth.currentUser?.getIdToken(),
});Middleware
Intercept requests/responses for analytics, logging, or mutation.
import { createBackflow, ResponseContext } from '@backflow/sdk';
const bf = createBackflow({
tenantId: 'my-tenant',
apiKey: 'bf_live_xxx',
middleware: {
onRequest: (ctx) => {
console.log(`→ ${ctx.method} ${ctx.path}`);
// Optionally mutate and return modified context
return ctx;
},
onResponse: (ctx: ResponseContext) => {
// Track analytics
analytics.track('api_call', {
method: ctx.method,
path: ctx.path,
status: ctx.status,
duration: ctx.duration,
});
}
}
});Middleware Types
interface RequestContext {
method: string;
path: string;
params?: Record<string, unknown>;
body?: unknown;
startTime: number;
}
interface ResponseContext extends RequestContext {
status: number;
duration: number;
response?: unknown;
error?: Error;
}
interface BackflowMiddleware {
onRequest?: (ctx: RequestContext) => RequestContext | void | Promise<RequestContext | void>;
onResponse?: (ctx: ResponseContext) => void | Promise<void>;
}Typed Resources
Files
// Upload
const result = await bf.files.upload(file, {
entityType: 'project',
entityId: 'proj-123',
});
// Multiple files
const results = await bf.files.uploadMultiple(files, {
entityType: 'project',
entityId: 'proj-123',
});
// List
const files = await bf.files.list('project', 'proj-123');
// Signed URL
const { url } = await bf.files.getSignedUrl('bucket', 'path/to/file.pdf');
// Delete
await bf.files.delete('bucket', 'path/to/file.pdf');
await bf.files.deleteByEntity('project', 'proj-123');Workflows
// Execute
const execution = await bf.workflows.execute({
steps: [
{ id: 'fetch', action: 'api_call', params: { url: '...' } },
{ id: 'analyze', action: 'llm_call', params: { prompt: '...' } },
],
});
// Subscribe to progress (WebSocket)
const unsubscribe = bf.workflows.subscribe(execution.id, {
onProgress: (event) => console.log('Step:', event.stepId, event.status),
onComplete: (result) => console.log('Done:', result),
onError: (err) => console.error(err),
});
// Control
await bf.workflows.pause(execution.id);
await bf.workflows.resume(execution.id);
await bf.workflows.cancel(execution.id);
await bf.workflows.retry(execution.id);
// History
const { data } = await bf.workflows.list({ limit: 10 });
const { history } = await bf.workflows.getHistory(execution.id);Human-in-the-Loop
Add pausePoint to steps requiring approval or user input:
const execution = await bf.workflows.execute({
allowPause: true,
steps: [
{ id: 'generate', action: 'llm_call', params: { prompt: '...' } },
{
id: 'review',
action: 'llm_call',
params: { prompt: 'Format the response...' },
pausePoint: {
message: 'Review AI response before sending',
requireApproval: true,
metadata: { options: ['approve', 'reject', 'edit'] },
notify: {
enabled: true,
userId: 'user-123',
channels: ['push', 'email'],
},
},
},
{ id: 'send', action: 'api_call', params: { url: '...' } },
],
});
// Listen for pause
bf.workflows.subscribe(execution.id, {
onProgress: (event) => {
if (event.status === 'paused') {
showApprovalDialog(event.pauseMetadata);
}
},
});
// Resume with user input (accessible as _userInput in subsequent steps)
await bf.workflows.resume(execution.id, {
userInput: { approved: true, feedback: 'Looks good' },
});Tenant & Secrets
// Config
const config = await bf.tenant.getConfig();
await bf.tenant.updateConfig({ settings: { theme: 'dark' } });
// Secrets
await bf.tenant.secrets.set('STRIPE_KEY', 'sk_xxx', {
provider: 'stripe',
description: 'Production API key',
});
const secrets = await bf.tenant.secrets.list();
await bf.tenant.secrets.rotate('STRIPE_KEY', 'sk_new_xxx');
await bf.tenant.secrets.delete('OLD_KEY');Apps (Sub-Tenants)
// List apps
const apps = await bf.tenant.apps.list();
// Create app
const app = await bf.tenant.apps.create('Production', {
environment: 'prod'
});
// Delete app
await bf.tenant.apps.delete('app-id');Custom Domains
// List domains
const domains = await bf.tenant.domains.list();
// Add domain with verification
const result = await bf.tenant.domains.add('api.mycompany.com', 'primary');
console.log(result.verificationInstructions.dnsRecord);
// Verify domain ownership
const { verified } = await bf.tenant.domains.verify('api.mycompany.com');
// Get verification instructions
const instructions = await bf.tenant.domains.getVerificationInstructions('api.mycompany.com');
// Remove domain
await bf.tenant.domains.remove('api.mycompany.com');SSE Subscriptions
Subscribe to Server-Sent Events for async job progress:
// Embed with subscription
const response = await bf.embeddings.embedDocument({
document: 'Long document...',
documentId: 'doc-123',
subscribable: true,
});
// Subscribe to job events
const unsubscribe = bf.client.subscribeSSE(response.subscribe.sse, {
onConnected: () => console.log('Connected'),
onProgress: (progress, message) => {
console.log(`${progress}% - ${message}`);
},
onCompleted: (data) => {
console.log('Done:', data.documentId);
unsubscribe();
},
onFailed: (error) => {
console.error('Failed:', error);
},
});React Example
function EmbedButton({ file }: { file: File }) {
const [status, setStatus] = useState<'idle' | 'progress' | 'done' | 'error'>('idle');
const [progress, setProgress] = useState(0);
const handleEmbed = async () => {
const text = await file.text();
const response = await bf.embeddings.embedDocument({
document: text,
documentId: file.name,
subscribable: true,
});
setStatus('progress');
bf.client.subscribeSSE(response.subscribe.sse, {
onProgress: (p) => setProgress(p),
onCompleted: () => setStatus('done'),
onFailed: () => setStatus('error'),
});
};
return (
<button onClick={handleEmbed} disabled={status === 'progress'}>
{status === 'progress' ? `${progress}%` : 'Embed'}
</button>
);
}SSE Handler Types
interface SSESubscribeHandlers<T = unknown> {
onConnected?: () => void;
onCompleted?: (data: T) => void;
onFailed?: (error: string) => void;
onProgress?: (progress: number, message?: string) => void;
onWarning?: (message: string) => void;
onError?: (error: Event) => void;
}Subscribable Endpoints
Routes that support async processing return a SubscribableResponse:
interface SubscribableResponse {
accepted: true;
jobId: string;
subscribe: {
sse: string; // SSE endpoint (recommended)
websocket: { path: string; channel: string };
poll: string; // Polling fallback
};
}Use bf.client.subscribeSSE(response.subscribe.sse, handlers) to listen for events.
Embeddings
// Single document
const result = await bf.embeddings.embedDocument({
document: 'Your document text...',
documentId: 'doc-123',
contentType: 'article',
});
// Batch
const batchResult = await bf.embeddings.embedBatch([
{ document: 'Doc 1...', documentId: 'doc-1' },
{ document: 'Doc 2...', documentId: 'doc-2' },
]);
// Search
const { results } = await bf.embeddings.search('doc-123', 'query text', 5);
const { results } = await bf.embeddings.semanticSearch('find similar content');
// Delete
await bf.embeddings.delete('doc-123');LLM
// Chat
const response = await bf.llm.chat([
{ role: 'user', content: 'Hello!' }
], { model: 'gpt-4', temperature: 0.7 });
// Simple completion
const response = await bf.llm.complete('Explain quantum computing');
// List models
const { models } = await bf.llm.models();Usage
const myUsage = await bf.usage.getMyUsage();
const userUsage = await bf.usage.getUserUsage('user-123');
const summary = await bf.usage.getSummary('user-123');Entities
CRUD operations for entity collections with soft delete support.
// List records (excludes deleted by default)
const { data, count } = await bf.entities.list('users', {
limit: 10,
offset: 0
});
// Include soft-deleted records
const allUsers = await bf.entities.list('users', { includeDeleted: true });
// Get single record
const user = await bf.entities.get('users', 'user-123');
// Get including deleted
const deletedUser = await bf.entities.get('users', 'user-123', { includeDeleted: true });
// Create
const newUser = await bf.entities.create('users', { name: 'John', email: 'john@example.com' });
// Update
const updated = await bf.entities.update('users', 'user-123', { name: 'John Doe' });
// Delete (soft delete by default)
const result = await bf.entities.delete('users', 'user-123');
// Returns: { success: true, softDeleted: true }
// Force hard delete
await bf.entities.delete('users', 'user-123', { hard: true });
// Disable soft delete for this request
await bf.entities.delete('users', 'user-123', { softDelete: false });
// Restore soft-deleted record
const restored = await bf.entities.restore('users', 'user-123');Types
interface ListEntitiesOptions {
limit?: number;
offset?: number;
includeDeleted?: boolean;
}
interface GetEntityOptions {
includeDeleted?: boolean;
}
interface DeleteEntityOptions {
hard?: boolean; // Force permanent deletion
softDelete?: boolean; // Set to false to disable soft delete
}
interface DeleteEntityResult {
success: boolean;
softDeleted?: boolean;
}
interface RestoreEntityResult {
success: boolean;
data?: EntityRecord;
}Dynamic Resources
For tenant-defined routes (auto-detected from config):
// Standard CRUD
const projects = await bf.projects.list(); // GET /projects
const project = await bf.projects.get('proj-123'); // GET /projects/proj-123
await bf.projects.create({ name: 'New Project' }); // POST /projects
await bf.projects.update('proj-123', { name: 'Updated' }); // PUT /projects/proj-123
await bf.projects.delete('proj-123'); // DELETE /projects/proj-123
// Nested resources with callable proxy
await bf.projects('proj-123').versions.list(); // GET /projects/proj-123/versions
await bf.projects('proj-123').shares.create({ userId: 'user-456' });
// Custom actions (method inferred from name)
await bf.projects.sharedWithMe(); // GET /projects/shared-with-me
await bf.teams.listProjects('team-123'); // GET /teams/team-123/projects
await bf.projects.analyze({ id: 'proj-123' }); // POST /projects/analyze
await bf.webhooks('proj-123').trigger({ event }); // POST /webhooks/proj-123/triggerPath Generation
The SDK automatically converts method names to kebab-case paths:
bf.projects.sharedWithMe() // → GET /projects/shared-with-me
bf.projects.updatePublicStatus('id', data) // → PATCH /projects/id/update-public-statusAction prefix stripping: When a method starts with an action verb, only the remainder becomes the path:
bf.teams.listProjects('team-123') // → GET /teams/team-123/projects (not /list-projects)
bf.projects.getVersions('id') // → GET /projects/id/versions
bf.projects.deleteVersion('id') // → DELETE /projects/id/versionMethod Inference
| Action prefix | HTTP Method |
|---|---|
| get, list, fetch, find, search, query, read, show, index | GET |
| shared, my, all, recent, versions, history, status, info, details, metadata, stats, count, check, verify, validate, exists, available | GET |
| delete, remove, destroy, clear, revoke, cancel, unsubscribe | DELETE |
| patch, update, modify, edit | PATCH |
| put, replace, set, save | PUT |
| (default) | POST |
Examples
// These all infer GET
bf.projects.sharedWithMe() // GET (starts with "shared")
bf.projects.myProjects() // GET (starts with "my")
bf.projects.recentActivity() // GET (starts with "recent")
bf.projects.versions('id') // GET (starts with "versions")
bf.projects.checkStatus('id') // GET (starts with "check")
// These infer DELETE
bf.projects.revokeShare('id', 'userId') // DELETE (starts with "revoke")
bf.subscriptions.cancel('id') // DELETE (starts with "cancel")
// These infer POST (default)
bf.projects.analyze(data) // POST
bf.projects.share('id', data) // POST
bf.projects.embed('id', data) // POSTRaw Requests
When needed, access HTTP methods directly:
const data = await bf.get('/custom/endpoint', { param: 'value' });
await bf.post('/custom/endpoint', { body: 'data' });
await bf.put('/custom/endpoint', { body: 'data' });
await bf.patch('/custom/endpoint', { body: 'data' });
await bf.delete('/custom/endpoint');Complex Config Routes
The SDK's dynamic proxy handles standard CRUD routes from config.json automatically. For complex routes, use raw methods.
Standard Routes (Auto-handled)
These config.json routes work with the dynamic proxy:
// config.json
{
"routes": [
{ "path": "/users", "method": "get" },
{ "path": "/users/:id", "method": "get" },
{ "path": "/users", "method": "post" },
{ "path": "/users/:id", "method": "put" },
{ "path": "/users/:id", "method": "delete" }
]
}// SDK calls
bf.users.list(); // GET /users
bf.users.get('123'); // GET /users/123
bf.users.create(data); // POST /users
bf.users.update('123', d); // PUT /users/123
bf.users.delete('123'); // DELETE /users/123Complex Routes (Use Raw Methods)
These patterns require raw bf.get/post/put/delete methods:
1. Routes with /api/ prefix
{ "path": "/api/github/repos/{username}", "method": "get" }// Dynamic proxy generates /github/repos - WRONG
bf.github.repos.get('scipher'); // ❌ GET /github/repos/scipher
// Use raw method
bf.get('/api/github/repos/scipher'); // ✅ GET /api/github/repos/scipher2. Multiple Path Parameters
{ "path": "/api/github/repos/{owner}/{repo}/contents/{path}", "method": "get" }// No dynamic proxy support for multi-param
bf.get(`/api/github/repos/${owner}/${repo}/contents/${path}`); // ✅3. Nested Custom Actions
{ "path": "/projects/:id/collaborators/:userId/permissions", "method": "patch" }// Deep nesting not supported by proxy
bf.patch(`/projects/${projectId}/collaborators/${userId}/permissions`, data); // ✅When to Use What
| Route Pattern | Use Dynamic Proxy | Use Raw Method |
|---|---|---|
/resource | ✅ bf.resource.list() | |
/resource/:id | ✅ bf.resource.get(id) | |
/resource/:id/action | ✅ bf.resource.action(id) | |
/api/resource | ✅ bf.get('/api/resource') | |
/a/:id/b/:id2/c | ✅ bf.get(\/a/${id}/b/${id2}/c`)` | |
| Custom path structure | ✅ Raw methods |
Helper Pattern
For frequently used complex routes, create wrapper functions:
// In your client file
export async function getGithubRepos(username: string) {
return bf.get<Repo[]>(`/api/github/repos/${username}`);
}
export async function getRepoContents(owner: string, repo: string, path: string) {
return bf.get(`/api/github/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`);
}
export async function updateCollaboratorPerms(projectId: string, userId: string, perms: Perms) {
return bf.patch(`/projects/${projectId}/collaborators/${userId}/permissions`, perms);
}