Audit routing (lifecycle hooks)
When to use this
Section titled “When to use this”Use audit routing when compliance or support needs a per-decision log: which tool ran (explore, execute, compile_metric), with what args summary, plus the SQL and row count after execution. Arivie does not store audit records internally — RFC-002 routes them through lifecycle hooks to your pipeline (Axiom, Datadog, S3, etc.).
Architecture
Section titled “Architecture”Base example: examples/with-nextjs/.
Config patch
Section titled “Config patch”Add hooks to defineArivie in arivie.config.ts. onToolCall receives tool (the tool id string) for every agent step; onAfterQuery fires after execute / compile_metric runs SQL:
defineArivie({ // ... owner, model, db, semantic, resolveUser hooks: { async onToolCall(ctx) { await auditSink.write({ event: "tool.call", tool: ctx.tool, ownerId: ctx.ownerId, userId: ctx.userId, durationMs: ctx.durationMs, }); }, async onAfterQuery(ctx) { await auditSink.write({ event: "sql.executed", sql: ctx.sql, rowCount: ctx.rows.length, ownerId: ctx.ownerId, userId: ctx.userId, durationMs: ctx.durationMs, }); }, },});The reference config shape (without hooks) is:
/* SPDX-License-Identifier: Apache-2.0 */import { dirname, join } from "node:path";import { fileURLToPath } from "node:url";import { defineArivie, type ArivieInstance } from "@arivie/core";import { postgresAdapter } from "@arivie/db-postgres";import { resolveMixpanelSource } from "./lib/mixpanel-source.js";import { makeMcpServer } from "@arivie/mcp";import type { MCPServer } from "@mastra/mcp";import { loadSemanticLayerSync, type SemanticLayer } from "@arivie/semantic";import { anthropic } from "@ai-sdk/anthropic";import { createGoogleGenerativeAI } from "@ai-sdk/google";import { createOpenAI } from "@ai-sdk/openai";import type { LanguageModel } from "ai";import { MockLanguageModelV3 } from "ai/test";
const __dirname = dirname(fileURLToPath(import.meta.url));const semanticPath = join(__dirname, "semantic");
function requireDatabaseUrl(): string { const url = process.env.DATABASE_URL; if (url == null || url.length === 0) { throw new Error( "DATABASE_URL is required — copy .env.example to .env and set your Postgres URL", ); } return url;}
function resolveModel(): LanguageModel { // Preference order: Gemini (cheapest) → Anthropic → OpenAI → mock. // Override the Gemini default via GOOGLE_MODEL (e.g. "gemini-2.5-flash"). const googleKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY; if (googleKey != null && googleKey.length > 0) { const google = createGoogleGenerativeAI({ apiKey: googleKey }); const modelId = process.env.GOOGLE_MODEL ?? "gemini-2.5-flash"; return google(modelId) as LanguageModel; } const anthropicKey = process.env.ANTHROPIC_API_KEY; if (anthropicKey != null && anthropicKey.length > 0) { return anthropic("claude-sonnet-4-20250514"); } const openaiKey = process.env.OPENAI_API_KEY; if (openaiKey != null && openaiKey.length > 0) { const openai = createOpenAI({ apiKey: openaiKey }); return openai(process.env.OPENAI_MODEL ?? "gpt-5") as LanguageModel; } console.warn( "[with-nextjs] No model key set (GOOGLE_GENERATIVE_AI_API_KEY|ANTHROPIC_API_KEY|OPENAI_API_KEY) — using deterministic mock model.", ); return new MockLanguageModelV3({ provider: "mock", modelId: "mock", doGenerate: { content: [ { type: "text", text: "Example mock response (set GOOGLE_GENERATIVE_AI_API_KEY for a live Gemini run).", }, ], finishReason: "stop", usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, warnings: [], }, } as unknown as ConstructorParameters<typeof MockLanguageModelV3>[0]) as LanguageModel;}
let cached: { arivie: ArivieInstance; mcp: MCPServer; semantic: SemanticLayer;} | undefined;
export async function getArivieRuntime(): Promise<{ arivie: ArivieInstance; mcp: MCPServer;}> { if (cached == null) { const postgres = postgresAdapter({ url: requireDatabaseUrl(), readOnlyRole: "arivie_reader", }); const { adapter: mixpanel, label: mixpanelLabel } = resolveMixpanelSource(); if (mixpanelLabel.includes("mock-plan-b")) { console.warn(`[with-nextjs] ${mixpanelLabel}`); } const semantic = loadSemanticLayerSync(semanticPath); const arivie = await defineArivie({ owner: { id: process.env.ARIVIE_OWNER_ID ?? "with-nextjs-owner", name: "With Next.js Example", }, model: resolveModel(), workspace: { rootDir: semanticPath }, sources: { postgres, mixpanel }, semantic: { path: semanticPath, mode: "preload" }, compileMetric: true, // JSON-IR routing for declared measures (arxiv 2502.00032) resolveUser: async () => ({ userId: "demo-user", permissions: ["analytics:read"], dbRole: "arivie_reader", }), }); const mcp = makeMcpServer({ agent: arivie.agent, semantic, db: postgres, ownerId: process.env.ARIVIE_OWNER_ID ?? "with-nextjs-owner", ownerName: "With Next.js Example", }); cached = { arivie, mcp, semantic }; } return { arivie: cached.arivie, mcp: cached.mcp };}Run it
Section titled “Run it”cd arivie && pnpm installpnpm --filter with-nextjs devcurl -X POST http://localhost:3000/api/arivie -H 'Content-Type: application/json' -d '{"prompt":"How many customers?"}'Verify your audit sink receives tool.call and sql.executed events for the request.