SMBs need to run custom analytics scripts against their operational data, but giving an AI agent raw code execution risks runaway costs, infinite loops, or broken outputs without guardrails.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
In this tutorial you’ll build a secure, budget-enforced code execution sandbox for small business analytics. Your application will accept natural-language analytics queries, use Azure OpenAI to generate analysis code, execute that code in an E2B cloud sandbox, and enforce per-user spending limits using REAA budget and circuit-breaker packages. By the end you’ll have a working Next.js dashboard with two API routes (POST /api/execute and GET /api/metrics), a Supabase-backed results store, Langfuse observability, and a full test suite with 90%+ coverage.
Prerequisites
Node.js >= 22 (check with node --version)
pnpm 10.x (install with corepack enable && corepack prepare pnpm@10 --activate or npm install -g pnpm)
TypeScript 5.9 (comes with the scaffold)
An Azure OpenAI resource with a deployed model (you’ll need the endpoint, API key, and deployment name)
An E2B account and API key (for the cloud sandbox)
A Supabase project URL and anon key
A Langfuse account (public and secret keys, base URL)
Familiarity with TypeScript and Next.js App Router conventions
Step 1: Scaffold the Next.js project
Create a new Next.js project with the App Router, TypeScript, and ESLint. The App Router gives you route handlers under app/api/ and file-based routing for the dashboard.
The scaffolder creates package.json, tsconfig.json, next.config.ts, eslint.config.mjs, vitest.config.ts, .gitignore, and a placeholder app/ and src/ directory. Update the package metadata and add the exact scripts and engines you’ll need — replace the top-level fields in package.json:
The scaffold sets up next.config.ts with a minimal empty config — the instrumentation hook isn’t needed for the test suite since tests invoke register() directly:
ts
import type { NextConfig } from "next";const nextConfig: NextConfig = {};export default nextConfig;
Expected output: Running pnpm typecheck exits 0 with no errors. The scaffold is ready.
Step 2: Install dependencies
This project uses REAA safety packages, an E2B sandbox SDK, Supabase for persistence, Langfuse for observability, the Vercel AI SDK with Azure OpenAI, Zod for validation, and p-retry for resilient sandbox creation. Pin every version exactly — no ^ or ~ ranges.
Expected output:pnpm install resolves all dependencies and creates pnpm-lock.yaml. You now have the REAA packages (@reaatech/agent-budget-engine, @reaatech/agent-budget-middleware, @reaatech/circuit-breaker-agents, etc.) and the third-party SDKs available in node_modules/.
Step 3: Configure environment variables
Create .env.local with the credentials for Azure OpenAI, E2B, Supabase, and Langfuse. Every variable is read from process.env at runtime — never commit real values to your repository.
Create .env.local with these placeholders:
env
# Env vars used by azure-ai-code-sandbox-for-smb-analytics.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=development# Azure OpenAI — read automatically by @ai-sdk/azureAZURE_OPENAI_API_KEY=<your-azure-openai-api-key>AZURE_OPENAI_ENDPOINT=<your-resource>.openai.azure.comAZURE_OPENAI_DEPLOYMENT_NAME=<deployment-name># E2B sandbox executionE2B_API_KEY=<your-e2b-api-key># Supabase persistenceSUPABASE_URL=<your-supabase-project-url>SUPABASE_ANON_KEY=<your-supabase-anon-key># Langfuse observabilityLANGFUSE_PUBLIC_KEY=<your-langfuse-public-key>LANGFUSE_SECRET_KEY=<your-langfuse-secret-key>LANGFUSE_BASE_URL=<your-langfuse-host># Budget configurationMAX_BUDGET_PER_ANALYSIS=5.0DEFAULT_BUDGET_LIMIT=10.0
Copy the same content into .env.example (with placeholder values) so other developers know which env vars the project needs.
Expected output: The app can read all env vars at runtime. No real secrets are in version control — only placeholders in .env.example.
Step 4: Define the Zod schemas
Create the shared schema layer that validates analysis requests, analysis results, and metrics responses. These schemas are used by both the API route handler and the analysis service — one source of truth for data shape.
Expected output:pnpm typecheck passes. The schemas produce meaningful errors on invalid input — for example, a { query: "" } object throws a ZodError because of z.string().min(1).
Step 5: Create the infrastructure clients
Three small modules wire up the external service clients. Each exports a singleton or factory function that the services layer will import.
Create src/lib/pricing-provider.ts — a PricingProvider that the BudgetController uses to estimate per-request costs. The interface is structural (imported from @reaatech/agent-budget-engine):
Expected output:pnpm typecheck passes. The three modules are importable from the services layer.
Step 6: Build the budget service
The budget service is the spending-control layer. It creates a BudgetController singleton from @reaatech/agent-budget-engine with an in-memory SpendStore and the pricing provider you just wrote. It exposes four functions: defineUserBudget, checkRequestBudget, recordRequestSpend, and getBudgetState. It also subscribes to budget events and logs them to Langfuse for observability.
Expected output:pnpm typecheck passes. The controller is a singleton — all parts of the app share the same budget state.
Step 7: Build the circuit breaker and sandbox services
The circuit breaker wraps sandbox execution with failure isolation. After 3 consecutive failures, the circuit opens and subsequent calls return a fallback instead of crashing. The sandbox service wraps the E2B SDK with retry logic and a configurable timeout.
Expected output:pnpm typecheck passes. The circuit breaker imports from three separate REAA packages — @reaatech/circuit-breaker-core (the CircuitBreaker class), @reaatech/circuit-breaker-persistence (the InMemoryAdapter), and @reaatech/circuit-breaker-agents (the CircuitOpenError type). The sandbox service uses p-retry to handle transient failures and Promise.race for timeouts.
Step 8: Build the analysis orchestrator
The analysis service is the central orchestrator. It checks the budget, calls Azure OpenAI to generate code from the user’s natural-language query, spins up an E2B sandbox, runs the generated code through the circuit breaker, records spend, persists the result to Supabase, and returns a structured AnalysisResult. This is where all the pieces come together.
Expected output:pnpm typecheck passes. The orchestrator ties together the budget service, circuit breaker, sandbox, Supabase, Langfuse, and the AI SDK.
Step 9: Create the instrumentation hook
Next.js supports a register() function exported from src/instrumentation.ts that runs once at startup. Use it to initialize the Langfuse client and define a wildcard budget that applies to all users by default. The function must gate Node-only initialization behind a NEXT_RUNTIME check so it doesn’t crash the Edge runtime.
Expected output: When register() runs (tests invoke it directly; for production you’ll want experimental.instrumentationHook: true in your Next.js config), a budget of $10 (or whatever DEFAULT_BUDGET_LIMIT is set to) is automatically defined for the wildcard scope *.
Step 10: Create the barrel export
Create src/index.ts to re-export the public types and functions that external consumers would use:
ts
export type { AnalysisRequest, AnalysisResult, MetricsResponse } from './lib/schemas.js';export { runAnalysis } from './services/analysis-service.js';
Expected output:pnpm typecheck passes. The barrel provides a clean public API surface for the package.
Step 11: Create the API routes
Two route handlers expose the analysis functionality over HTTP. They live under app/api/ following App Router conventions.
Create app/api/execute/route.ts — the POST handler that accepts an analysis query:
Create app/api/metrics/route.ts — the GET handler for budget state:
ts
import { type NextRequest, NextResponse } from "next/server";import { getBudgetState } from "@/src/services/budget-service.js";export function GET(req: NextRequest) { const scopeKey = req.headers.get("x-budget-scope-key") ?? "default"; const state = getBudgetState(scopeKey); if (!state) { return NextResponse.json( { error: "No budget defined for scope" }, { status: 404 } ); } return NextResponse.json({ remaining: state.remaining, spent: state.spent, limit: state.limit, state: state.state, });}
Expected output:pnpm typecheck passes. The routes use NextRequest and NextResponse from next/server and the @/ path alias configured in tsconfig.json ("@/*": ["./*"]).
Step 12: Create the dashboard UI
The dashboard is a client component with a textarea for queries, an Execute button, a budget progress bar, and error/result display areas. It calls POST /api/execute on submit and GET /api/metrics on mount.
Expected output: Running pnpm dev and navigating to http://localhost:3000 shows a dashboard with a textarea, Execute button, and budget bar.
Step 13: Run the tests
The test suite covers every module with happy-path, error-path, and boundary tests. Run the full suite:
terminal
pnpm test
Expected output: All 65 tests pass (numFailedTests === 0), coverage thresholds are met (lines >= 90%, branches >= 90%, functions >= 90%, statements >= 90%), and vitest-report.json is written with the results.
Add DynamoDB or Redis persistence for the circuit breaker — replace InMemoryAdapter with DynamoDBAdapter or RedisAdapter to survive process restarts across multiple instances. The @aws-sdk/client-dynamodb and @aws-sdk/util-dynamodb packages are already installed as dev dependencies.
Add a budget admin API — create PUT /api/budget and DELETE /api/budget/:scope endpoints for managing user budgets at runtime.
Add rate limiting — integrate the REAA circuit breaker with per-user rate limits to prevent a single misbehaving query from consuming all sandbox resources.