Skip to content

Schema Validation ​

Concorde uses StandardSchema to provide runtime validation of all event data. This ensures that both incoming and outgoing messages conform to your defined types.

Schema Definition ​

Schemas are defined using any StandardSchema-compatible library. We recommend ArkType for its excellent TypeScript integration:

typescript
import { type } from 'arktype'

const userSchema = type({
  userId: 'string',
  username: 'string',
  email: 'email', // Built-in email validation
  age: 'number',
  status: "'active' | 'inactive' | 'pending'"
})

Supported Schema Libraries ​

Concorde works with any StandardSchema-compatible validation library:

typescript
import { type } from 'arktype'

const messageSchema = type({
  content: 'string>0', // Non-empty string
  timestamp: 'number',
  priority: "'low' | 'medium' | 'high'"
})

Zod ​

typescript
import { z } from 'zod'

const messageSchema = z.object({
  content: z.string().min(1),
  timestamp: z.number(),
  priority: z.enum(['low', 'medium', 'high'])
})

Valibot ​

typescript
import * as v from 'valibot'

const messageSchema = v.object({
  content: v.pipe(v.string(), v.minLength(1)),
  timestamp: v.number(),
  priority: v.picklist(['low', 'medium', 'high'])
})

Validation Flow ​

Server-Side Validation ​

When triggering events from the server, data is validated before being sent to Pusher:

typescript
// ✅ Valid data - will be sent
await server.trigger('chat', 'message', {
  content: 'Hello world!',
  timestamp: Date.now(),
  priority: 'medium'
})

// ❌ Invalid data - will throw an error
await server.trigger('chat', 'message', {
  content: '', // Empty string not allowed
  timestamp: 'invalid', // Should be number
  priority: 'urgent' // Not in enum
})
// Throws: "Trying to send invalid data: [validation errors]"

Client-Side Validation ​

Outgoing Data (Client Triggers) ​

typescript
const channel = client.subscribe('chat')

// ✅ Valid data
await channel.trigger('message', {
  content: 'Hello!',
  timestamp: Date.now(),
  priority: 'low'
})

// ❌ Invalid data
await channel.trigger('message', {
  content: 123, // Should be string
  timestamp: Date.now(),
  priority: 'low'
})
// Throws: "Trying to send invalid data: [validation errors]"

Incoming Data (Server Events) ​

typescript
channel.bind('message', (data) => {
  // This handler will only be called if data is valid
  // data is guaranteed to match the schema
  console.log(data.content) // Always a string
})

// If invalid data is received, it's logged and ignored:
// Console warning: "Received invalid payload [validation errors]"

Advanced Schema Patterns ​

Optional Fields ​

typescript
const userProfileSchema = type({
  userId: 'string',
  username: 'string',
  avatar: 'string | undefined', // Optional field
  'preferences?': { // Optional object
    theme: "'light' | 'dark'",
    notifications: 'boolean'
  }
})

Nested Objects ​

typescript
const orderSchema = type({
  orderId: 'string',
  customer: {
    id: 'string',
    name: 'string',
    email: 'email'
  },
  items: [{
    productId: 'string',
    quantity: 'number>0',
    price: 'number>0'
  }],
  total: 'number>0'
})

Array Validation ​

typescript
const notificationSchema = type({
  id: 'string',
  recipients: 'string[]', // Array of strings
  tags: "'urgent' | 'normal' | 'low'[]", // Array of enum values
  metadata: 'object' // Any object
})

Custom Validation ​

typescript
const eventSchema = type({
  timestamp: 'number',
  data: 'object'
}).pipe((data) => {
  // Custom validation logic
  if (data.timestamp > Date.now()) {
    return { success: false, issues: ['Timestamp cannot be in the future'] }
  }
  return { success: true, data }
})

Error Messages ​

Concorde provides detailed error messages for validation failures:

typescript
try {
  await server.trigger('user-events', 'user-joined', {
    userId: 123, // Should be string
    username: '', // Empty string
    timestamp: 'invalid' // Should be number
  })
} catch (error) {
  console.log(error.message)
  // "Trying to send invalid data: [
  //   { path: ['userId'], message: 'Expected string, got number' },
  //   { path: ['username'], message: 'String must not be empty' },
  //   { path: ['timestamp'], message: 'Expected number, got string' }
  // ]"
}

Performance Considerations ​

  • Validation Cost: Schema validation happens once per event
  • Schema Caching: Schemas are cached for optimal performance
  • Async Validation: Validation is asynchronous and non-blocking
  • Memory Usage: Minimal overhead with efficient schema compilation

Best Practices ​

  1. Use Strict Schemas: Define precise constraints for better validation
  2. Keep Schemas Simple: Complex nested structures can impact performance
  3. Version Your Schemas: Plan for schema evolution in production
  4. Test Validation: Write tests for both valid and invalid data
  5. Handle Failures Gracefully: Always catch validation errors on the server

Type Inference ​

Concorde automatically infers TypeScript types from your schemas:

typescript
const messageSchema = type({
  content: 'string',
  timestamp: 'number',
  author: {
    id: 'string',
    name: 'string'
  }
})

// TypeScript automatically knows the shape:
channel.bind('message', (data) => {
  // data: { content: string, timestamp: number, author: { id: string, name: string } }
  console.log(data.author.name) // Type-safe access
})

Next Steps ​