Middleware
Intercept sync operations with before and after hooks.
Sync middleware intercepts state updates and RPC calls at the directory level. Place middleware.ts (or _middleware.ts) in any sync handler directory to apply logic to all operations in that subtree.
File placement
Middleware applies to all handlers within its directory and subdirectories.
Exports
Middleware files export before and/or after functions.
before
Runs before handler processing. Can reject the operation.
import type { SyncMiddlewareContext, MiddlewareResult } from '@robojs/sync/server'
export function before(ctx: SyncMiddlewareContext): MiddlewareResult {
// Rate limiting example
if (isRateLimited(ctx.client.id)) {
return { reject: true, reason: 'rate_limited' }
}
console.log(`[Sync] ${ctx.messageType} on "${ctx.cleanKey}" by ${ctx.client.id}`)
return { continue: true }
}
export function before(ctx) {
// Rate limiting example
if (isRateLimited(ctx.client.id)) {
return { reject: true, reason: 'rate_limited' }
}
console.log(`[Sync] ${ctx.messageType} on "${ctx.cleanKey}" by ${ctx.client.id}`)
return { continue: true }
}after
Runs after successful state broadcast. Can't reject -- the update already happened.
import type { SyncMiddlewareContext } from '@robojs/sync/server'
export function after(ctx: SyncMiddlewareContext): void {
console.log(`[Sync] Completed ${ctx.messageType} on "${ctx.cleanKey}"`)
}
export function after(ctx) {
console.log(`[Sync] Completed ${ctx.messageType} on "${ctx.cleanKey}"`)
}MiddlewareResult type
type MiddlewareResult =
| { continue: true } // Allow operation
| { reject: true; reason?: string } // Block operation | { continue: true } // Allow operation
| { reject: true; reason?: string } // Block operationSyncMiddlewareContext
| Field | Type | Description |
|---|---|---|
state | T | The state data |
client | { id: string; data?: ClientData } | Client making the request |
params | Record<string, string> | Dynamic route parameters |
key | string[] | Full key array |
cleanKey | string | Normalized key string |
messageType | 'update' | 'call' | Type of operation |
Execution order
beforehooks run root-to-leaf: the root middleware runs first, then directory-level, then handler-level.afterhooks run leaf-to-root: the opposite direction.- If any
beforehook returns{ reject: true }, processing stops immediately. No further middleware or handlers execute.
Use cases
Authentication
import type { SyncMiddlewareContext, MiddlewareResult } from '@robojs/sync/server'
export function before(ctx: SyncMiddlewareContext): MiddlewareResult {
if (!ctx.client.data?.authenticated) {
return { reject: true, reason: 'not_authenticated' }
}
return { continue: true }
}
export function before(ctx) {
if (!ctx.client.data?.authenticated) {
return { reject: true, reason: 'not_authenticated' }
}
return { continue: true }
}Logging
import type { SyncMiddlewareContext, MiddlewareResult } from '@robojs/sync/server'
export function before(ctx: SyncMiddlewareContext): MiddlewareResult {
console.log(`[${ctx.messageType}] ${ctx.cleanKey} by ${ctx.client.id}`)
return { continue: true }
}
export function after(ctx: SyncMiddlewareContext): void {
console.log(`[${ctx.messageType}] ${ctx.cleanKey} completed`)
}
export function before(ctx) {
console.log(`[${ctx.messageType}] ${ctx.cleanKey} by ${ctx.client.id}`)
return { continue: true }
}
export function after(ctx) {
console.log(`[${ctx.messageType}] ${ctx.cleanKey} completed`)
}Rate limiting
import type { SyncMiddlewareContext, MiddlewareResult } from '@robojs/sync/server'
const lastUpdate = new Map<string, number>()
export function before(ctx: SyncMiddlewareContext): MiddlewareResult {
const now = Date.now()
const last = lastUpdate.get(ctx.client.id) ?? 0
if (now - last < 50) {
return { reject: true, reason: 'too_fast' }
}
lastUpdate.set(ctx.client.id, now)
return { continue: true }
}
const lastUpdate = new Map()
export function before(ctx) {
const now = Date.now()
const last = lastUpdate.get(ctx.client.id) ?? 0
if (now - last < 50) {
return { reject: true, reason: 'too_fast' }
}
lastUpdate.set(ctx.client.id, now)
return { continue: true }
}