Backend functions allow you to execute server-side logic from your Spooky application. Currently, Spooky supports backend integration through the outbox pattern, where client operations create job records that are processed asynchronously by a backend service.
How It Works
The outbox pattern decouples your frontend from backend services:
Your client calls db.run() to create a job record in SurrealDB
A separate job runner service polls for pending jobs
The job runner executes the HTTP request to your backend API
The job status is updated in the database
Your client can query the job status reactively
Note
Direct backend calls may be supported in future versions. For now, the outbox pattern is the recommended approach for all backend integrations.
Configuration
Backend functions are configured in your spooky.yml file in the schema directory.
type: The backend type (currently only http is supported)
baseUrl: The base URL of your HTTP backend service
spec: Path to the OpenAPI specification file that defines your API routes
auth (optional): Authentication configuration for the backend
type: Authentication type (currently only token is supported)
token: The bearer token to include in HTTP requests
method.type: The method for calling the backend (use outbox for the outbox pattern)
method.table: The name of the job table in SurrealDB (e.g., job)
method.schema: Path to the SurrealQL schema file that defines the job table structure
Authentication
When you configure an auth block in your backend configuration, the job runner will automatically include the token in all HTTP requests to that backend using the Authorization: Bearer <token> header.
This is useful for securing your backend APIs and ensuring that only authorized job runners can execute requests.
yaml
# Backend with authenticationbackends:api: type: http baseUrl: https://api.example.com spec: ../api/openapi.yml auth: type: token token: ${API_TOKEN} # Can use environment variables method: type: outbox table: job schema: ./src/outbox/api.surql
On your backend server, validate the bearer token in your middleware:
TypeScript
import { bearerAuth } from 'hono/bearer-auth';app.use('/*', bearerAuth({token: process.env.API_AUTH_TOKEN}));
OpenAPI Specification
Your backend routes are defined using an OpenAPI specification. Each path becomes a callable route from your Spooky client.
yaml
openapi: 3.1.0info:version: 1.0.0title: examplepaths:/spookify: post: requestBody: content: application/json: schema: type: object properties: id: type: string example: thread:kv9b3b... required: - id responses: '200': description: ok '404': description: Thread not found '500': description: Internal server error
The request body schema defines the parameters that will be validated when you call db.run(). In this example, the /spookify endpoint requires an id parameter.
Job Table Schema
The job table stores outbox records and tracks their execution status. Here’s the required schema:
sql
DEFINE TABLE job SCHEMAFULLPERMISSIONS FOR select, create, update, delete WHERE $access = "account" AND assigned_to.author.id = $auth.id;-- Link to parent record (e.g., thread, user)DEFINE FIELD assigned_to ON TABLE job TYPE record<thread>PERMISSIONS FOR create, select WHERE true FOR update WHERE false;-- API route pathDEFINE FIELD path ON TABLE job TYPE stringPERMISSIONS FOR create, select WHERE true FOR update WHERE false;-- JSON payload for the requestDEFINE FIELD payload ON TABLE job TYPE anyPERMISSIONS FOR create, select WHERE true FOR update WHERE false;-- Retry configurationDEFINE FIELD retries ON TABLE job TYPE int DEFAULT ALWAYS 0PERMISSIONS FOR create, select WHERE true FOR update WHERE false;DEFINE FIELD max_retries ON TABLE job TYPE int DEFAULT ALWAYS 3;DEFINE FIELD retry_strategy ON TABLE job TYPE string DEFAULT ALWAYS "linear"ASSERT $value IN ["linear", "exponential"]PERMISSIONS FOR create, select WHERE true FOR update WHERE false;-- Job status lifecycleDEFINE FIELD status ON TABLE job TYPE string DEFAULT ALWAYS "pending"ASSERT $value IN ["pending", "processing", "success", "failed"]PERMISSIONS FOR create, select WHERE true FOR update WHERE false;-- Error trackingDEFINE FIELD errors ON TABLE job TYPE array<object> DEFAULT ALWAYS []PERMISSIONS FOR create WHERE true FOR select, update WHERE false;-- TimestampsDEFINE FIELD updated_at ON TABLE job TYPE datetimeDEFAULT ALWAYS time::now()PERMISSIONS FOR create, select WHERE true FOR update WHERE false;DEFINE FIELD created_at ON TABLE job TYPE datetimeVALUE time::now()PERMISSIONS FOR create, select WHERE true FOR update WHERE false;
Key Fields
assigned_to: Links the job to a parent record (optional, but useful for querying jobs by entity)
path: The API route path from your OpenAPI spec
payload: JSON payload containing the request parameters
status: Current job state: pending, processing, success, or failed
retries: Number of retry attempts so far
max_retries: Maximum number of retry attempts before marking as failed
retry_strategy: Retry timing strategy (linear or exponential)
errors: Array of error objects from failed execution attempts
Calling Backend Functions
Use the db.run() method to create a job and call your backend function:
db.run(backend: string, // Backend name from spooky.ymlpath: string, // Route path from OpenAPI specpayload: object, // Request parametersoptions?: { assignedTo?: string, // Link to parent record max_retries?: number, // Override default max retries retry_strategy?: 'linear' | 'exponential' // Override retry strategy})
Parameters
backend: The name of the backend from your spooky.yml (e.g., 'api')
path: The API route path (e.g., '/spookify')
payload: An object containing the request parameters defined in your OpenAPI schema
options (optional):
assignedTo: Record ID to link this job to (useful for querying related jobs)
max_retries: Override the default maximum retry attempts (default: 3)
retry_strategy: Override the retry strategy (default: 'linear')
Warning
The db.run() method validates the payload against your OpenAPI schema. Missing required parameters will throw an error.
Querying Job Status
Jobs are stored as regular SurrealDB records, so you can query them using the query builder. The most common pattern is to use .related() to fetch jobs for a specific entity: