Patterns & Recipes
Copy, paste, ship.
Production-ready patterns for common use cases. Each recipe includes the complete code, an explanation of the approach, and which faces are used.
Auth + RBAC setup
Protect routes by role. Create users with roles, verify sessions, and gate access to faces based on permissions.
import { ee } from '@/lib/ee'
import { NextRequest, NextResponse } from 'next/server'
// Middleware: verify session + check role
export async function withAuth(
req: NextRequest,
requiredRole: 'admin' | 'editor' | 'viewer'
) {
const token = req.headers.get('Authorization')?.replace('Bearer ', '')
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const session = await ee.auth.verifySession({ token })
if (!session.valid) {
return NextResponse.json({ error: 'Invalid session' }, { status: 401 })
}
const user = await ee.auth.getUser({ id: session.userId })
if (!user.roles.includes(requiredRole)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
return { user, session }
}
// Usage in a route handler
export async function POST(req: NextRequest) {
const auth = await withAuth(req, 'admin')
if (auth instanceof NextResponse) return auth
// auth.user is available — admin-only logic here
return NextResponse.json({ data: 'secret admin data' })
}File upload with preview
Upload files to storage and generate optimised preview thumbnails using media transforms. Presigned URLs for direct browser uploads.
import { ee } from '@/lib/ee'
// 1. Get a presigned upload URL (browser uploads directly to storage)
const upload = await ee.storage.createPresignedUpload({
key: `uploads/${crypto.randomUUID()}.jpg`,
contentType: 'image/jpeg',
maxSize: '10mb',
expiresIn: '15m',
})
// 2. Browser uploads to upload.url (no server proxy needed)
// await fetch(upload.url, { method: 'PUT', body: file })
// 3. Generate a thumbnail preview after upload
const thumbnail = await ee.media.transform({
source: upload.key,
output: upload.key.replace('uploads/', 'thumbnails/'),
operations: [
{ type: 'resize', width: 400, height: 300, fit: 'cover' },
{ type: 'format', format: 'webp', quality: 80 },
],
})
// thumbnail.url => CDN URL for the optimised previewAI chat with streaming
Stream AI responses to the browser using Server-Sent Events. Token-by-token output with proper connection handling.
import { ee } from '@/lib/ee'
// API route: stream AI response as SSE
export async function POST(req: Request) {
const { messages } = await req.json()
const stream = await ee.ai.generate({
model: 'claude-sonnet-4-20250514',
messages,
stream: true,
})
// Return as a ReadableStream (SSE)
return new Response(
new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const data = JSON.stringify({
text: chunk.text,
done: chunk.done,
})
controller.enqueue(
new TextEncoder().encode(`data: ${data}\n\n`)
)
}
controller.close()
},
}),
{
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
}
)
}Webhook handler
Verify webhook signatures, parse typed events, and route to the correct handler. Idempotent processing with deduplication.
import { ee } from '@/lib/ee'
import type { WebhookEvent } from '@evileye/sdk'
export async function POST(req: Request) {
const body = await req.text()
const signature = req.headers.get('x-ee-signature')!
// 1. Verify the webhook signature
const valid = await ee.webhook.verify({
body,
signature,
secret: process.env.WEBHOOK_SECRET!,
})
if (!valid) {
return new Response('Invalid signature', { status: 401 })
}
// 2. Parse the typed event
const event: WebhookEvent = JSON.parse(body)
// 3. Route to the correct handler (TypeScript narrows the type)
switch (event.type) {
case 'user.created':
await onUserCreated(event.data) // { userId, email }
break
case 'invoice.paid':
await onInvoicePaid(event.data) // { invoiceId, amount }
break
case 'storage.uploaded':
await onFileUploaded(event.data) // { key, size, type }
break
}
return new Response('OK', { status: 200 })
}Multi-tenant data isolation
Entity-scoped queries ensure tenants never see each other’s data. The SDK enforces isolation at the client level.
import { createClient } from '@evileye/sdk'
// Each tenant gets their own scoped client
function createTenantClient(entityId: string, token: string) {
return createClient({
token,
entity: entityId,
// All queries are automatically scoped to this entity
// Cross-entity access is impossible with scoped tokens
})
}
// In a request handler:
export async function GET(req: Request) {
const entityId = req.headers.get('x-entity-id')!
const token = req.headers.get('authorization')!.replace('Bearer ', '')
const tenant = createTenantClient(entityId, token)
// This query ONLY returns data for this entity
const users = await tenant.database.query({
table: 'users',
where: { active: true },
orderBy: { createdAt: 'desc' },
limit: 50,
})
// Even if you omit a WHERE clause, the entity scope applies
// There is no way to access another tenant\'s data
return Response.json({ data: users })
}Background job with retry
Enqueue background jobs with automatic retry, exponential backoff, and dead-letter handling. Monitor progress in real-time.
import { ee } from '@/lib/ee'
// 1. Enqueue a background job
const job = await ee.queue.enqueue({
queue: 'email-campaigns',
payload: {
campaignId: 'camp_123',
recipientCount: 50_000,
},
options: {
retries: 5,
backoff: 'exponential', // 1s, 2s, 4s, 8s, 16s
deadLetterQueue: 'failed-campaigns',
timeout: '5m',
},
})
// 2. Process jobs with a compute function
await ee.compute.createFunction({
name: 'process-email-campaign',
queue: 'email-campaigns',
handler: async (payload) => {
const campaign = await ee.database.get({
table: 'campaigns',
id: payload.campaignId,
})
for (const batch of chunk(campaign.recipients, 100)) {
await ee.email.sendBatch({
template: campaign.templateId,
recipients: batch,
})
}
return { sent: campaign.recipients.length }
},
})
// 3. Check job status
const status = await ee.queue.getJob({ id: job.id })
// status.state => 'completed' | 'processing' | 'failed' | 'queued'Ready to build your own?
Start with the quickstart, then adapt these patterns to your use case.