Github|...

Introduction

Sp00ky is a reactive, local-first framework for SurrealDB. Write a query once and it stays correct, live, offline, and typed end to end.

Threads.tsx
import { useQuery } from '@spooky-sync/client-solid';
import { For } from 'solid-js';
import { db } from './db';

function Threads() {
  const threads = useQuery(db, () =>
    db.query('thread')
      .related('author')
      .orderBy('created_at', 'desc')
      .limit(20)
      .build()
  );

  return (
    <For each={threads.data()}>
      {(thread) => (
        <div>
          <h2>{thread.title}</h2>
          <span>{thread.author.username}</span>
        </div>
      )}
    </For>
  );
}

That component is live, typed, and works offline. There is no fetch call, no cache invalidation, no WebSocket setup, no loading-state machine, and no hand-written types. You didn’t opt into any of it.

Three things to know

Your schema is the source of truth

Write plain SurrealQL. spky generate turns it into typed clients, and the same file drives sync rules and row-level permissions.

schema.surql
DEFINE TABLE thread SCHEMAFULL;
DEFINE FIELD title      ON TABLE thread TYPE string;
DEFINE FIELD author     ON TABLE thread TYPE record<user>;
DEFINE FIELD created_at ON TABLE thread TYPE datetime VALUE time::now();
schema.gen.ts
type Thread = {
  id: RecordId<"thread">;
  title: string;
  author: RecordId<"user">;
  created_at: Date;
};

No drift, no duplicated model definitions, no guessing what shape a row is.

Queries are maintained, not refetched

A sidecar keeps each registered query’s result up to date incrementally and tells clients only what changed. When data moves, whether from another user, another device, another tab, or a background job, your UI re-renders.

await db.create(threadId, { title: 'Hello world' });
// Every useQuery('thread') updates. Everywhere. Instantly.

Backend calls go through the database

db.run() writes a row instead of making an HTTP request. The server picks it up and calls your service. That one change makes server work offline-safe, retryable, and visible in the UI as an ordinary live query.

await db.run('api', '/spookify', { id: thread.id });
// Queued offline, retried on failure, visible as a row you can subscribe to.

Pick your path

Explore