- Overview
- Middleware Pipeline
- Authentication Middleware
- Authorization and Permissions Middleware
- Bootstrap Middleware
- Logging Middleware
- Performance Middleware
- Plugin Middleware
- Route Structure
- Creating Custom Middleware
- Complete Examples
SonicJS AI uses Hono, a lightweight web framework optimized for Cloudflare Workers. The middleware pipeline processes every request through a series of layers before reaching route handlers.
- Middleware: Functions that execute before route handlers to process requests
- Middleware Ordering: Order matters - middleware executes in the sequence it's registered
- Route Protection: Middleware can protect routes based on authentication, roles, and permissions
- Context Object: Middleware can set values on the context (
c.set()) for downstream use
The middleware pipeline executes in the following order for every request:
// File: /Users/lane/Dev/refs/sonicjs-ai/src/index.ts
// 1. Bootstrap - System initialization (runs once per worker)
app.use('*', bootstrapMiddleware())
// 2. Logging - Request/response logging
app.use('*', loggingMiddleware())
app.use('*', securityLoggingMiddleware())
app.use('*', performanceLoggingMiddleware(1000)) // Log slow requests
// 3. Standard Middleware
app.use('*', logger()) // Hono's built-in logger
app.use('*', cors()) // CORS headers
app.use('*', securityHeaders()) // Security headers
app.use('/api/*', prettyJSON()) // JSON formatting for API routes
// 4. Route-specific middleware (applied to specific paths)
app.use('/admin/*', requireAuth())
app.use('/admin/*', requireRole(['admin', 'editor']))
app.use('/admin/*', cacheHeaders(60))Request
↓
Bootstrap Middleware → Check/run system initialization
↓
Logging Middleware → Log request start
↓
Security Logging → Check for suspicious patterns
↓
Performance Logging → Track request timing
↓
Standard Middleware → CORS, security headers, etc.
↓
Route-specific Middleware → Auth, roles, permissions
↓
Route Handler → Execute business logic
↓
Logging Middleware → Log request completion
↓
Response
The authentication system uses JWT tokens stored in HTTP-only cookies.
/Users/lane/Dev/refs/sonicjs-ai/src/middleware/auth.ts
import { AuthManager } from '../middleware/auth'
// Generate JWT token (pass JWT_SECRET from c.env)
const token = await AuthManager.generateToken(
userId,
email,
role,
c.env.JWT_SECRET
)
// Verify JWT token — always pass the secret from c.env
const payload = await AuthManager.verifyToken(token, c.env.JWT_SECRET)
// Returns: { userId, email, role, exp, iat } or null
// Or, from inside a Hono handler, let the helper extract the token
// (Authorization header / auth_token cookie) and secret for you:
const payload = await AuthManager.verifyAuthRequest(c)
// Hash password
const hash = await AuthManager.hashPassword(password)
// Verify password
const isValid = await AuthManager.verifyPassword(password, hash)Requires valid authentication to access a route.
import { requireAuth } from '../middleware/auth'
// Protect a route
app.get('/protected', requireAuth(), async (c) => {
const user = c.get('user')
// user contains: { userId, email, role, exp, iat }
return c.json({ message: 'Welcome!', user })
})How it works:
- Checks for token in
Authorizationheader (Bearer token) - Falls back to
auth_tokencookie if no header present - Verifies token with KV cache (5-minute TTL)
- Falls back to JWT verification if not cached
- Sets
userobject on context for downstream use - Returns 401 error or redirects to login if invalid
Example Usage:
// API route with authentication
authRoutes.get('/me', requireAuth(), async (c) => {
const user = c.get('user')
const db = c.env.DB
const userData = await db.prepare(
'SELECT id, email, username, role FROM users WHERE id = ?'
).bind(user.userId).first()
return c.json({ user: userData })
})Requires specific role(s) to access a route.
import { requireRole } from '../middleware/auth'
// Single role
app.get('/admin-only',
requireAuth(),
requireRole('admin'),
async (c) => {
return c.json({ message: 'Admin area' })
}
)
// Multiple roles (any of them)
app.get('/content-edit',
requireAuth(),
requireRole(['admin', 'editor']),
async (c) => {
return c.json({ message: 'Content editing area' })
}
)Role Hierarchy:
admin- Full system accesseditor- Content management and publishingviewer- Read-only access
Allows authenticated and unauthenticated access, but populates user if authenticated.
import { optionalAuth } from '../middleware/auth'
// Public API with optional auth
app.get('/content', optionalAuth(), async (c) => {
const user = c.get('user')
const db = c.env.DB
// Show different content based on authentication
const query = user
? 'SELECT * FROM content WHERE status IN (?, ?, ?)'
: 'SELECT * FROM content WHERE status = ?'
const params = user
? ['published', 'draft', 'scheduled']
: ['published']
const { results } = await db.prepare(query).bind(...params).all()
return c.json({ data: results, authenticated: !!user })
})Fine-grained permission system for controlling access to specific resources.
/Users/lane/Dev/refs/sonicjs-ai/src/middleware/permissions.ts
import { PermissionManager } from '../middleware/permissions'
// Get user permissions
const permissions = await PermissionManager.getUserPermissions(db, userId)
// Returns: { userId, role, permissions: string[], teamPermissions: {} }
// Check single permission
const hasPermission = await PermissionManager.hasPermission(
db,
userId,
'content.edit'
)
// Check multiple permissions
const permissionMap = await PermissionManager.checkMultiplePermissions(
db,
userId,
['content.edit', 'content.publish', 'users.create']
)
// Returns: { 'content.edit': true, 'content.publish': false, ... }
// Clear user permission cache
PermissionManager.clearUserCache(userId)
// Clear all permission cache
PermissionManager.clearAllCache()Requires specific permission to access a route.
import { requirePermission } from '../middleware/permissions'
// Single permission
app.post('/content',
requireAuth(),
requirePermission('content.create'),
async (c) => {
// User has content.create permission
return c.json({ message: 'Content created' })
}
)
// Permission with team context
app.get('/teams/:teamId/settings',
requireAuth(),
requirePermission('team.settings', 'teamId'),
async (c) => {
const teamId = c.req.param('teamId')
// User has team.settings permission for this specific team
return c.json({ message: `Team ${teamId} settings` })
}
)Requires at least one of the specified permissions.
import { requireAnyPermission } from '../middleware/permissions'
app.post('/content/publish',
requireAuth(),
requireAnyPermission([
'content.publish',
'content.admin'
]),
async (c) => {
// User has either content.publish OR content.admin permission
return c.json({ message: 'Content published' })
}
)Permissions follow the pattern: resource.action
Common Permissions:
content.create- Create contentcontent.edit- Edit contentcontent.publish- Publish contentcontent.delete- Delete contentusers.create- Create usersusers.edit- Edit usersusers.delete- Delete usersteam.settings- Manage team settingsmedia.upload- Upload media filesmedia.delete- Delete media files
Log user activities for audit trails:
import { logActivity } from '../middleware/permissions'
await logActivity(
db,
userId,
'content.published', // action
'content', // resourceType
contentId, // resourceId
{ title: 'My Post' }, // details (optional)
ipAddress, // IP address (optional)
userAgent // User agent (optional)
)Handles system initialization on worker startup.
/Users/lane/Dev/refs/sonicjs-ai/src/middleware/bootstrap.ts
The bootstrap middleware runs once per Cloudflare Worker instance to:
- Run pending database migrations
- Sync collection configurations
- Bootstrap core plugins
- Install demo plugins (development only)
import { bootstrapMiddleware, resetBootstrap } from '../middleware/bootstrap'
// Apply to all routes
app.use('*', bootstrapMiddleware())
// Reset bootstrap flag (useful for testing)
resetBootstrap()let bootstrapComplete = false
export function bootstrapMiddleware() {
return async (c: Context, next: Next) => {
// Skip if already bootstrapped
if (bootstrapComplete) {
return next()
}
// Skip bootstrap for static assets
const path = c.req.path
if (
path.startsWith('/images/') ||
path.startsWith('/assets/') ||
path === '/health' ||
path.endsWith('.js') ||
path.endsWith('.css')
) {
return next()
}
try {
console.log('[Bootstrap] Starting system initialization...')
// 1. Run database migrations
const migrationService = new MigrationService(c.env.DB)
await migrationService.runPendingMigrations()
// 2. Sync collection configurations
await syncCollections(c.env.DB)
// 3. Bootstrap core plugins
const bootstrapService = new PluginBootstrapService(c.env.DB)
const needsBootstrap = await bootstrapService.isBootstrapNeeded()
if (needsBootstrap) {
await bootstrapService.bootstrapCorePlugins()
await bootstrapService.installDemoPlugins()
}
bootstrapComplete = true
console.log('[Bootstrap] System initialization completed')
} catch (error) {
console.error('[Bootstrap] Error during initialization:', error)
// Continue even if bootstrap fails
}
return next()
}
}- Bootstrap runs on first request only
- Skips static assets for performance
- Continues even if bootstrap fails (graceful degradation)
- Use
resetBootstrap()in tests to force re-initialization
Comprehensive request/response logging with security monitoring.
/Users/lane/Dev/refs/sonicjs-ai/src/middleware/logging.ts
Logs all HTTP requests and responses.
import { loggingMiddleware } from '../middleware/logging'
app.use('*', loggingMiddleware())What it logs:
- Request method, URL, and headers
- Response status code
- Request duration
- User ID (if authenticated)
- IP address and user agent
- Request ID (generated UUID)
Example logged data:
{
method: 'POST',
url: 'https://example.com/api/content',
status: 201,
duration: 145, // milliseconds
userId: 'user-123',
requestId: 'req-abc-xyz',
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
source: 'http-middleware'
}Provides more verbose logging including headers and request body.
import { detailedLoggingMiddleware } from '../middleware/logging'
// Use for specific routes that need detailed logging
app.use('/api/critical/*', detailedLoggingMiddleware())Additional information logged:
- Request headers (all)
- Response headers (all)
- Content-Type and Content-Length
- Response size
Monitors suspicious activity and security events.
import { securityLoggingMiddleware } from '../middleware/logging'
app.use('*', securityLoggingMiddleware())What it monitors:
- Suspicious request patterns (SQL injection, XSS attempts)
- Authentication failures
- Admin area access
- Unauthorized access attempts
Suspicious patterns detected:
const suspiciousPatterns = [
/script[^>]*>/i, // XSS attempts
/javascript:/i, // JavaScript protocol
/on\w+\s*=/i, // Event handlers
/\.\.\/\.\.\//, // Directory traversal
/\/etc\/passwd/i, // System file access
/union\s+select/i, // SQL injection
/drop\s+table/i // SQL injection
]Example security log:
await logger.logSecurity(
'Suspicious request pattern detected',
'medium', // severity: low, medium, high, critical
{
userId: user?.userId,
requestId,
ipAddress,
userAgent,
method,
url,
tags: ['suspicious-pattern', 'xss-attempt']
}
)Tracks slow requests for performance monitoring.
import { performanceLoggingMiddleware } from '../middleware/logging'
// Log requests slower than 1000ms (1 second)
app.use('*', performanceLoggingMiddleware(1000))
// Log requests slower than 500ms for critical API
app.use('/api/critical/*', performanceLoggingMiddleware(500))Example performance log:
{
message: 'Slow request detected: GET /api/content took 1234ms',
method: 'GET',
url: '/api/content',
duration: 1234,
threshold: 1000,
userId: 'user-123',
requestId: 'req-abc-xyz',
tags: ['slow-request', 'performance']
}Middleware for caching, compression, and security headers.
/Users/lane/Dev/refs/sonicjs-ai/src/middleware/performance.ts
Adds cache-control headers to responses.
import { cacheHeaders } from '../middleware/performance'
// Cache for 60 seconds
app.use('/admin/*', cacheHeaders(60))
// Cache for 5 minutes
app.use('/api/static/*', cacheHeaders(300))How it works:
- Only caches successful HTML responses (200 status)
- Sets
Cache-Control: private, max-age={maxAge} - Private caching prevents CDN caching of authenticated pages
Compresses responses with gzip or brotli.
import { compressionMiddleware } from '../middleware/performance'
app.use('*', compressionMiddleware)Note: Currently disabled in production due to encoding issues with Cloudflare Workers.
Adds security headers to all responses.
import { securityHeaders } from '../middleware/performance'
app.use('*', securityHeaders())Headers added:
'X-Content-Type-Options': 'nosniff'
'X-Frame-Options': 'SAMEORIGIN'
'X-XSS-Protection': '1; mode=block'Controls access to plugin routes based on plugin activation status.
/Users/lane/Dev/refs/sonicjs-ai/src/middleware/plugin-middleware.ts
Ensures a plugin is active before allowing access to its routes.
import { requireActivePlugin } from '../middleware/plugin-middleware'
// Protect FAQ plugin routes
app.use('/admin/faq/*', requireActivePlugin('faq'))
app.route('/admin/faq', adminFAQRoutes)
// Protect workflow plugin routes
app.use('/admin/workflow/*', requireActivePlugin('workflow'))
app.route('/admin/workflow', createWorkflowAdminRoutes())
// Protect cache plugin routes
app.use('/admin/cache/*', requireActivePlugin('cache'))
app.route('/admin/cache', cacheRoutes)How it works:
- Queries database for plugin status
- Returns 404 with user-friendly message if plugin is not active
- Allows request to continue if plugin is active
- Fails open (allows access) if database query fails
Example error page:
<div class="min-h-screen flex items-center justify-center bg-gray-900">
<div class="text-center">
<h1 class="text-4xl font-bold text-white mb-4">
Feature Not Available
</h1>
<p class="text-gray-300 mb-6">
The faq plugin is not currently active.
</p>
<a href="/admin" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg">
Return to Admin Dashboard
</a>
</div>
</div>Requires multiple plugins to be active.
import { requireActivePlugins } from '../middleware/plugin-middleware'
// Requires both workflow AND email plugins
app.use('/admin/notifications/*', requireActivePlugins([
'workflow',
'email'
]))import {
getActivePlugins,
isPluginActive
} from '../middleware/plugin-middleware'
// Get all active plugins
const plugins = await getActivePlugins(db)
// Returns: [{ name, display_name, icon, settings }, ...]
// Check if specific plugin is active
const isActive = await isPluginActive(db, 'faq')
// Returns: booleanSonicJS AI organizes routes into logical modules.
src/routes/
├── api.ts # Public API endpoints
├── api-media.ts # Media API endpoints
├── admin.ts # Admin dashboard and core routes
├── admin-content.ts # Content management routes
├── admin-faq.ts # FAQ management routes
├── admin-design.ts # Design/theme management
├── admin-checkboxes.ts # Checkbox field type routes
├── admin-logs.ts # System logs viewer
├── admin-users.ts # User management routes
├── admin-media.ts # Media library management
├── admin-plugins.ts # Plugin management routes
├── auth.ts # Authentication routes
├── content.ts # Content API routes
├── docs.ts # Documentation routes
└── media.ts # Media serving routes
No authentication required.
// Authentication pages
GET /auth/login # Login page
POST /auth/login # Login form submission
GET /auth/register # Registration page
POST /auth/register # Registration form submission
GET /auth/logout # Logout
POST /auth/logout # Logout API
// Password reset
POST /auth/request-password-reset # Request password reset
GET /auth/reset-password # Reset password page
POST /auth/reset-password # Reset password form
// Invitation acceptance
GET /auth/accept-invitation # Accept invitation page
POST /auth/accept-invitation # Accept invitation form
// Public API
GET /api/ # OpenAPI specification
GET /api/health # Health check
GET /api/collections # List collections
GET /api/content # List content (published only)
GET /api/collections/:collection/content # Collection content
// Documentation
GET /docs # Documentation home
// Static files
GET /images/* # Serve images
GET /media/serve/:key # Serve media files
// Health check
GET /health # System healthRequires authentication (Bearer token or cookie).
// User info
GET /auth/me # Get current user
POST /auth/refresh # Refresh token
// Media upload
POST /api/media/upload # Upload single file
POST /api/media/upload-multiple # Upload multiple files
POST /api/media/bulk-delete # Delete multiple files
DELETE /api/media/:id # Delete single file
PATCH /api/media/:id # Update file metadataRequires authentication + admin or editor role.
// Dashboard
GET /admin/ # Admin dashboard
GET /admin/api/stats # Dashboard statistics (HTMX)
GET /admin/api/system-status # System status (HTMX)
// Collections
GET /admin/collections # List collections
GET /admin/collections/new # New collection form
POST /admin/collections # Create collection
GET /admin/collections/:id # Edit collection
PUT /admin/collections/:id # Update collection
DELETE /admin/collections/:id # Delete collection
// Collection fields
POST /admin/collections/:id/fields # Add field
PUT /admin/collections/:collectionId/fields/:fieldId # Update field
DELETE /admin/collections/:collectionId/fields/:fieldId # Delete field
POST /admin/collections/:collectionId/fields/reorder # Reorder fields
// Content
GET /admin/content/ # List content
GET /admin/content/new # New content form
POST /admin/content/ # Create content
GET /admin/content/:id/edit # Edit content form
PUT /admin/content/:id # Update content
DELETE /admin/content/:id # Delete content
// Media
GET /admin/media/ # Media library
GET /admin/media/search # Search media (HTMX)
GET /admin/media/:id/details # File details (HTMX)
POST /admin/media/upload # Upload files
PUT /admin/media/:id # Update metadata
DELETE /admin/media/:id # Delete file
// Users
GET /admin/users # List users
POST /admin/users/:id/toggle # Toggle user status
GET /admin/users/export # Export users CSV
// Plugins
GET /admin/plugins # List plugins
POST /admin/plugins/:id/toggle # Toggle plugin status
GET /admin/plugins/:id # Plugin details
// Settings
GET /admin/settings # Settings page
GET /admin/settings/:tab # Settings tab
POST /admin/settings # Save settings
// Migrations
GET /admin/api/migrations/status # Migration status
POST /admin/api/migrations/run # Run migrations
GET /admin/api/migrations/validate # Validate schema
// Logs
GET /admin/logs # View logs
GET /admin/logs/:id # Log details
// API Reference
GET /admin/api-reference # API documentation
GET /admin/field-types # Field types referenceRoutes that require specific plugins to be active.
// FAQ Plugin
GET /admin/faq/ # List FAQs
GET /admin/faq/new # New FAQ form
POST /admin/faq/ # Create FAQ
GET /admin/faq/:id # Edit FAQ
PUT /admin/faq/:id # Update FAQ
DELETE /admin/faq/:id # Delete FAQ
// Workflow Plugin
GET /admin/workflow/ # Workflow dashboard
GET /admin/workflow/rules # Workflow rules
POST /admin/workflow/rules # Create rule
GET /api/workflow/content/:id # Content workflow status
POST /api/workflow/content/:id/transition # Transition content
// Cache Plugin
GET /admin/cache/ # Cache dashboard
POST /admin/cache/clear # Clear cache
GET /admin/cache/stats # Cache statisticsimport { Context, Next } from 'hono'
type Bindings = {
DB: D1Database
KV: KVNamespace
}
type Variables = {
user?: {
userId: string
email: string
role: string
}
customData?: any
}
export function customMiddleware() {
return async (c: Context<{ Bindings: Bindings; Variables: Variables }>, next: Next) => {
// 1. Pre-processing (before route handler)
console.log('Before route handler')
// 2. Set context variables
c.set('customData', { foo: 'bar' })
// 3. Call next middleware/handler
await next()
// 4. Post-processing (after route handler)
console.log('After route handler')
}
}export function timingMiddleware() {
return async (c: Context, next: Next) => {
const start = Date.now()
// Store start time in context
c.set('startTime', start)
// Execute request
await next()
// Calculate duration
const duration = Date.now() - start
// Add header to response
c.header('X-Response-Time', `${duration}ms`)
// Log if slow
if (duration > 1000) {
console.warn(`Slow request: ${c.req.method} ${c.req.path} - ${duration}ms`)
}
}
}
// Usage
app.use('*', timingMiddleware())export function rateLimitMiddleware(maxRequests: number, windowMs: number) {
const requests = new Map<string, { count: number; resetTime: number }>()
return async (c: Context, next: Next) => {
const clientId = c.req.header('cf-connecting-ip') || 'unknown'
const now = Date.now()
// Get or create rate limit record
let record = requests.get(clientId)
if (!record || now > record.resetTime) {
record = { count: 0, resetTime: now + windowMs }
requests.set(clientId, record)
}
// Check rate limit
if (record.count >= maxRequests) {
const retryAfter = Math.ceil((record.resetTime - now) / 1000)
c.header('Retry-After', retryAfter.toString())
return c.json({ error: 'Rate limit exceeded' }, 429)
}
// Increment counter
record.count++
// Add rate limit headers
c.header('X-RateLimit-Limit', maxRequests.toString())
c.header('X-RateLimit-Remaining', (maxRequests - record.count).toString())
c.header('X-RateLimit-Reset', record.resetTime.toString())
await next()
}
}
// Usage
app.use('/api/*', rateLimitMiddleware(100, 60000)) // 100 requests per minuteimport { z } from 'zod'
export function validateQuery(schema: z.ZodSchema) {
return async (c: Context, next: Next) => {
const query = Object.fromEntries(
new URL(c.req.url).searchParams.entries()
)
const result = schema.safeParse(query)
if (!result.success) {
return c.json({
error: 'Invalid query parameters',
details: result.error.errors
}, 400)
}
// Store validated query in context
c.set('validatedQuery', result.data)
await next()
}
}
// Usage
const paginationSchema = z.object({
page: z.string().regex(/^\d+$/).transform(Number).default('1'),
limit: z.string().regex(/^\d+$/).transform(Number).default('20'),
sort: z.enum(['asc', 'desc']).default('desc')
})
app.get('/content',
validateQuery(paginationSchema),
async (c) => {
const query = c.get('validatedQuery')
// query is typed and validated
return c.json({ page: query.page, limit: query.limit })
}
)export function withTransaction() {
return async (c: Context, next: Next) => {
const db = c.env.DB
try {
// Start transaction
await db.exec('BEGIN TRANSACTION')
// Store transaction flag
c.set('inTransaction', true)
// Execute route handler
await next()
// Commit if successful
await db.exec('COMMIT')
} catch (error) {
// Rollback on error
await db.exec('ROLLBACK')
throw error
}
}
}
// Usage
app.post('/bulk-operation',
requireAuth(),
withTransaction(),
async (c) => {
// All database operations will be in a transaction
const db = c.env.DB
await db.prepare('INSERT INTO table1 ...').run()
await db.prepare('INSERT INTO table2 ...').run()
return c.json({ message: 'All changes committed' })
}
)import { Hono } from 'hono'
import { requireAuth, requireRole } from '../middleware/auth'
import { requirePermission } from '../middleware/permissions'
import { logActivity } from '../middleware/permissions'
const app = new Hono()
// Admin route with authentication, role, and permission checks
app.post('/admin/users/:id/delete',
requireAuth(), // Require authentication
requireRole(['admin']), // Require admin role
requirePermission('users.delete'), // Require specific permission
async (c) => {
const userId = c.req.param('id')
const currentUser = c.get('user')
const db = c.env.DB
try {
// Soft delete user
await db.prepare(
'UPDATE users SET is_active = 0, deleted_at = ? WHERE id = ?'
).bind(Date.now(), userId).run()
// Log activity
await logActivity(
db,
currentUser.userId,
'user.deleted',
'users',
userId,
{ deletedUserId: userId },
c.req.header('cf-connecting-ip'),
c.req.header('user-agent')
)
return c.json({
message: 'User deleted successfully',
userId
})
} catch (error) {
console.error('Error deleting user:', error)
return c.json({ error: 'Failed to delete user' }, 500)
}
}
)import { Hono } from 'hono'
import { optionalAuth } from '../middleware/auth'
import { getCacheService, CACHE_CONFIGS } from '../plugins/cache'
const app = new Hono()
app.get('/api/content',
optionalAuth(), // Optional authentication
async (c) => {
const user = c.get('user')
const db = c.env.DB
const limit = Math.min(
parseInt(c.req.query('limit') || '50'),
100
)
// Use cache
const cache = getCacheService(CACHE_CONFIGS.api!)
const cacheKey = cache.generateKey(
'content-list',
`user:${user?.userId || 'public'}:limit:${limit}`
)
const cached = await cache.get(cacheKey)
if (cached) {
c.header('X-Cache-Status', 'HIT')
return c.json(cached)
}
// Build query based on authentication
let query = 'SELECT * FROM content WHERE '
const params: any[] = []
if (user) {
// Authenticated users see draft, published, and scheduled
query += 'status IN (?, ?, ?)'
params.push('draft', 'published', 'scheduled')
} else {
// Unauthenticated users only see published
query += 'status = ?'
params.push('published')
}
query += ` ORDER BY created_at DESC LIMIT ${limit}`
const { results } = await db.prepare(query)
.bind(...params)
.all()
const response = {
data: results,
meta: {
count: results.length,
limit,
authenticated: !!user,
timestamp: new Date().toISOString()
}
}
// Cache the response
await cache.set(cacheKey, response)
c.header('X-Cache-Status', 'MISS')
return c.json(response)
}
)import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { requireAuth } from '../middleware/auth'
import { requirePermission } from '../middleware/permissions'
import { logActivity } from '../middleware/permissions'
const app = new Hono()
// Validation schema
const publishSchema = z.object({
contentId: z.string().uuid(),
publishAt: z.number().optional(),
notifySubscribers: z.boolean().default(false)
})
// Custom middleware to check content ownership
function requireContentOwnership() {
return async (c: Context, next: Next) => {
const { contentId } = await c.req.json()
const user = c.get('user')
const db = c.env.DB
const content = await db.prepare(
'SELECT author_id FROM content WHERE id = ?'
).bind(contentId).first() as any
if (!content) {
return c.json({ error: 'Content not found' }, 404)
}
// Allow if user is author or has admin role
if (content.author_id !== user.userId && user.role !== 'admin') {
return c.json({ error: 'Not authorized' }, 403)
}
// Store content in context
c.set('content', content)
await next()
}
}
// Middleware chain for publishing content
app.post('/content/publish',
requireAuth(), // 1. Require authentication
requirePermission('content.publish'), // 2. Require permission
zValidator('json', publishSchema), // 3. Validate request body
requireContentOwnership(), // 4. Check ownership
async (c) => { // 5. Route handler
const user = c.get('user')
const data = c.req.valid('json')
const content = c.get('content')
const db = c.env.DB
const publishAt = data.publishAt || Date.now()
const status = publishAt > Date.now() ? 'scheduled' : 'published'
// Update content
await db.prepare(`
UPDATE content
SET status = ?, published_at = ?, scheduled_at = ?, updated_at = ?
WHERE id = ?
`).bind(
status,
status === 'published' ? publishAt : null,
status === 'scheduled' ? publishAt : null,
Date.now(),
data.contentId
).run()
// Send notifications
if (data.notifySubscribers) {
// Queue notification job
await c.env.EMAIL_QUEUE?.send({
type: 'content_published',
contentId: data.contentId,
userId: user.userId
})
}
// Log activity
await logActivity(
db,
user.userId,
status === 'published' ? 'content.published' : 'content.scheduled',
'content',
data.contentId,
{
status,
publishAt,
notifySubscribers: data.notifySubscribers
},
c.req.header('cf-connecting-ip'),
c.req.header('user-agent')
)
return c.json({
message: `Content ${status} successfully`,
contentId: data.contentId,
status,
publishAt: new Date(publishAt).toISOString()
})
}
)import { Hono } from 'hono'
import { requireAuth, requireRole } from '../middleware/auth'
import { requireActivePlugin } from '../middleware/plugin-middleware'
const app = new Hono()
// All routes in this router require 'workflow' plugin to be active
app.use('*', requireActivePlugin('workflow'))
app.use('*', requireAuth())
app.use('*', requireRole(['admin', 'editor']))
app.get('/admin/workflow/', async (c) => {
const db = c.env.DB
// Get workflow rules
const { results: rules } = await db.prepare(`
SELECT * FROM workflow_rules
WHERE is_active = 1
ORDER BY priority DESC
`).all()
// Get workflow history
const { results: history } = await db.prepare(`
SELECT * FROM workflow_history
ORDER BY created_at DESC
LIMIT 50
`).all()
return c.json({
rules,
history,
meta: {
rulesCount: rules.length,
timestamp: new Date().toISOString()
}
})
})
app.post('/admin/workflow/rules', async (c) => {
const user = c.get('user')
const data = await c.req.json()
const db = c.env.DB
const ruleId = crypto.randomUUID()
await db.prepare(`
INSERT INTO workflow_rules (
id, name, description, conditions, actions,
priority, is_active, created_by, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
ruleId,
data.name,
data.description,
JSON.stringify(data.conditions),
JSON.stringify(data.actions),
data.priority || 0,
1,
user.userId,
Date.now()
).run()
return c.json({
message: 'Workflow rule created',
ruleId
}, 201)
})
export default appimport { Hono } from 'hono'
import { html } from 'hono/html'
import { requireAuth, requireRole } from '../middleware/auth'
const app = new Hono()
app.use('*', requireAuth())
app.use('*', requireRole(['admin', 'editor']))
// Render full page
app.get('/admin/content/', async (c) => {
const db = c.env.DB
const { results } = await db.prepare(`
SELECT * FROM content
ORDER BY created_at DESC
LIMIT 20
`).all()
return c.html(html`
<!DOCTYPE html>
<html>
<head>
<title>Content Management</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<h1>Content</h1>
<!-- Search form -->
<form
hx-get="/admin/content/search"
hx-target="#content-list"
hx-trigger="input changed delay:500ms from:#search-input"
>
<input
id="search-input"
name="q"
type="text"
placeholder="Search content..."
/>
</form>
<!-- Content list -->
<div id="content-list">
${results.map((item: any) => html`
<div class="content-item">
<h2>${item.title}</h2>
<p>${item.status}</p>
<button
hx-delete="/admin/content/${item.id}"
hx-confirm="Delete this content?"
hx-target="closest .content-item"
hx-swap="outerHTML"
>
Delete
</button>
</div>
`)}
</div>
</body>
</html>
`)
})
// HTMX endpoint for search
app.get('/admin/content/search', async (c) => {
const query = c.req.query('q') || ''
const db = c.env.DB
const { results } = await db.prepare(`
SELECT * FROM content
WHERE title LIKE ? OR data LIKE ?
ORDER BY created_at DESC
LIMIT 20
`).bind(`%${query}%`, `%${query}%`).all()
return c.html(html`
${results.map((item: any) => html`
<div class="content-item">
<h2>${item.title}</h2>
<p>${item.status}</p>
<button
hx-delete="/admin/content/${item.id}"
hx-confirm="Delete this content?"
hx-target="closest .content-item"
hx-swap="outerHTML"
>
Delete
</button>
</div>
`)}
`)
})
// HTMX endpoint for delete
app.delete('/admin/content/:id', async (c) => {
const id = c.req.param('id')
const db = c.env.DB
await db.prepare('DELETE FROM content WHERE id = ?')
.bind(id)
.run()
// Return empty response to remove element
return c.html('')
})
export default appThis documentation provides a comprehensive guide to routing and middleware in SonicJS AI:
- Middleware Pipeline: Ordered execution from bootstrap through to route handlers
- Authentication: JWT-based auth with requireAuth, requireRole, and optionalAuth
- Permissions: Fine-grained permission system with requirePermission and requireAnyPermission
- Bootstrap: One-time system initialization on worker startup
- Logging: Request, security, and performance logging
- Performance: Caching, compression, and security headers
- Plugins: Route protection based on plugin activation
- Routes: Complete route structure with examples
- Custom Middleware: Patterns for creating your own middleware
- Examples: Real-world examples combining multiple concepts
All middleware is designed to work with Cloudflare Workers' edge environment and Hono's lightweight framework.