@douglance/stdb-zod-bridge
v1.0.0
Published
Auto-generate Zod validation schemas from SpacetimeDB TypeScript modules
Maintainers
Readme
@spacetimedb/zod-bridge
Auto-generate Zod validation schemas from SpacetimeDB TypeScript module schemas for runtime type validation across the WASM boundary.
Why Use This?
SpacetimeDB modules define tables and reducers with TypeScript types, but client code often needs runtime validation:
- Validate user input before sending to reducers
- Catch type errors before they reach the server
- Get helpful error messages for invalid data
- Auto-generate validation from your existing schema (single source of truth)
Installation
npm install @spacetimedb/zod-bridge @spacetimedb/zod-bridge-runtime zodQuick Start
1. Generate Schemas
Given a SpacetimeDB module:
// src/schema.ts
import { schema, table, t } from 'spacetimedb/server';
const Position = table({ name: 'Position', public: true }, {
entity_id: t.identity().primaryKey(),
x: t.f32(),
y: t.f32(),
});
const gameSchema = schema(Position);
gameSchema.reducer('join_game', { name: t.string() }, (ctx, { name }) => {
// ...
});
export default gameSchema;Generate Zod schemas:
npx zod-bridge src/schema.ts src/generated/zod-schemas.ts2. Use Generated Schemas
// src/client.ts
import { PositionSchema, JoinGameArgsSchema } from './generated/zod-schemas';
import { z } from 'zod';
// Validate reducer arguments
function safeJoinGame(input: unknown) {
try {
const args = JoinGameArgsSchema.parse(input);
conn.reducers.joinGame(args); // Guaranteed valid
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation failed:', error.errors);
// Show user-friendly error message
}
}
}
// Validate table data
const position = PositionSchema.parse(rawData);CLI Usage
Basic Generation
npx zod-bridge <input> <output>Example:
npx zod-bridge src/schema.ts src/generated/zod-schemas.tsWatch Mode
Auto-regenerate when schema changes:
npx zod-bridge src/schema.ts src/generated/zod-schemas.ts --watchOptions
--watch,-w: Watch input file and regenerate on changes--no-variants: Skip generating Create/Update variant schemas--help,-h: Show help--version,-v: Show version
Generated Output
For each table, the generator creates:
- Base Schema: Validates the full table row
- Type Definition: TypeScript type from Zod schema
- Create Schema (optional): Omits auto-increment fields
- Update Schema (optional): Makes all fields except primary key optional
Example:
// Generated from: table Position
export const PositionSchema = z.object({
entity_id: zodSpacetime.identity(),
x: z.number(),
y: z.number(),
});
export type Position = z.infer<typeof PositionSchema>;
// For reducer: join_game(name: string)
export const JoinGameArgsSchema = z.object({
name: z.string().min(1).max(20),
});
export type JoinGameArgs = z.infer<typeof JoinGameArgsSchema>;Type Mapping
| SpacetimeDB Type | Zod Schema | Notes |
|------------------|------------|-------|
| t.string() | z.string() | |
| t.bool() | z.boolean() | |
| t.f32(), t.f64() | zodNumeric.f64() | Range-validated number |
| t.u8() | zodNumeric.u8() | 0-255 |
| t.u16() | zodNumeric.u16() | 0-65535 |
| t.u32() | zodNumeric.u32() | 0-4294967295 |
| t.u64() | zodNumeric.u64() | BigInt 0-18446744073709551615n |
| t.i8() | zodNumeric.i8() | -128-127 |
| t.i32() | zodNumeric.i32() | -2147483648-2147483647 |
| t.identity() | zodSpacetime.identity() | Accepts Identity, hex string, or BigInt |
| t.timestamp() | zodSpacetime.timestamp() | Accepts Date, ISO string, or Unix ms |
| t.connectionId() | zodSpacetime.connectionId() | Accepts ConnectionId or string |
| t.scheduleAt() | zodSpacetime.scheduleAt() | Accepts ScheduleAt or BigInt (microseconds) |
See @spacetimedb/zod-bridge-runtime for numeric and SpacetimeDB type validators.
Before vs After
Before (No Validation)
// Hope userInput is valid, crash if not
conn.reducers.joinGame({ name: userInput });Problems:
- No validation until server receives data
- Unclear error messages
- User sees "Internal Server Error"
- Debugging requires reading server logs
After (Zod Validation)
import { JoinGameArgsSchema } from './generated/zod-schemas';
import { z } from 'zod';
function safeJoinGame(input: unknown) {
try {
const args = JoinGameArgsSchema.parse(input);
conn.reducers.joinGame(args); // Guaranteed valid
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation failed:', error.errors);
// [{
// code: "too_small",
// minimum: 1,
// path: ["name"],
// message: "String must contain at least 1 character(s)"
// }]
}
}
}Benefits:
- Catch errors before sending to server
- Clear, actionable error messages
- Show user-friendly validation feedback
- Type-safe client code
Programmatic API
import { generate } from '@spacetimedb/zod-bridge';
await generate({
inputFile: './src/schema.ts',
outputFile: './src/generated/zod-schemas.ts',
includeVariants: true, // Generate Create/Update schemas
});Integration with Build Tools
Package.json Script
{
"scripts": {
"generate:schemas": "zod-bridge src/schema.ts src/generated/zod-schemas.ts",
"dev": "zod-bridge src/schema.ts src/generated/zod-schemas.ts --watch & vite dev"
}
}Pre-commit Hook
#!/bin/bash
# .git/hooks/pre-commit
npx zod-bridge src/schema.ts src/generated/zod-schemas.ts
git add src/generated/zod-schemas.tsLimitations
Current version supports primitive types and SpacetimeDB built-in types. Complex types planned for future releases:
t.array()- Coming soont.option()- Coming soont.object()- Coming soont.enum()- Coming soon
Error Messages
The generator provides detailed error messages:
Error: No table definitions found in src/schema.ts.
Expected to find tables declared as:
const TableName = table({ name: 'TableName', public: true }, { ... });
Make sure your schema file imports the 'table' function from 'spacetimedb/server'.License
MIT
Contributing
Issues and PRs welcome at github.com/yourusername/spacetimedb-utils
