| title | TypeScript |
|---|---|
| description | The Volcano SDK includes comprehensive TypeScript definitions for type-safe development. This guide covers type usage, generics, and best practices. |
The Volcano SDK includes comprehensive TypeScript definitions for type-safe development. This guide covers type usage, generics, and best practices.
TypeScript definitions are included with the SDK - no additional packages needed:
npm install @volcano.dev/sdkImport types alongside the SDK:
import { VolcanoAuth } from '@volcano.dev/sdk';
import type { User, Session, AuthResponse } from '@volcano.dev/sdk';
const volcano = new VolcanoAuth({
apiUrl: process.env.VOLCANO_API_URL!,
anonKey: process.env.VOLCANO_ANON_KEY!,
});
// Types are inferred automatically
const { user, session, error } = await volcano.auth.signIn({
email: 'user@example.com',
password: 'password123',
});
if (user) {
console.log(user.email); // TypeScript knows this is a string
}interface VolcanoAuthConfig {
/** API URL (defaults to https://api.volcano.dev) */
apiUrl?: string;
/** Project anon key (required) */
anonKey: string;
/** Access token for server-side use */
accessToken?: string;
/** Refresh token for server-side use */
refreshToken?: string;
}The root SDK and the Next.js middleware entrypoint export separate User types.
type UserStatus = 'active' | 'banned' | 'deleted';
interface User {
id: string;
project_id?: string;
email: string;
email_confirmed?: boolean;
user_metadata?: Record<string, JsonValue>;
app_metadata?: Record<string, JsonValue>;
avatar_url?: string;
status?: UserStatus;
banned_until?: string | null;
last_sign_in_at?: string;
created_at: string;
updated_at: string;
}interface Session {
access_token: string;
refresh_token: string;
expires_in: number;
}interface AuthResponse {
user: User | null;
session: Session | null;
error: Error | null;
}
interface UserResponse {
user: User | null;
error: Error | null;
}
interface SessionResponse {
session: Session | null;
error: Error | null;
}Use generics for type-safe query results:
interface Post {
id: string;
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
user_id: string;
created_at: string;
}
// Query returns Post[]
const { data, error } = await volcano.from<Post>('posts').select('*').eq('status', 'published');
if (data) {
data.forEach((post) => {
console.log(post.title); // TypeScript knows this is string
console.log(post.status); // TypeScript knows this is 'draft' | 'published' | 'archived'
});
}// TypeScript validates the insert values
const { data, error } = await volcano.insert<Post>('posts', {
title: 'New Post',
content: 'Content here',
status: 'draft',
// user_id is likely auto-set by RLS
});const { data, error } = await volcano
.update<Post>('posts', {
status: 'published', // TypeScript ensures this is a valid status
})
.eq('id', postId);interface QueryBuilder<T> {
select(columns: string): QueryBuilder<T>;
eq(column: string, value: FilterValue): QueryBuilder<T>;
neq(column: string, value: FilterValue): QueryBuilder<T>;
gt(column: string, value: FilterValue): QueryBuilder<T>;
gte(column: string, value: FilterValue): QueryBuilder<T>;
lt(column: string, value: FilterValue): QueryBuilder<T>;
lte(column: string, value: FilterValue): QueryBuilder<T>;
like(column: string, pattern: string): QueryBuilder<T>;
ilike(column: string, pattern: string): QueryBuilder<T>;
is(column: string, value: null): QueryBuilder<T>;
in(column: string, values: FilterValue[]): QueryBuilder<T>;
order(column: string, options?: { ascending?: boolean }): QueryBuilder<T>;
limit(count: number): QueryBuilder<T>;
offset(count: number): QueryBuilder<T>;
execute(): Promise<QueryResult<T>>;
then<TResult1 = QueryResult<T>, TResult2 = never>(
resolve?: ((value: QueryResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
reject?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): Promise<TResult1 | TResult2>;
}
interface QueryResult<T> {
data: T[] | null;
error: Error | null;
count?: number;
}Query and mutation builders are thenable: await volcano.from<Post>('posts').select('*')
and await volcano.insert<Post>('posts', values) infer QueryResult<Post>. Calling
.execute() explicitly returns the same result type.
interface StorageObject {
id: string;
bucket_id: string;
name: string;
owner_id?: string;
is_public: boolean;
size: number;
mime_type: string;
etag?: string;
metadata?: Record<string, JsonValue>;
created_at: string;
updated_at: string;
public_url?: string;
}
interface StorageUploadOptions {
contentType?: string;
}
interface StorageUploadResponse {
data: StorageObject | null;
error: Error | null;
}
interface StorageDownloadResponse {
data: Blob | null;
error: Error | null;
}
interface StorageListResponse {
data: StorageObject[] | null;
error: Error | null;
nextCursor: string | null;
}const storage = volcano.storage.from('avatars');
// Upload
const { data, error }: StorageUploadResponse = await storage.upload('user/avatar.jpg', file, {
contentType: 'image/jpeg',
});
// Download
const { data: blob }: StorageDownloadResponse = await storage.download('user/avatar.jpg');
// List
const { data: files, nextCursor }: StorageListResponse = await storage.list('user/', {
limit: 100,
});Import realtime types from the realtime module:
import { VolcanoRealtime, RealtimeChannel } from '@volcano.dev/sdk/realtime';
import type {
PostgresChange,
PresenceState,
ConnectContext,
DisconnectContext,
ErrorContext,
RealtimeConfig,
ChannelOptions,
WebSocketConstructor,
} from '@volcano.dev/sdk/realtime';interface RealtimeConfig {
apiUrl: string;
anonKey: string;
accessToken?: string;
getToken?: () => Promise<string>;
volcanoClient?: VolcanoAuth;
fetchConfig?: FetchConfig;
webSocket?: WebSocketConstructor;
}
interface ChannelOptions {
type?: 'broadcast' | 'presence' | 'postgres';
autoFetch?: boolean;
fetchBatchWindowMs?: number;
fetchMaxBatchSize?: number;
}interface ConnectContext {
client?: string;
latency?: number;
}
interface DisconnectContext {
code?: number;
reason?: string;
reconnect?: boolean;
}
interface ErrorContext {
error?: unknown;
message?: string;
code?: number;
}message and code are validated fields from the connection event. error is
the original transport value, which may be a plain object rather than an
Error; narrow it before reading transport-specific fields.
interface PostgresChange {
table: string;
schema: string;
type: 'INSERT' | 'UPDATE' | 'DELETE';
record?: Record<string, unknown>;
old_record?: Record<string, unknown>;
columns?: string[];
timestamp: string;
id?: string | number;
mode?: 'lightweight';
}interface Message {
id: string;
content: string;
user_id: string;
created_at: string;
}
channel.onPostgresChanges('INSERT', 'public', 'messages', (change: PostgresChange) => {
const message = change.record as Message;
console.log('New message:', message.content);
});import type { PresenceState } from '@volcano.dev/sdk/realtime';
channel.onPresenceSync((state: PresenceState) => {
for (const connection of Object.values(state)) {
console.log(connection.client, connection.user, connection.connInfo);
}
});Each entry is a server connection record. One user can have multiple connections.
connInfo and chanInfo contain server metadata when present. track() keeps
local application state; it does not place custom fields on these records.
interface InvokeParams {
userId: string;
action: 'fetch' | 'update' | 'delete';
}
interface InvokeResult {
success: boolean;
data?: Record<string, unknown>;
message?: string;
}
const { data, error } = await volcano.functions.invoke<InvokeParams, InvokeResult>('process-user', {
userId: '123',
action: 'fetch',
});
if (data) {
console.log(data.success); // TypeScript knows this is boolean
console.log(data.message); // TypeScript knows this is string | undefined
}A platform-layer invocation failure is a VolcanoSystemError (exported from
@volcano.dev/sdk). Narrow with the VolcanoSystemError.is() type guard — it
gives full type narrowing without a cast and holds across duplicate SDK copies
in a bundle:
import { VolcanoSystemError } from '@volcano.dev/sdk';
if (error && VolcanoSystemError.is(error)) {
// deploy down/failed, gateway error, or network failure — infra, retryable
console.error(error.status, error.message); // `status` is typed here
}Writing a durable function and calling one are two
different type surfaces. The handler's types come from the /durable subpath,
which is where the authoring API lives:
import { durable, type DurableContext, type DurableHandler } from '@volcano.dev/sdk/durable';
interface OrderInput {
order_id: number;
}
interface OrderResult {
order_id: number;
outcome: 'shipped' | 'refunded';
}
// `DurableContext` is what a helper takes when steps live outside the handler.
async function charge(ctx: DurableContext, order: Order) {
return ctx.step('charge', () => chargeCard(order));
}
const run: DurableHandler<OrderInput, OrderResult> = async (input, ctx) => {
const order = await ctx.step('load', () => loadOrder(input.order_id));
await charge(ctx, order);
await ctx.wait('settle', '30s');
return { order_id: order.id, outcome: 'shipped' };
};
export const handler = durable(run);ctx.step, ctx.map and ctx.parallel are generic in what they return, so a
step's result type flows into the rest of the handler without a cast.
StepOptions, WaitUntilOptions, BatchOptions, BatchResult, Retry and
DurableDuration are exported alongside them for anything you build on top, as
are WaitForApprovalOptions and the ApprovalDecision that ctx.waitForApproval
resolves with.
Starting and reading executions uses the main entry point instead:
import type { DurableExecutionStatus } from '@volcano.dev/sdk';
const running: DurableExecutionStatus = 'running';
const { data } = await volcano.durable.list(projectId, 'order-pipeline', { status: running });
const execution = await volcano.durable.get(projectId, 'order-pipeline', executionId);
if (execution.data?.status === 'succeeded') {
const result = execution.data.result as OrderResult;
}DurableExecution is the handle start, get and stop resolve with, and
PaginatedDurableExecutions is what list returns. Both are generated from
the API contract, so they follow the wire's snake_case (function_id,
created_at, result_expired). result is unknown there — the platform
returns whatever the handler produced and cannot know its type, so narrow or
assert it on the way out.
volcano.durable.approvals resolves with DurableApproval,
PaginatedDurableApprovals, and DurableApprovalStats, generated the same
way, and takes DurableApprovalListOptions and DurableApprovalStatsOptions.
type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple';
interface OAuthProvider {
provider: OAuthProviderName;
linked_at: string;
updated_at: string;
}
interface OAuthAPIParams {
endpoint: string;
method?: string;
body?: JsonValue;
}// TypeScript ensures valid provider names
volcano.auth.signInWithOAuth('google'); // OK
volcano.auth.signInWithOAuth('invalid'); // Error: not assignable to 'OAuthProviderName'
// Get linked providers
const { providers } = await volcano.auth.getLinkedOAuthProviders();
if (providers) {
providers.forEach((p: OAuthProvider) => {
console.log(`${p.provider} linked at ${p.linked_at}`);
});
}interface AuthSession {
id: string;
user_id: string;
provider: 'email' | 'google' | 'github' | 'microsoft' | 'apple' | 'anonymous';
user_agent?: string;
ip_address?: string;
last_ip_address?: string;
expires_at: string;
last_activity_at?: string;
session_started_at?: string;
is_active: boolean;
is_current: boolean;
created_at: string;
updated_at: string;
}
interface SessionsResponse {
sessions: AuthSession[] | null;
total: number;
page: number;
limit: number;
total_pages: number;
error: Error | null;
}The middleware User requires id, email, and status. Its timestamps are optional, matching the auth response contract.
import type {
ServerClientConfig,
ServerClient,
User,
GetUserResult,
RefreshTokenResult,
} from '@volcano.dev/sdk/next/middleware';
interface User {
id: string;
email: string;
status: 'active' | 'banned' | 'deleted';
user_metadata?: Record<string, unknown>;
created_at?: string;
updated_at?: string;
}
interface ServerClientConfig {
anonKey: string;
apiUrl?: string;
accessToken?: string;
}
interface ServerClient {
getUser(accessToken: string): Promise<GetUserResult>;
refreshToken(refreshToken: string): Promise<RefreshTokenResult>;
}
interface GetUserResult {
user: User | null;
error: Error | null;
}
interface RefreshTokenResult {
accessToken: string | null;
refreshToken: string | null;
error: Error | null;
}A recursive type for JSON-serializable values:
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };Values accepted by query filters:
type FilterValue = string | number | boolean | null | Date;Type for user metadata:
type UserMetadata = Record<string, JsonValue>;
// Usage
await volcano.auth.signUp({
email: 'user@example.com',
password: 'password',
metadata: {
name: 'Alice',
preferences: {
theme: 'dark',
notifications: true,
},
} as UserMetadata,
});Create interfaces for your database tables:
// types/database.ts
export interface Post {
id: string;
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
user_id: string;
created_at: string;
updated_at: string;
}
export interface Comment {
id: string;
post_id: string;
user_id: string;
content: string;
created_at: string;
}
export interface User {
id: string;
email: string;
name: string;
avatar_url?: string;
}function isPost(data: unknown): data is Post {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'title' in data &&
'content' in data
);
}
const { data } = await volcano.from('posts').select('*');
if (data && data.every(isPost)) {
// data is typed as Post[]
}Always check for errors and null data:
const { data, error } = await volcano.from<Post>('posts').select('*');
if (error) {
// Handle error
console.error(error.message);
return;
}
if (!data) {
// Handle empty result
return;
}
// data is guaranteed to be Post[]
data.forEach((post) => {
console.log(post.title);
});const unsubscribe = volcano.auth.onAuthStateChange((user: User | null) => {
if (user) {
// TypeScript knows user properties
console.log(user.email);
}
});Enable strict TypeScript for best type checking:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true
}
}- Getting Started - Basic SDK usage
- Database - Query builder with types
- Realtime - Typed realtime subscriptions