Github|...

SolidJS Guide

This guide walks you through building a reactive app with SolidJS and Sp00ky.

1. Installation

Install the required dependencies:

Bash
pnpm add @spooky-sync/core @spooky-sync/client-solid surrealdb

2. Generate Schema Types

First, create your schema file and generate TypeScript types:

Bash
# Recommended: drive from sp00ky.yml's clientTypes entries
spky generate

# One-shot variant
spky --input ./schema/src/schema.surql --output ./src/schema.gen.ts --format typescript

This generates a schema.gen.ts file with your schema definition and types.

3. Initialize Database

Create a db.ts file to configure and export your database instance:

src/db.ts
TypeScript
import type { SyncedDbConfig } from '@spooky-sync/client-solid';
import { schema, SURQL_SCHEMA } from './schema.gen';

export const dbConfig: SyncedDbConfig<typeof schema> = {
  logLevel: 'info',
  schema,
  schemaSurql: SURQL_SCHEMA,
  database: {
    namespace: 'main',
    database: 'main',
    // `spky dev` exposes SurrealDB on 8666 by default.
    endpoint: 'ws://localhost:8666/rpc',
    store: 'indexeddb',
  },
};

4. App Entry Point

Initialize the database in your root component before rendering the app:

src/App.tsx
tsx
import { Sp00kyProvider } from '@spooky-sync/client-solid';
import { dbConfig } from './db';
import { MainContent } from './MainContent';

export default function App() {
  // `Sp00kyProvider` constructs a `SyncedDb` from the config and
  // awaits its `init()` internally. While it's connecting it shows
  // the `fallback` instead of the children. After that, descendants
  // can pull the client with `useDb()` and queries with `useQuery()`.
  return (
    <Sp00kyProvider
      config={dbConfig}
      fallback={<div>Initializing database...</div>}
      onError={(err) => console.error('Failed to initialize database:', err)}
    >
      <MainContent />
    </Sp00kyProvider>
  );
}

5. Querying Data with useQuery

Use the useQuery hook to create reactive queries that automatically update:

src/components/ThreadList.tsx
tsx
import { For, Show } from 'solid-js';
import { useDb, useQuery } from '@spooky-sync/client-solid';
import type { schema } from '../schema.gen';

export function ThreadList() {
  // `useDb` reads the client from the `<Sp00kyProvider>` context.
  const db = useDb<typeof schema>();

  // Build the query. `useQuery` re-renders the component when the
  // result set changes (locally or from another tab / device).
  const threadsQuery = useQuery(() =>
    db.query('thread')
      .related('author')
      .orderBy('created_at', 'desc')
      .limit(20)
      .build()
  );

  return (
    <div>
      <Show when={threadsQuery.isLoading()}>
        <div>Loading...</div>
      </Show>
      
      <Show when={threadsQuery.error()}>
        <div>Error: {threadsQuery.error()?.message}</div>
      </Show>
      
      <ul>
        <For each={threadsQuery.data() || []}>
          {(thread) => (
            <li>
              <h3>{thread.title}</h3>
              <p>{thread.content}</p>
              <small>By {thread.author?.username}</small>
            </li>
          )}
        </For>
      </ul>
    </div>
  );
}

6. Authentication with Auth Context

Create an auth context to manage user authentication state:

src/lib/auth.tsx
tsx
import { createContext, useContext, createSignal, onCleanup, type JSX } from 'solid-js';
import { useDb } from '@spooky-sync/client-solid';
import type { schema } from '../schema.gen';

interface AuthContextType {
  userId: () => string | null;
  isLoading: () => boolean;
  signIn: (username: string, password: string) => Promise<void>;
  signUp: (username: string, password: string) => Promise<void>;
  signOut: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType>();

export function AuthProvider(props: { children: JSX.Element }) {
  const db = useDb<typeof schema>();
  const [userId, setUserId] = createSignal<string | null>(null);
  const [isInitializing, setIsInitializing] = createSignal(true);

  // Subscribe to auth state changes — fires synchronously with the
  // current value on subscribe, then on every signin/signout.
  const unsubscribe = db.auth.subscribe((uid) => {
    setUserId(uid);
    setIsInitializing(false);
  });

  onCleanup(() => unsubscribe());

  const signIn = async (username: string, password: string) => {
    await db.auth.signIn('account', { username, password });
  };

  const signUp = async (username: string, password: string) => {
    await db.auth.signUp('account', { username, password });
  };

  const signOut = async () => {
    await db.signOut();
  };

  const value: AuthContextType = {
    userId,
    isLoading: () => isInitializing(),
    signIn,
    signUp,
    signOut,
  };

  return (
    <AuthContext.Provider value={value}>
      {props.children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

7. Creating and Updating Data

Use the database methods to create and update records:

TypeScript
import { RecordId, Uuid } from 'surrealdb';
import { useDb } from '@spooky-sync/client-solid';
import type { schema } from './schema.gen';

function useMutations() {
  const db = useDb<typeof schema>();

  // Create a new thread. `db.create` takes a fully-qualified record
  // id ("thread:abc…") plus a typed payload — there's no separate
  // table argument.
  const createThread = async (title: string, content: string, authorId: string) => {
    const id = \`thread:\${Uuid.v4().toString().replace(/-/g, '')}\`;
    await db.create(id, {
      title,
      content,
      active: true,
      author: new RecordId('user', authorId),
    });
  };

  // Update a thread. The third arg is a partial — only listed fields
  // are merged.
  const updateThread = async (
    threadId: string,
    updates: { title?: string; content?: string },
  ) => {
    await db.update('thread', threadId, updates);
  };

  // Delete a thread.
  const deleteThread = async (threadId: string) => {
    await db.delete('thread', threadId);
  };

  return { createThread, updateThread, deleteThread };
}