Use the SelfDB JavaScript/TypeScript SDK (@selfdb/js-sdk) to implement backend requirements with SelfDB BaaS. Provides Auth (with built-in users table), Tables (custom data with CRUD), Storage (buckets/files), and Realtime (WebSocket subscriptions). DO NOT create a users table - SelfDB manages users internally via selfdb.auth.users.
Build the backend layer using SelfDB as the BaaS: Auth + Tables + Storage + Realtime.
DO NOT create a users table. SelfDB manages users internally through selfdb.auth.users. The built-in user has:
interface UserRead {
id: string; // UUID - use this as user_id in your tables
email: string;
firstName: string;
lastName: string;
role: 'USER' | 'ADMIN';
createdAt: string;
updatedAt: string;
}
For extended user data (bio, avatar, preferences), create a user_profiles table with user_id UUID PRIMARY KEY that references the SelfDB user.
import { SelfDB } from '@selfdb/js-sdk';
const selfdb = new SelfDB({
baseUrl: string, // Required: API base URL
apiKey: string, // Required: API key
timeout?: number // Optional: request timeout in ms
});
// Four main modules:
selfdb.auth // Authentication + User management (BUILT-IN USERS TABLE)
selfdb.tables // Table CRUD + Column operations + Data operations
selfdb.storage // Bucket CRUD + File operations
selfdb.realtime // Phoenix Channels WebSocket for live updates
// Login - returns tokens
const tokens = await selfdb.auth.login({ email: string, password: string });
// Returns: { access_token: string, refresh_token: string, token_type: string }
// Get current logged-in user
const user = await selfdb.auth.me();
// Returns: UserRead
// Refresh access token
const newTokens = await selfdb.auth.refresh({ refreshToken: string });
// Returns: TokenPair
// Logout (revoke current refresh token)
await selfdb.auth.logout({ refreshToken?: string });
// Returns: { message: string }
// Logout from all devices
await selfdb.auth.logoutAll();
// Returns: { message: string }
// Count users
const { count } = await selfdb.auth.count({ search?: string });
This is the built-in users table. DO NOT create your own users table.
// Create a new user
const user = await selfdb.auth.users.create({
email: string, // Required
password: string, // Required
firstName: string, // Required (camelCase!)
lastName: string, // Required (camelCase!)
role?: 'USER' | 'ADMIN' // Optional, defaults to 'USER'
});
// Returns: UserRead
// List users with pagination
const users = await selfdb.auth.users.list({
skip?: number,
limit?: number,
search?: string,
sortBy?: string,
sortOrder?: 'asc' | 'desc'
});
// Returns: UserRead[]
// Get user by ID
const user = await selfdb.auth.users.get(userId: string);
// Returns: UserRead
// Update user
const updated = await selfdb.auth.users.update(userId: string, {
firstName?: string,
lastName?: string,
password?: string,
role?: 'USER' | 'ADMIN'
});
// Returns: UserRead
// Delete user
await selfdb.auth.users.delete(userId: string);
// Returns: { message: string, user_id: string }
All data operations require a tableId, NOT a table name. Use this helper pattern:
const tableIdCache: Record<string, string> = {};
async function getTableId(tableName: string): Promise<string> {
if (tableIdCache[tableName]) return tableIdCache[tableName];
const tables = await selfdb.tables.list({ search: tableName, limit: 100 });
const table = tables.find((t) => t.name === tableName);
if (!table) throw new Error(`Table "${tableName}" not found`);
tableIdCache[tableName] = table.id;
return table.id;
}
// Create table
const table = await selfdb.tables.create({
name: string,
table_schema: TableSchema, // See column types below
public: boolean
});
// Returns: TableRead
// List tables
const tables = await selfdb.tables.list({
skip?: number,
limit?: number,
search?: string,
sortBy?: string,
sortOrder?: 'asc' | 'desc'
});
// Returns: TableRead[]
// Get table by ID
const table = await selfdb.tables.get(tableId: string);
// Returns: TableRead
// Update table
const updated = await selfdb.tables.update(tableId: string, {
name?: string,
public?: boolean,
realtime_enabled?: boolean // Enable/disable realtime events
});
// Returns: TableRead
// Delete table
await selfdb.tables.delete(tableId: string);
// Returns: { message: string, table_id: string }
// Count tables
const { count } = await selfdb.tables.count({ search?: string });
type ColumnType = 'text' | 'varchar' | 'integer' | 'bigint' | 'boolean' | 'timestamp' | 'jsonb' | 'uuid';
interface ColumnSchema {
type: ColumnType; // Required
nullable?: boolean; // Optional, defaults to true
default?: unknown; // Optional default value
}
// Example table_schema:
const schema = {
id: { type: 'uuid', nullable: false },
user_id: { type: 'uuid', nullable: false },
title: { type: 'text', nullable: false },
content: { type: 'text', nullable: true },
views: { type: 'integer', nullable: true, default: 0 },
published: { type: 'boolean', nullable: true, default: false },
metadata: { type: 'jsonb', nullable: true },
created_at: { type: 'timestamp', nullable: true },
updated_at: { type: 'timestamp', nullable: true }
};
// Add column
const table = await selfdb.tables.columns.add(tableId: string, {
name: string,
type: ColumnType,
nullable?: boolean,
default_value?: unknown
});
// Returns: TableRead
// Update column
const table = await selfdb.tables.columns.update(
tableId: string,
columnName: string,
{
new_name?: string,
type?: ColumnType,
nullable?: boolean,
default_value?: unknown
}
);
// Returns: TableRead
// Remove column
const table = await selfdb.tables.columns.remove(tableId: string, columnName: string);
// Returns: TableRead
// Insert row
const row = await selfdb.tables.data.insert(tableId: string, row: Record<string, unknown>);
// Returns: Record<string, unknown> (the inserted row)
// Update row
const updated = await selfdb.tables.data.updateRow(
tableId: string,
rowId: string,
updates: Record<string, unknown>,
options?: { idColumn?: string } // Default: 'id'
);
// Returns: Record<string, unknown>
// Delete row
await selfdb.tables.data.deleteRow(
tableId: string,
rowId: string,
options?: { idColumn?: string }
);
// Returns: { message: string, row_id: string }
// Fetch with options (alternative to query builder)
const result = await selfdb.tables.data.fetch(tableId: string, {
page?: number,
pageSize?: number,
search?: string,
sortBy?: string,
sortOrder?: 'asc' | 'desc'
});
// Returns: TableDataResponse
const result = await selfdb.tables.data
.query(tableId)
.search('term') // Text search
.sort('column', 'desc') // Sort by column
.page(1) // Page number (1-indexed)
.pageSize(25) // Results per page (1-1000)
.execute();
// Returns:
interface TableDataResponse {
data: Record<string, unknown>[];
total: number;
page: number;
pageSize: number;
}
files.upload() requires bucketIdfiles.download() requires bucketName + pathconst bucketIdCache: Record<string, string> = {};
async function getBucketId(bucketName: string): Promise<string> {
if (bucketIdCache[bucketName]) return bucketIdCache[bucketName];
const buckets = await selfdb.storage.buckets.list({ search: bucketName, limit: 100 });
const bucket = buckets.find((b) => b.name === bucketName);
if (!bucket) throw new Error(`Bucket "${bucketName}" not found`);
bucketIdCache[bucketName] = bucket.id;
return bucket.id;
}
// Create bucket
const bucket = await selfdb.storage.buckets.create({
name: string,
public: boolean
});
// Returns: BucketResponse
// List buckets
const buckets = await selfdb.storage.buckets.list({
skip?: number,
limit?: number,
search?: string,
sortBy?: string,
sortOrder?: 'asc' | 'desc'
});
// Returns: BucketResponse[]
// Get bucket by ID
const bucket = await selfdb.storage.buckets.get(bucketId: string);
// Returns: BucketResponse
// Update bucket
const updated = await selfdb.storage.buckets.update(bucketId: string, {
name?: string,
public?: boolean
});
// Returns: BucketResponse
// Delete bucket
await selfdb.storage.buckets.delete(bucketId: string);
// Returns: void
// Count buckets
const { count } = await selfdb.storage.buckets.count({ search?: string });
// Upload file (uses bucketId)
const upload = await selfdb.storage.files.upload(bucketId: string, {
filename: string,
data: ArrayBuffer | Uint8Array | Blob | string,
path?: string,
contentType?: string
});
// Returns: { success: boolean, bucket: string, path: string, size: number, file_id: string }
// Download file (uses bucketName + path, NOT bucketId!)
const arrayBuffer = await selfdb.storage.files.download({
bucketName: string,
path: string
});
// Returns: ArrayBuffer
// List files
const result = await selfdb.storage.files.list({
bucketId?: string,
skip?: number,
limit?: number,
pageSize?: number,
search?: string
});
// Returns: { data: FileResponse[], total: number, page: number, pageSize: number }
// Get file by ID
const file = await selfdb.storage.files.get(fileId: string);
// Returns: FileResponse
// Update file metadata
const updated = await selfdb.storage.files.updateMetadata(
fileId: string,
metadata: Record<string, unknown>
);
// Returns: FileResponse
// Delete file
await selfdb.storage.files.delete(fileId: string);
// Returns: void
// Storage statistics
const stats = await selfdb.storage.files.stats();
// Returns: { total_files: number, total_size: number, buckets_count: number }
// Count files
const { count } = await selfdb.storage.files.count({ bucketId?: string, search?: string });
const { count } = await selfdb.storage.files.totalCount({ search?: string });
Phoenix Channels WebSocket for live table updates.
// Connect (must be logged in first to set access token)
await selfdb.realtime.connect();
// Get connection state
const state = selfdb.realtime.getState();
// Returns: 'disconnected' | 'connecting' | 'connected' | 'disconnecting'
// Disconnect
await selfdb.realtime.disconnect();
// Channel topic format: 'table:{tableName}' (use table NAME, not ID)
const channel = selfdb.realtime.channel(`table:${tableName}`);
// Register event handlers (chainable)
channel
.on('INSERT', (payload) => console.log('New row:', payload.new))
.on('UPDATE', (payload) => console.log('Updated:', payload.new, 'was:', payload.old))
.on('DELETE', (payload) => console.log('Deleted:', payload.old))
.on('*', (payload) => console.log('Any event:', payload));
// Subscribe to start receiving events
await channel.subscribe();
// Get channel state
const state = channel.getState();
// Returns: 'closed' | 'joining' | 'joined' | 'leaving'
// Unsubscribe
await channel.unsubscribe();
// Remove specific handler
channel.off('INSERT', handlerFunction);
channel.off('INSERT'); // Remove all INSERT handlers
interface RealtimePayload {
event: 'INSERT' | 'UPDATE' | 'DELETE';
table: string;
new: Record<string, unknown> | null; // New row data (null for DELETE)
old: Record<string, unknown> | null; // Old row data (null for INSERT)
raw: unknown;
}
If events aren't arriving, ensure realtime_enabled is true:
const table = await selfdb.tables.get(tableId);
if (!table.realtime_enabled) {
await selfdb.tables.update(tableId, { realtime_enabled: true });
}
import {
SelfDBError, // Base class for all errors
APIConnectionError, // Network/timeout failures
BadRequestError, // 400/422 - Invalid request
AuthenticationError, // 401 - Login required
PermissionDeniedError, // 403 - Not allowed
NotFoundError, // 404 - Resource not found
ConflictError, // 409 - Resource conflict
InternalServerError // 5xx - Server error
} from '@selfdb/js-sdk';
try {
await selfdb.tables.get('missing-id');
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Table not found');
} else if (error instanceof AuthenticationError) {
console.log('Please login first');
} else if (error instanceof PermissionDeniedError) {
console.log('Access denied');
} else if (error instanceof SelfDBError) {
console.log(`SelfDB error: ${error.message}, status: ${error.status}`);
}
}
selfdb.auth.usersuser_id columns for owned tables (references SelfDB user.id)-- or /* */)getTableId() helper before any selfdb.tables.data.* callgetBucketId() helper before any selfdb.storage.files.upload() callselfdb.realtime.connect() requires access token