Skip to content

SDK

TypeScript SDK for Backflow API with auto-discovery and middleware support.

Installation

bash
npm install @backflow/sdk

Local Development

Link the SDK locally for development:

bash
# In backflow/apps/sdk
npm link

# In your project
npm link @backflow/sdk

Or use file reference in package.json:

json
{
  "dependencies": {
    "@backflow/sdk": "file:../backflow/apps/sdk"
  }
}

Local Server

Run Backflow locally:

bash
# 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:

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:

typescript
// 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:

bash
# 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

VariableDescription
BACKFLOW_API_KEYJWT or API key for auth
BACKFLOW_TENANT_IDYour tenant identifier
BACKFLOW_ENDPOINTAPI endpoint (default: https://api.backflow.dev)

Quick Start

typescript
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

typescript
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)

typescript
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)

typescript
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_id claim
  • Sent as Authorization: Bearer header
  • Best for: browser apps, user-specific actions

Combined Auth

Both can be used together - API key for tenant identity, token for user identity:

typescript
const bf = createBackflow({
  clientId: 'my-app',
  apiKey: '<your-api-key>',
  getAuthToken: () => auth.currentUser?.getIdToken(),
});

Middleware

Intercept requests/responses for analytics, logging, or mutation.

typescript
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

typescript
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

typescript
// 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

typescript
// 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:

typescript
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

typescript
// 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)

typescript
// 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

typescript
// 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:

typescript
// 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

tsx
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

typescript
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:

typescript
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

typescript
// 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

typescript
// 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

typescript
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.

typescript
// 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

typescript
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):

typescript
// 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/trigger

Path Generation

The SDK automatically converts method names to kebab-case paths:

typescript
bf.projects.sharedWithMe()     // → GET /projects/shared-with-me
bf.projects.updatePublicStatus('id', data)  // → PATCH /projects/id/update-public-status

Action prefix stripping: When a method starts with an action verb, only the remainder becomes the path:

typescript
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/version

Method Inference

Action prefixHTTP Method
get, list, fetch, find, search, query, read, show, indexGET
shared, my, all, recent, versions, history, status, info, details, metadata, stats, count, check, verify, validate, exists, availableGET
delete, remove, destroy, clear, revoke, cancel, unsubscribeDELETE
patch, update, modify, editPATCH
put, replace, set, savePUT
(default)POST

Examples

typescript
// 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)    // POST

Raw Requests

When needed, access HTTP methods directly:

typescript
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:

json
// 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" }
  ]
}
typescript
// 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/123

Complex Routes (Use Raw Methods)

These patterns require raw bf.get/post/put/delete methods:

1. Routes with /api/ prefix

json
{ "path": "/api/github/repos/{username}", "method": "get" }
typescript
// 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/scipher

2. Multiple Path Parameters

json
{ "path": "/api/github/repos/{owner}/{repo}/contents/{path}", "method": "get" }
typescript
// No dynamic proxy support for multi-param
bf.get(`/api/github/repos/${owner}/${repo}/contents/${path}`);  // ✅

3. Nested Custom Actions

json
{ "path": "/projects/:id/collaborators/:userId/permissions", "method": "patch" }
typescript
// Deep nesting not supported by proxy
bf.patch(`/projects/${projectId}/collaborators/${userId}/permissions`, data);  // ✅

When to Use What

Route PatternUse Dynamic ProxyUse Raw Method
/resourcebf.resource.list()
/resource/:idbf.resource.get(id)
/resource/:id/actionbf.resource.action(id)
/api/resourcebf.get('/api/resource')
/a/:id/b/:id2/cbf.get(\/a/${id}/b/${id2}/c`)`
Custom path structure✅ Raw methods

Helper Pattern

For frequently used complex routes, create wrapper functions:

typescript
// 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);
}

Released under the ISC License.