Github|...

Vanilla JS / TS

Use the core client directly, with no framework bindings.

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

Installation

pnpm add @spooky-sync/core surrealdb

Generate Schema Types

First, generate your schema types from your SurrealDB schema:

# 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
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:

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:

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:

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
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);