Github|...

Vanilla JS / TS

You can use Sp00ky with plain JavaScript or TypeScript without any framework. This guide shows how to use the core Sp00kyClient directly.

Installation

Bash
pnpm add @spooky-sync/core surrealdb

Generate Schema Types

First, generate your schema types from your SurrealDB schema:

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

Initialize the Client

Create and initialize a Sp00kyClient instance:

src/db.ts
TypeScript
import { Sp00kyClient, type Sp00kyConfig } from '@spooky-sync/core';
import { schema, SURQL_SCHEMA } from './schema.gen';

const config: Sp00kyConfig<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', // or 'memory'
  },
};

export const client = new Sp00kyClient<typeof schema>(config);

// Initialize the client (idempotent — safe to await multiple times).
export async function initDatabase() {
  await client.init();
  console.log('Sp00ky client initialized');
}

Querying Data

Use the query builder to fetch data and subscribe to updates:

TypeScript
import { client } from './db';

// Build a query. The second argument is `QueryOptions` — pass `{}`
// for defaults. SolidJS users get the same builder from `db.query()`
// without the options argument.
const builder = client.query('thread', {})
  .related('author')
  .orderBy('created_at', 'desc')
  .limit(10)
  .build();

// Register the query with the client and get back its `{ hash }`.
const { hash } = await builder.run();

// Subscribe to query updates. `immediate: true` invokes the callback
// synchronously with whatever the local store has right now.
const unsubscribe = await client.subscribe(
  hash,
  (records) => {
    console.log('Threads updated:', records);
    renderThreads(records);
  },
  { immediate: true }
);

// Later: unsubscribe when done
// unsubscribe();

function renderThreads(threads: any[]) {
  const container = document.getElementById('threads');
  if (!container) return;
  
  container.innerHTML = threads.map(thread => \`
    <div class="thread">
      <h3>\${thread.title}</h3>
      <p>\${thread.content}</p>
      <small>By \${thread.author?.username || 'Unknown'}</small>
    </div>
  \`).join('');
}

Authentication

Use the auth service to handle user authentication:

TypeScript
import { client } from './db';

// Sign up a new user
async function signUp(username: string, password: string) {
  try {
    await client.auth.signUp('account', { username, password });
    console.log('Signed up successfully');
  } catch (error) {
    console.error('Sign up failed:', error);
  }
}

// Sign in
async function signIn(username: string, password: string) {
  try {
    await client.auth.signIn('account', { username, password });
    console.log('Signed in successfully');
  } catch (error) {
    console.error('Sign in failed:', error);
  }
}

// Subscribe to auth state changes
client.auth.subscribe((userId) => {
  if (userId) {
    console.log('User logged in:', userId);
    showAuthenticatedUI();
  } else {
    console.log('User logged out');
    showLoginUI();
  }
});

// Sign out
async function signOut() {
  await client.auth.signOut();
}

Creating and Updating Records

Use the client methods to mutate data:

TypeScript
import { client } from './db';
import { RecordId, Uuid } from 'surrealdb';

// Create a new thread. `client.create` takes a fully-qualified record
// id ("thread:abc…") plus a typed payload.
async function createThread(title: string, content: string, authorId: string) {
  const id = \`thread:\${Uuid.v4().toString().replace(/-/g, '')}\`;

  await client.create(id, {
    title,
    content,
    active: true,
    author: new RecordId('user', authorId),
  });

  console.log('Thread created:', id);
}

// Update a thread. Only listed fields are merged.
async function updateThread(threadId: string, title: string) {
  await client.update('thread', threadId, { title });
  console.log('Thread updated');
}

// Delete a thread.
async function deleteThread(threadId: string) {
  await client.delete('thread', threadId);
  console.log('Thread deleted');
}

Complete Example

Here’s a complete working example:

src/main.ts
TypeScript
import { client, initDatabase } from './db';

async function main() {
  // Initialize database
  await initDatabase();
  
  // Subscribe to auth state
  client.auth.subscribe((userId) => {
    if (userId) {
      loadThreads();
    }
  });
  
  // Load and display threads
  async function loadThreads() {
    const builder = client.query('thread', {})
      .related('author')
      .orderBy('created_at', 'desc')
      .limit(20)
      .build();

    const { hash } = await builder.run();

    await client.subscribe(hash, (threads) => {
      renderThreads(threads);
    }, { immediate: true });
  }
  
  function renderThreads(threads: any[]) {
    const container = document.getElementById('threads');
    if (!container) return;
    
    container.innerHTML = threads.map(thread => \`
      <article>
        <h2>\${thread.title}</h2>
        <p>\${thread.content}</p>
        <footer>By \${thread.author?.username || 'Unknown'}</footer>
      </article>
    \`).join('');
  }
}

main().catch(console.error);