LogoRobo.js

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.

middleware.tsApplies to ALL sync operations
middleware.tsApplies to game/* operations
middleware.tsApplies to game/[roomId]/*
state.ts

Exports

Middleware files export before and/or after functions.

before

Runs before handler processing. Can reject the operation.

src/sync/game/middleware.ts
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 }
}
src/sync/game/middleware.js

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.

src/sync/game/middleware.ts
import type { SyncMiddlewareContext } from '@robojs/sync/server'

export function after(ctx: SyncMiddlewareContext): void {
	console.log(`[Sync] Completed ${ctx.messageType} on "${ctx.cleanKey}"`)
}
src/sync/game/middleware.js

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 operation

SyncMiddlewareContext

FieldTypeDescription
stateTThe state data
client{ id: string; data?: ClientData }Client making the request
paramsRecord<string, string>Dynamic route parameters
keystring[]Full key array
cleanKeystringNormalized key string
messageType'update' | 'call'Type of operation

Execution order

  • before hooks run root-to-leaf: the root middleware runs first, then directory-level, then handler-level.
  • after hooks run leaf-to-root: the opposite direction.
  • If any before hook returns { reject: true }, processing stops immediately. No further middleware or handlers execute.

Use cases

Authentication

src/sync/middleware.ts
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 }
}
src/sync/middleware.js

export function before(ctx) {
	if (!ctx.client.data?.authenticated) {
		return { reject: true, reason: 'not_authenticated' }
	}
	return { continue: true }
}

Logging

src/sync/middleware.ts
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`)
}
src/sync/middleware.js

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

src/sync/game/middleware.ts
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 }
}
src/sync/game/middleware.js

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 }
}

Next Steps

On this page