Typescript SDK

TypeScript / JavaScript Quick Start

WoWSQL TypeScript SDK — Get started in minutes

Get your API key: In your WoWSQL project dashboard, go to Settings → API to find your anon key (wowsql_anon_...) and service role key (wowsql_service_...). The anon key is safe for client-side use (respects Row Level Security). The service role key bypasses RLS and should only be used server-side.

Installation

Official package: View on npm →

npm install @wowsql/sdk
# pin this release: npm install @wowsql/sdk@3.10.0
# or: yarn add @wowsql/sdk
# or: pnpm add @wowsql/sdk

Initialize Client

import WowSQLClient from '@wowsql/sdk';

// Initialize client — connects to PostgREST at /rest/v1/
const client = new WowSQLClient({
  projectUrl: 'myproject',  // → https://myproject.wowsqlconnect.com
  apiKey: 'wowsql_anon_your_key_here'
});

// Or with full URL
const clientFull = new WowSQLClient({
  projectUrl: 'https://myproject.wowsqlconnect.com',
  apiKey: 'wowsql_anon_your_key_here'
});

// Query data — uses GET /rest/v1/users?select=*
const users = await client.table('users').select('*').get();

How it works under the hood: The SDK translates your query-builder calls into standard PostgREST HTTP requests. For example, client.table('users').select('id,email').eq('status','active').limit(10).get() becomes GET /rest/v1/users?select=id,email&status=eq.active&limit=10 with the apikey header.

Example API Call (PostgREST)

You can also call the PostgREST API directly with fetch. WoWSQL uses standard PostgREST — all filtering, ordering, and pagination is done via query parameters:

const PROJECT_URL = 'https://myproject.wowsqlconnect.com';
const API_KEY = 'wowsql_anon_your_key_here';

// SELECT with filters, ordering, and pagination
const response = await fetch(
  `${PROJECT_URL}/rest/v1/users?select=id,email,name&status=eq.active&order=created_at.desc&limit=10`,
  {
    headers: {
      'apikey': API_KEY,
      'Content-Type': 'application/json'
    }
  }
);
const data = await response.json();
console.log(data);

// INSERT a new row
const insertRes = await fetch(`${PROJECT_URL}/rest/v1/users`, {
  method: 'POST',
  headers: {
    'apikey': API_KEY,
    'Content-Type': 'application/json',
    'Prefer': 'return=representation'
  },
  body: JSON.stringify({ email: 'john@example.com', name: 'John Doe', status: 'active' })
});
const newUser = await insertRes.json();

// UPDATE a row
await fetch(`${PROJECT_URL}/rest/v1/users?id=eq.123`, {
  method: 'PATCH',
  headers: {
    'apikey': API_KEY,
    'Content-Type': 'application/json',
    'Prefer': 'return=representation'
  },
  body: JSON.stringify({ name: 'Updated Name' })
});

// DELETE a row
await fetch(`${PROJECT_URL}/rest/v1/users?id=eq.123`, {
  method: 'DELETE',
  headers: { 'apikey': API_KEY }
});

PostgREST Filter Syntax (Query Params)

When calling the REST API directly, filters go in the URL as query parameters:

OperatorPostgREST SyntaxExample
Equalscol=eq.value?status=eq.active
Not equalscol=neq.value?status=neq.deleted
Greater thancol=gt.value?age=gt.18
Greater or equalcol=gte.value?age=gte.18
Less thancol=lt.value?price=lt.100
Less or equalcol=lte.value?price=lte.100
Pattern matchcol=like.*value*?name=like.*john*
Case-insensitivecol=ilike.*value*?email=ilike.*@gmail.com
IS NULLcol=is.null?deleted_at=is.null
IS NOT NULLcol=not.is.null?email=not.is.null
IN listcol=in.(a,b,c)?role=in.(admin,mod)
NOT INcol=not.in.(a,b)?status=not.in.(deleted,banned)
Orderorder=col.asc?order=created_at.desc
Paginationlimit=N&offset=M?limit=20&offset=40
Select columnsselect=a,b,c?select=id,email,name

Query Data

Basic Queries

// Get all records - use .select() before .get()
const users = await client.table('users').select('*').get();

// With filters - use .filter() or helper methods
const adults = await client.table('users')
  .select('*')
  .filter({ column: 'age', operator: 'gte', value: 18 })
  .get();

// Or using filter(column, operator, value)
const adults2 = await client.table('users')
  .select('*')
  .filter('age', 'gte', 18)
  .get();

// Select specific columns
const names = await client.table('users')
  .select(['id', 'name'])
  .get();

Advanced Queries

// Multiple filters with sorting
const activeUsers = await client.table('users')
  .filter({ column: 'status', operator: 'eq', value: 'active' })
  .filter({ column: 'age', operator: 'gt', value: 18 })
  .order('created_at', 'desc')
  .limit(20)
  .offset(0)
  .get();

console.log(`Found ${activeUsers.count} users`);
activeUsers.data.forEach(user => console.log(user.name));

Filter Helper Methods

// Using helper methods (more readable)
const users = await client.table('users')
  .eq('status', 'active')
  .gt('age', 18)
  .like('email', '%@gmail.com')
  .isNotNull('email_verified_at')
  .in('role', ['admin', 'moderator'])
  .between('created_at', '2024-01-01', '2024-12-31')
  .get();

// OR conditions
const usersOr = await client.table('users')
  .eq('status', 'active')
  .or('role', 'eq', 'admin')
  .get();

GROUP BY and Aggregates

Basic GROUP BY

// Group by category with counts
const result = await client.table('products')
  .select('category', 'COUNT(*) as count', 'AVG(price) as avg_price')
  .groupBy('category')
  .get();

// Group by date
const result2 = await client.table('orders')
  .select('DATE(created_at) as date', 'COUNT(*) as orders', 'SUM(total) as revenue')
  .groupBy('DATE(created_at)')
  .orderBy('date', 'desc')
  .get();

// Multiple GROUP BY columns
const result3 = await client.table('sales')
  .select('region', 'category', 'SUM(amount) as total')
  .groupBy(['region', 'category'])
  .get();

HAVING Clause

In 3.10.0 having() conditions are sent as ordinary column filters and groupBy() only adds its columns to the select list. PostgREST does not run SQL GROUP BY/HAVING on /rest/v1, so aggregate expressions such as COUNT(*) are not evaluated. Use a database view or an RPC function for real aggregates.

// Filter aggregated results
const result = await client.table('products')
  .select('category', 'COUNT(*) as count')
  .groupBy('category')
  .having('COUNT(*)', 'gt', 10)
  .get();

// Multiple HAVING conditions
const result2 = await client.table('orders')
  .select('DATE(created_at) as date', 'SUM(total) as revenue')
  .groupBy('DATE(created_at)')
  .having('SUM(total)', 'gt', 1000)
  .having('COUNT(*)', 'gte', 5)
  .get();

Multiple ORDER BY

// Order by multiple columns
const result = await client.table('products')
  .select('*')
  .orderByMultiple([
    { column: 'category', direction: 'asc' },
    { column: 'price', direction: 'desc' },
    { column: 'created_at', direction: 'desc' }
  ])
  .get();

CRUD Operations

Create Record

const result = await client.table('users').create({
  name: 'John Doe',
  email: 'john@example.com',
  age: 30
});

console.log(`Created user with ID: ${result.id}`);

Update Record

await client.table('users').update(123, {
  email: 'newemail@example.com'
});

console.log('User updated successfully');

Delete Record

await client.table('users').delete(123);
console.log('User deleted successfully');

Get Single Record by ID

const user = await client.table('users').getById(123);
console.log(`User: ${user.name}`);

Get First Record

// Get first matching record
const user = await client.table('users')
  .filter({ column: 'status', operator: 'eq', value: 'active' })
  .first();

if (user) {
  console.log(`First active user: ${user.name}`);
}

Type-safe Queries

Use generics for full type safety and autocomplete:

import { WowSQLClient, QueryResponse } from '@wowsql/sdk';

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

const client = new WowSQLClient({ projectUrl: 'myproject', apiKey: 'key' });

// Type-safe table access
const users: QueryResponse<User> = await client.table<User>('users').get();

// Full type safety and autocomplete
users.data.forEach((user: User) => {
  console.log(user.name);  // TypeScript knows this is a string
});

Error Handling

import { WowSQLError } from '@wowsql/sdk';

try {
  const users = await client.table('users').select('*').get();
  console.log(`Success: ${users.data.length} users`);
} catch (error) {
  if (error instanceof WowSQLError) {
    if (error.statusCode === 401) console.error('Authentication failed — check your API key');
    else if (error.statusCode === 404) console.error('Table or resource not found');
    else console.error(`Error (${error.statusCode}): ${error.message}`);
  } else throw error;
}

Filter Operators

Available operators for filtering data:

OperatorDescriptionExample (TypeScript)
eqEquals.filter({ column: 'status', operator: 'eq', value: 'active' })
neqNot equals.filter({ column: 'status', operator: 'neq', value: 'deleted' })
gtGreater than.filter({ column: 'age', operator: 'gt', value: 18 })
gteGreater than or equal.filter({ column: 'age', operator: 'gte', value: 18 })
ltLess than.filter({ column: 'price', operator: 'lt', value: 100 })
lteLess than or equal.filter({ column: 'price', operator: 'lte', value: 100 })
likePattern matching.filter({ column: 'name', operator: 'like', value: 'john' })
isIS NULL / IS NOT NULL.filter({ column: 'deleted_at', operator: 'is', value: null })
inIN operator.filter('category', 'in', ['electronics', 'books'])
not_inNOT IN.filter('status', 'not_in', ['deleted', 'archived'])
betweenBETWEEN.filter('price', 'between', [10, 100])
not_betweenNOT BETWEEN.filter('age', 'not_between', [18, 65])

Authentication

Use the same API key for authentication operations. Auth endpoints are at /auth/v1/ on your project URL.

Sign Up & Sign In

import { ProjectAuthClient } from '@wowsql/sdk';

const auth = new ProjectAuthClient({
  projectUrl: 'myproject',   // → https://myproject.wowsqlconnect.com
  apiKey: 'wowsql_anon_your_key_here'
});

// Sign up → POST /auth/v1/signup
const result = await auth.signUp({
  email: 'user@example.com',
  password: 'SecurePassword123',
  full_name: 'John Doe'
});
console.log(`Access token: ${result.session?.accessToken}`);

// Sign in → POST /auth/v1/login
const login = await auth.signIn({
  email: 'user@example.com',
  password: 'SecurePassword123'
});
if (login.requires2fa) {
  // TOTP is on for this user: finish with verifyTwoFactorLogin (see Two-Factor Authentication)
  console.log(`2FA required, method: ${login.method}`);
} else {
  console.log(`Access token: ${login.session?.accessToken}`);
}

Direct REST Call (Auth)

// Sign up directly via REST
const res = await fetch('https://myproject.wowsqlconnect.com/auth/v1/signup', {
  method: 'POST',
  headers: {
    'apikey': 'wowsql_anon_your_key_here',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'user@example.com',
    password: 'SecurePassword123',
    full_name: 'John Doe'
  })
});

// Sign in directly via REST
const loginRes = await fetch('https://myproject.wowsqlconnect.com/auth/v1/login', {
  method: 'POST',
  headers: {
    'apikey': 'wowsql_anon_your_key_here',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'user@example.com',
    password: 'SecurePassword123'
  })
});
const session = await loginRes.json();
// session.access_token, session.refresh_token, session.user

CAPTCHA (Turnstile)

Optional. Only required when dashboard Attack Protection → Turnstile is enabled. Create a Cloudflare Turnstile widget for your app domain and paste the site key and secret in Attack Protection (WoWSQL does not share a platform widget). GET /auth/v1/settings returns your captcha.site_key. Render Turnstile with that key, then pass the token. Omit it when captcha is off — existing calls are unchanged.

await auth.signUp({
  email: 'user@example.com',
  password: 'SecurePassword123',
  captchaToken: turnstileToken,
});
await auth.signIn({
  email: 'user@example.com',
  password: 'SecurePassword123',
  captchaToken: turnstileToken,
});
await auth.forgotPassword('user@example.com', turnstileToken);
await auth.sendOtp({ email: 'user@example.com', captchaToken: turnstileToken });
await auth.sendMagicLink('user@example.com', 'login', turnstileToken);
await auth.resendVerification('user@example.com', turnstileToken);

Password Reset

// Request password reset
await auth.forgotPassword('user@example.com');

// Reset password with token from email
await auth.resetPassword('reset_token_from_email', 'NewSecurePassword123');

OTP Authentication

Provide exactly one of email or phone. Email OTP needs Auth → Emails configured. Phone OTP needs Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91) and phone OTP enabled. purpose: login, signup, or password_reset.

// Email OTP
await auth.sendOtp({ email: 'user@example.com', purpose: 'login' });
const emailLogin = await auth.verifyOtp({
  email: 'user@example.com',
  otp: '123456',
  purpose: 'login'
});

// Phone OTP (SMS) — E.164, e.g. +15551234567
await auth.sendOtp({ phone: '+15551234567', purpose: 'login' });
const phoneLogin = await auth.verifyOtp({
  phone: '+15551234567',
  otp: '123456',
  purpose: 'login'
});
console.log(`Logged in: ${phoneLogin.user?.id}`);
// Send magic link
await auth.sendMagicLink('user@example.com', 'login');   // sendMagicLink(email, purpose?, captchaToken?)

// Verify email with token
await auth.verifyEmail('verification_token_from_email');

// Resend verification email
await auth.resendVerification('user@example.com');

OAuth Authentication

Setup checklist — do this once per provider, per project:
1. In your WoWSQL dashboard → Project → Auth → Providers, enable the provider (e.g. Google) and paste the Client ID and Client Secret from your provider's developer console.
2. Copy the Redirect URI shown in the dashboard (https://<slug>.wowsqlconnect.com/auth/v1/oauth/<provider>/callback) and add it to your provider's list of Authorised redirect URIs (Google: Cloud Console → Credentials → OAuth 2.0 client → Authorised redirect URIs).
3. Make sure the Client ID / Secret are pasted without leading or trailing spaces.

How the callback works: The OAuth callback URL (/auth/v1/oauth/<provider>/callback) is handled entirely by the WoWSQL auth service — your application does not need to send an API key on that URL. The service exchanges the code for tokens, creates/updates the user record, and then redirects to your frontend_redirect_uri with the session tokens as query parameters. You only need to handle the final redirect in your frontend.

// ── Step 1: Server-side — get the Google authorization URL ──────────────────
// Call from a server-side route (Next.js API route, Express handler, etc.).
// Never expose your service key client-side.
import { ProjectAuthClient } from '@wowsql/sdk';

const auth = new ProjectAuthClient({
  projectUrl: process.env.WOWSQL_PROJECT_URL!, // e.g. https://<slug>.wowsqlconnect.com
  apiKey:     process.env.WOWSQL_ANON_KEY!,    // wowsql_anon_...
});

// frontend_redirect_uri: where YOUR app should land after the OAuth flow finishes
// Internally calls: GET /auth/v1/oauth/google?frontend_redirect_uri=...
const { authorizationUrl } = await auth.getOAuthAuthorizationUrl(
  'google',
  'https://yourapp.com/auth/callback'   // must be an allowed origin in your app
);
// Redirect the browser to authorizationUrl — Google handles login from here.
// Google will call: https://<slug>.wowsqlconnect.com/auth/v1/oauth/google/callback
// WowSQL auth service exchanges the code and redirects to your frontend_redirect_uri.


// ── Step 2: Frontend — handle the redirect from WowSQL ──────────────────────
// WowSQL redirects to https://yourapp.com/auth/callback?access_token=...&refresh_token=...
// Read the tokens from the URL and store them.
// Example (Next.js page / React component):
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

export default function AuthCallbackPage() {
  const router = useRouter();

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const accessToken  = params.get('access_token');
    const refreshToken = params.get('refresh_token');
    const error        = params.get('error');

    if (error) {
      console.error('OAuth error:', error);
      router.replace('/auth/login?error=' + error);
      return;
    }
    if (accessToken) {
      // Persist tokens however your app manages session (cookie, localStorage, etc.)
      sessionStorage.setItem('wowsql_access_token',  accessToken);
      sessionStorage.setItem('wowsql_refresh_token', refreshToken ?? '');
      router.replace('/dashboard');
    }
  }, []);

  return <p>Signing you in…</p>;
}


// ── (Advanced) Manual code exchange — only if you handle the callback yourself
const result = await auth.exchangeOAuthCallback(
  'google',
  'authorization_code_from_callback',
  'https://yourapp.com/auth/callback'  // optional; the server exchanges the code with the project callback URL
);
console.log(`OAuth login successful: ${result.user?.id}`);

Session and Token Refresh

Token refresh calls POST /auth/v1/refresh-token with the stored refresh token and stores the new token pair. From 3.10.0 every SDK uses this route; before 3.10.0 the TypeScript and Java SDKs called routes that do not exist, and Flutter and Rust had no refresh method.

// Current user → GET /auth/v1/me (uses the stored access token)
const me = await auth.getUser();

// Refresh tokens → POST /auth/v1/refresh-token (uses the stored refresh token)
const refreshed = await auth.refreshSession();
console.log(refreshed.session?.accessToken);

// Read or restore the stored session
const { accessToken, refreshToken } = auth.getSession();
auth.setSession({ accessToken: 'saved_access_token', refreshToken: 'saved_refresh_token' });

// Sign out → POST /auth/v1/logout
await auth.logout();

Two-Factor Authentication (TOTP)

Added in 3.10.0. When a user has TOTP two-factor authentication enabled, sign-in does not return a session. It returns a challenge instead: requires_2fa is true and a short-lived pending_token is set (SDK field names are in the example). Finish the sign-in with the verify call, using a 6-digit code from the authenticator app or one of the user's backup codes. Status, setup, enable, disable and recovery-code calls run as the signed-in user: the SDK sends your API key and the stored access token. Enabling 2FA and rotating recovery codes return new backup codes once; show them to the user and do not store them in plain text.

// 1. Enrol (user is signed in)
const setup = await auth.setupTotp();             // POST /auth/v1/2fa/setup-totp
// Show setup.qr_code_url (a PNG data URI when available) or setup.otpauth_uri in your UI
const enabled = await auth.enableTotp('123456');  // POST /auth/v1/2fa/verify-and-enable
console.log(enabled.backup_codes);               // shown once

// 2. Sign in when 2FA is on
const login = await auth.signIn({ email: 'user@example.com', password: 'SecurePassword123' });
if (login.requires2fa) {
  // POST /auth/v1/2fa/verify-login
  const done = await auth.verifyTwoFactorLogin(login.pendingToken!, { code: '123456' });
  // or: { backupCode: 'one-of-your-backup-codes' }
  console.log(done.session?.accessToken);
}

// 3. Manage
const status = await auth.getTwoFactorStatus();               // { enabled, method }
const rotated = await auth.regenerateRecoveryCodes('123456'); // { backup_codes, recovery_codes }
await auth.disableTotp({ code: '123456' });                   // or { backupCode: '...' }

Settings, Anonymous Sign-in, Identity Linking and Email Change

Added in 3.10.0. Anonymous sign-in and identity linking must be enabled in the project Auth settings (anonymous_auth_enabled, manual_linking_enabled); otherwise the server rejects the call. Linking and email change run as the signed-in user.

// Public auth settings → GET /auth/v1/settings
const settings = await auth.getSettings(); // allow_signup, anonymous_auth_enabled, captcha, ...

// Anonymous sign-in → POST /auth/v1/sign-in-anonymous
const anon = await auth.signInAnonymous();

// Link another OAuth identity to the signed-in user → POST /auth/v1/oauth/{provider}/link
await auth.linkOAuthIdentity('github', {
  code: 'authorization_code_from_callback',
  redirectUri: 'https://yourapp.com/auth/callback'
});

// Change email → POST /auth/v1/update-email
await auth.updateEmail('new@example.com');

Storage Operations

Initialize Storage

The SDK storage client calls /api/v1/storage/projects/{project_slug}/… on your project host (buckets, files, stats, quota); the REST routes below are served at /storage/v1/.

import { WowSQLStorage } from '@wowsql/sdk';

const storage = new WowSQLStorage({
  projectUrl: 'myproject',   // → https://myproject.wowsqlconnect.com
  apiKey: 'wowsql_anon_your_key_here'
});

File Operations

// Upload: upload(bucketName, fileData, path?, fileName?) → POST .../buckets/{bucket}/files?folder=...
// path is the object path; its folder part ("uploads") is sent as the folder query parameter.
// Pass fileName when uploading a Buffer, so the stored name is not taken from the path.
const result = await storage.upload('documents', file, 'uploads/document.pdf', 'document.pdf');
console.log(`Uploaded: ${result.path} (${result.size} bytes)`);

// Upload a local file (Node.js): uploadFromPath(filePath, bucketName?, path?)
await storage.uploadFromPath('path/to/document.pdf', 'documents', 'uploads/document.pdf');

// Public URL (public buckets): /storage/v1/object/public/{bucket}/{path}
const url = storage.getPublicUrl('documents', 'uploads/document.pdf');
console.log(`Public URL: ${url}`);

// Expiring signed URLs (getFileUrl / getPresignedUrl in earlier versions of this page) are not part of the SDK in 3.10.0.
// Use getPublicUrl for public buckets, or download() with your API key for private ones.

// Download: download(bucketName, filePath) → ArrayBuffer (GET .../buckets/{bucket}/files/{path}/download)
const data = await storage.download('documents', 'uploads/document.pdf');

// List files: listFiles(bucketName, { prefix, limit, offset }) → GET .../buckets/{bucket}/files
const files = await storage.listFiles('documents', { prefix: 'uploads/' });
files.forEach(file => console.log(`${file.path}: ${file.sizeMb.toFixed(2)} MB`));

// Delete file: deleteFile(bucketName, filePath) → DELETE .../buckets/{bucket}/files/{path}
await storage.deleteFile('documents', 'uploads/document.pdf');

// Check usage (GET .../stats). getQuota() calls .../quota, which returns the same fields as /stats.
const stats = await storage.getStats();
console.log(`${stats.total_files} files, ${stats.total_size_gb.toFixed(2)} GB used`);

Direct REST Call (Storage)

// Upload via REST
const formData = new FormData();
formData.append('file', fileBlob, 'photo.png');

await fetch('https://myproject.wowsqlconnect.com/storage/v1/object/avatars/photo.png', {
  method: 'POST',
  headers: { 'apikey': 'wowsql_anon_your_key_here' },
  body: formData
});

// Download via REST
const fileRes = await fetch(
  'https://myproject.wowsqlconnect.com/storage/v1/object/avatars/photo.png',
  { headers: { 'apikey': 'wowsql_anon_your_key_here' } }
);

// Public file (no auth needed if bucket is public)
// GET https://myproject.wowsqlconnect.com/storage/v1/object/public/{bucket}/{path}

Bucket Management

// Create bucket: createBucket(name, { public, fileSizeLimit, allowedMimeTypes }) → POST .../buckets
await storage.createBucket('avatars', { public: true });

// List buckets → GET .../buckets
const buckets = await storage.listBuckets();

// Update → PATCH .../buckets/{name}
await storage.updateBucket('avatars', { public: false });

// Delete bucket → DELETE .../buckets/{name}
await storage.deleteBucket('avatars');

Realtime

Subscribe to INSERT / UPDATE / DELETE, broadcast ephemeral events, and track presence. Uses the same anon or service_role key as REST.

WebSocket URL: wss://<project>.wowsqlconnect.com/realtime/v1/websocket?apikey=<wowsql_anon_... or wowsql_service_...>. Browsers cannot set WebSocket headers, so auth is always the query param. Enable a table first with POST /realtime/v1/enable and body {"schema_name":"public","table_name":"messages"}. Channel send / presence do not need a Postgres trigger. Payloads are capped at 64 KiB. Missing key closes 4001; invalid key closes 4003 (do not reconnect). Multi-replica deployments need REDIS_URL.

import WowSQLClient from '@wowsql/sdk';

const client = new WowSQLClient({
  projectUrl: 'myproject',
  apiKey: 'wowsql_anon_your_key_here',
});

const channel = client.realtime.channel('chat');
channel
  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => {
    console.log('New message:', payload.new);
  })
  .on('broadcast', { event: 'typing' }, ({ payload }) => {
    console.log('typing', payload);
  })
  .subscribe();

await channel.send({ type: 'broadcast', event: 'typing', payload: { user: 'Alice' } });

const sub = client.realtime.subscribe(
  { schema: 'public', table: 'messages', event: '*' },
  (change) => console.log(change.event, change.new),
);
await sub.unsubscribe();

Schema DDL (WowSQLSchema)

Requires service role key: Schema operations use wowsql_service_... (not the anon key). POST /api/v2/schema/tables requires a primaryKey column with type UUID. Use auto_increment: true for DEFAULT gen_random_uuid(). Raw DDL via POST /api/v2/schema/execute cannot use SERIAL PRIMARY KEY. Existing tables are unchanged.

import { WowSQLSchema } from '@wowsql/sdk';

const schema = new WowSQLSchema({
  projectUrl: 'myproject',   // → https://myproject.wowsqlconnect.com
  serviceKey: 'wowsql_service_your_key_here'
});

// Create table → POST /api/v2/schema/tables
await schema.createTable({
  tableName: 'items',
  columns: [
    { name: 'id', type: 'UUID', auto_increment: true },
    { name: 'title', type: 'TEXT' },
    { name: 'price', type: 'NUMERIC' },
    { name: 'created_at', type: 'TIMESTAMPTZ', default: 'NOW()' },
  ],
  primaryKey: 'id',
});

// Execute raw DDL → POST /api/v2/schema/execute
await schema.execute(`
  ALTER TABLE items ADD COLUMN description TEXT;
  CREATE INDEX idx_items_title ON items (title);
`);