Github|...

What is a backend

Your own HTTP service, called through the database instead of over the network. That one change is what makes server calls reactive and offline-safe.

A backend is your own HTTP service (any language, any framework) registered in sp00ky.yml and described by an OpenAPI spec. You call it from the client like this:

await db.run('api', '/spookify', { id: thread.id });

Note what’s missing: no URL, no fetch, no error handling for a dropped connection, no loading flag. db.run() doesn’t make an HTTP request.

The outbox model

db.run() writes a row to a table in SurrealDB. Sp00ky’s job runner picks that row up and makes the HTTP call to your service on the server side, then writes the result back onto the same row.

sequenceDiagram
    participant App as Your app
    participant DB as SurrealDB
    participant Runner as Job runner (SSP)
    participant Backend as Your backend

    App->>DB: db.run() writes a job row
    DB-->>App: row appears in the live query (status: pending)
    DB->>Runner: change event
    Runner->>Backend: POST /your-route
    Backend-->>Runner: 200 + result
    Runner->>DB: UPDATE row (status: success)
    DB-->>App: live notification, UI updates

The row is an ordinary synced record, so job state is application state. You subscribe to it the same way you subscribe to anything else, and the progress spinner in your UI is just a live query.

// the job row is a normal reactive query
const jobs = useQuery(db, () =>
  db.query('job')
    .where({ status: 'pending' })
    .build()
);

<Show when={jobs.data()?.length}>Working…</Show>

Why go through the database

Direct HTTP callSp00ky outbox
OfflineFailsQueued locally, sent when back online
Retry on failureYou write itConfigurable, with backoff
TimeoutsYou write itPer-backend, optionally per-call
Progress in the UIComponent stateA live row, visible in every tab and device
Survives a page reloadNoYes. The row is still there
Cancel a running jobHardspky jobs kill, or set a field
AuditabilityLogs, if you kept themEvery call is a row you can query

The trade-off is that calls are asynchronous by design. db.run() resolves when the row is written, not when your backend replies. If you need a synchronous request/response, call your service directly with fetch. Sp00ky doesn’t stop you, you just lose everything in the right-hand column.

Note

The same machinery powers Jobs, Schedules and Workflows. A schedule creates job rows on a cadence; a workflow runs a DAG of them. Learn the job row and you know all three.

Other app types

apps: in sp00ky.yml holds more than backends. Every entry has a type:

TypeWhat it is
backendAn HTTP service invoked through the outbox. This page.
frontendYour web app. Deployed as a Docker image, or as a static SPA on Cloudflare Workers.
dockerA prebuilt image run alongside the stack, a mail catcher, a cache, a third-party service. Usually scope: devOnly.

Next