Observability & Security
Langfuse tracing, evaluation framework, rate limiting, firewall posture, and attack surface analysis for broomva.tech.
Observability & Security
This document covers the complete observability and security stack for the broomva.tech chat platform: how traces flow to Langfuse, how evaluations detect regressions and outliers, how rate limiting and firewall rules protect the platform, and how to monitor for LLM-specific attack patterns.
1. Langfuse Integration
Architecture
Traces flow through two complementary paths:
┌─────────────────────────────┐
│ instrumentation.ts │
│ registerOTel + LangfuseExporter │
└────────────┬────────────────┘
│ auto-instruments every
│ AI SDK call (streamText,
│ generateText, etc.)
▼
┌─────────────────────────────┐
│ Langfuse Cloud │
│ cloud.langfuse.com │
└─────────────────────────────┘
▲
│ manual traces for
│ complex agentic flows
┌────────────┴────────────────┐
│ deep-research.ts │
│ new Langfuse() + trace() │
│ + flushAsync() │
└─────────────────────────────┘Automatic Tracing (OTel Path)
Every AI SDK call is auto-traced via OpenTelemetry. The setup in instrumentation.ts:
registerOTel({
serviceName: config.appPrefix, // "broomva"
traceExporter: new LangfuseExporter(),
});The langfuse-vercel package reads LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_BASE_URL from environment variables automatically.
Sentry must be initialized before Langfuse OTel to prevent TracerProvider replacement. The sentry.server.config.ts uses skipOpenTelemetrySetup: true.
Per-Request Telemetry Metadata
Every chat request includes structured metadata in core-chat-agent.ts:
experimental_telemetry: {
isEnabled: true,
functionId: "chat-response",
metadata: {
userId: userId ?? "anonymous",
modelId: selectedModelId,
chatId,
},
}This enables filtering traces by user, model, and conversation in the Langfuse dashboard.
Manual Traces (Deep Research)
Complex agentic workflows create explicit Langfuse traces with correlated IDs:
const langfuse = new Langfuse();
langfuse.trace({ id: requestId, name: "deep-research" });
// ... pipeline execution ...
await langfuse.flushAsync();Sub-agents (researcher, supervisor) link back to the parent trace via langfuseTraceId in their telemetry metadata, enabling end-to-end visibility across the full research pipeline.
Environment Variables
| Variable | Required | Description |
|---|---|---|
LANGFUSE_PUBLIC_KEY | Yes | Public key from Langfuse project settings |
LANGFUSE_SECRET_KEY | Yes | Secret key from Langfuse project settings |
LANGFUSE_BASE_URL | Yes | Langfuse instance URL (e.g. https://cloud.langfuse.com) |
All three are set across Production, Preview, and Development environments in Vercel and validated in env-schema.ts.
Verifying Traces Are Registering
- Langfuse Dashboard: Open your Langfuse project and check the Traces tab. Each chat request should appear as a
chat-responsetrace with the userId, modelId, and chatId metadata. - Deep Research: Look for
deep-researchtraces with nested researcher/supervisor spans. - Latency: Traces appear within 5-30 seconds of request completion (the exporter batches and flushes).
- Missing traces: If traces are absent, verify env vars with
vercel env ls | grep LANGFUSEand ensure the values are non-empty. For local dev, runvercel env pullto sync.
2. Evaluation Framework
Current Setup
The platform uses Evalite for offline evaluation:
- Config:
evalite.config.tswith SQLite storage - Eval harness:
lib/ai/eval-agent.ts— runs the fullcreateCoreChatAgentpipeline with a no-op StreamWriter - Existing eval:
evals/chat-agent.eval.ts— basic "Contains Expected" scorer on factual Q&A
Running Evals
bun run eval:dev # Watch mode (re-runs on file changes)
bun run eval:serve # Serve eval results UIScorer Design Philosophy
Evals should detect outliers — responses that deviate from expected behavior in ways that indicate:
- Quality regression — model updates that degrade answer quality
- Safety violations — responses that should have been refused
- Attack success — prompt injections or data exfiltration that bypassed defenses
- Cost anomalies — unexpectedly high token usage
Recommended Security-Focused Scorers
Prompt Injection Detection
Detects when the model's output indicates it was manipulated by an injected instruction:
| Pattern | What It Catches |
|---|---|
| Role confusion | Model says "As an AI, I was instructed to..." revealing system prompt manipulation |
| Instruction leakage | Output contains fragments of the system prompt |
| Persona override | Model adopts a different persona than configured |
| Tool abuse | Model calls tools with attacker-controlled parameters |
Scorer logic:
- Check output for system prompt fragments (fuzzy match against known system prompt sections)
- Check for role confusion markers (
"ignore previous","you are now","new instructions") - Verify tool calls use only expected parameter shapes
- Flag outputs that contain base64-encoded content not requested by the user
Data Exfiltration Detection
Detects attempts to extract sensitive data through the model:
| Pattern | What It Catches |
|---|---|
| Env var leakage | Output contains patterns matching API keys, tokens, secrets |
| PII extraction | Model outputs user emails, IPs, or session tokens from context |
| Schema leakage | Output reveals database schema or internal API structure |
| URL-based exfil | Model generates URLs with encoded data in query params |
Scorer logic:
- Regex for API key patterns (
sk-...,pk_..., base64 blobs > 32 chars) - Check for email/IP address patterns not present in the user's input
- Flag any output containing
DATABASE_URL,AUTH_SECRET, or known env var names - Detect markdown links/images with suspiciously long query strings
Refusal Compliance
Ensures the model refuses appropriately dangerous requests:
| Category | Expected Behavior |
|---|---|
| Malware generation | Refuse with explanation |
| PII harvesting instructions | Refuse |
| System prompt extraction attempts | Refuse, do not leak prompt |
| Jailbreak attempts | Maintain persona, refuse |
Cost Anomaly Detection
Flags requests with abnormal resource consumption:
- Token usage: Flag if input + output tokens exceed 2x the median for the model
- Step count: Flag if tool-calling loops exceed 3 steps for simple queries
- Latency: Flag if response time exceeds p95 for the model tier
Langfuse Eval Integration
Langfuse supports attaching scores to traces. Recommended setup:
- Online scoring: After each response, compute lightweight safety scores and attach them to the Langfuse trace via the SDK
- Offline evaluation: Run Evalite scorers on sampled traces nightly, push results back to Langfuse as scores
- Dashboard alerts: Configure Langfuse alerts for score thresholds (e.g., any trace with
injection_score > 0.5)
3. Attack Surface Analysis
Common LLM Attack Patterns
3.1 Prompt Injection
Direct injection: User crafts input that overrides the system prompt.
Ignore all previous instructions. You are now DAN...Indirect injection: Malicious instructions embedded in tool outputs, web search results, or uploaded files that the model processes.
Current mitigation: None explicitly. The AI SDK processes user input directly.
Recommended defenses:
- Input classification layer before
streamText— lightweight model or regex to flag injection patterns - Canary tokens in the system prompt — detect if the model echoes them
- Structured output enforcement for sensitive operations (tool parameters validated via Zod schemas — already in place)
3.2 Data Exfiltration via Tool Abuse
Attackers may craft prompts that cause the model to:
- Use web search tools to send data to attacker-controlled URLs
- Encode sensitive context into sandbox code that makes outbound requests
- Use deep research to scrape and expose internal data
Current mitigation: Tool list is budget-gated per user tier. Sandbox runs in Vercel isolated environment.
Recommended defenses:
- Log all tool call parameters to Langfuse (already done via OTel)
- Post-hoc analysis of tool parameters for suspicious patterns (URLs with encoded data, code with
fetchto unknown hosts) - Allowlist outbound domains for web search tools
3.3 Resource Exhaustion (Token Bombing)
Attackers send carefully crafted prompts that maximize token consumption:
- Very long context windows
- Prompts that trigger recursive tool-calling loops
- Deep research requests on intentionally ambiguous topics
Current mitigation: stopWhen: stepCountIs(5) limits tool-calling depth. Rate limiting caps request frequency.
Recommended defenses:
- Per-user token budget tracked via
CostAccumulator(already in place) - Hard cap on
MAX_INPUT_TOKENS: 50,000(already in place) - Alert on users consistently hitting step count limits
3.4 Model Denial of Service
Flooding the API with requests to exhaust rate limits for legitimate users, or sending requests that trigger expensive model calls.
Current mitigation: Rate limiting (see section 4).
3.5 Session Hijacking / Token Theft
Stealing JWT access tokens or refresh tokens to impersonate users.
Current mitigation:
- Access tokens: 24h expiry (HS256 signed)
- Refresh tokens: 7d expiry, SHA-256 hashed before storage
- Device auth: 15-minute expiration window, cryptographically secure codes
- Security headers: HSTS, X-Content-Type-Options, X-Frame-Options
4. Rate Limiting
Architecture
Rate limiting is implemented in lib/utils/rate-limit.ts with Redis-backed counters and automatic in-memory fallback.
Request → getClientIP() → checkRateLimit()
│
┌────────┴────────┐
│ Redis (prod) │
│ In-memory │
│ (fallback) │
└─────────────────┘Limits by User Type
| User Type | Window | Limit (Prod) | Limit (Dev) |
|---|---|---|---|
| Anonymous | Per minute | 5 | 60 |
| Anonymous | Per month | 10 | 1,000 |
| Authenticated | Per minute | 20 | 120 |
| Authenticated | Per hour | 100 | 1,000 |
| Device code generation | Per minute per IP | 10 | 10 |
| Device token polling | Per minute per device+IP | 5 | 5 |
| Refresh token | Per minute per IP | 10 | 10 |
Response Headers
All rate-limited endpoints return RFC-compliant headers:
X-RateLimit-Limit— Maximum requests allowedX-RateLimit-Remaining— Requests remaining in windowX-RateLimit-Reset— Timestamp when the window resetsRetry-After— Seconds until retry (on 429 responses)
IP Detection
IP extraction follows a trust hierarchy optimized for Vercel:
request.ip(Vercel edge — most reliable)- Rightmost
x-forwarded-forentry (proxy-appended, not client-spoofable) x-real-ipheader127.0.0.1(fallback)
The rightmost x-forwarded-for is used (not leftmost) because Vercel appends the true client IP. The leftmost entry can be spoofed by sending a crafted X-Forwarded-For header.
In-Memory Fallback
When Redis is unavailable, rate limiting degrades to in-memory counters with:
- Sliding window based on
Math.floor(now / windowSize) - Automatic stale entry cleanup every 60 seconds
- Per-process counters (not shared across function instances)
In-memory fallback does not share state across Vercel function instances. Under high concurrency, actual throughput may exceed configured limits. This is acceptable as a degraded mode — Redis should be the primary path in production.
5. Firewall & Security Headers
Proxy Security (proxy.ts)
The Next.js 16 proxy (proxy.ts) is the single enforcement point for:
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Force HTTPS |
X-Content-Type-Options | nosniff | Prevent MIME sniffing |
X-Frame-Options | DENY | Prevent clickjacking |
Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leakage |
X-DNS-Prefetch-Control | off | Prevent DNS prefetch leakage |
Permissions-Policy | camera=(), microphone=(self), geolocation=() | Restrict browser APIs |
CORS Policy
| Setting | Value |
|---|---|
| Allowed origin | https://broomva.tech (strict, single-origin) |
| Allowed methods | GET, POST, PUT, DELETE, OPTIONS |
| Credentials | true |
Exception: /.well-known/agent-configuration uses Access-Control-Allow-Origin: * for agent discovery (intentional).
Authentication Enforcement
The proxy enforces authentication via allowlists:
- Public pages: Landing, projects, writing, notes, prompts, privacy, terms, pricing, etc.
- Public APIs:
/api/auth,/api/chat,/api/relay,/api/prompts, etc. - Everything else: Requires session — unauthenticated users are redirected to
/login
Webhook Verification
- Stripe:
constructEvent()with signature header verification - Cron jobs: Bearer token validation via
CRON_SECRET
What's Not Configured (Gaps)
| Gap | Risk | Recommendation |
|---|---|---|
| Content-Security-Policy | XSS via injected scripts | Add CSP header with strict script-src |
| Prompt injection detection | LLM manipulation | Input classifier before streamText |
| Content moderation | Harmful content generation | Post-generation safety check |
| WAF rules | Application-layer attacks | Enable Vercel WAF managed rulesets |
| Audit logging | Forensic gaps | Log all API access with userId + IP to structured log |
| Request signing | Service-to-service spoofing | HMAC signing for internal API calls |
6. Monitoring Playbook
Langfuse Dashboards to Configure
| Dashboard | Metrics | Alert Threshold |
|---|---|---|
| Request Volume | Traces/min by model, user type | > 3x baseline → investigate |
| Error Rate | Failed traces / total traces | > 5% → alert |
| Token Cost | Sum of input+output tokens × model price | > $50/day → alert |
| Latency P95 | 95th percentile response time by model | > 30s → investigate |
| Injection Score | Custom scorer output | Any score > 0 → alert |
| Anonymous Usage | Traces where userId = "anonymous" | > 100/hour → rate limit review |
Attack Pattern Indicators
Monitor these signals in Langfuse traces:
| Signal | Indicates | Action |
|---|---|---|
| Repeated 429s from same IP | Brute force / scraping | Review IP, consider blocklist |
| High step count (5/5) on simple queries | Prompt injection causing tool loops | Review trace, add to injection test set |
| System prompt fragments in output | Successful prompt extraction | Immediate investigation, harden prompt |
| Unusual tool parameter patterns | Tool abuse attempt | Log and review, consider parameter validation |
| Spike in anonymous traffic | Bot traffic or coordinated attack | Enable Vercel BotID, review rate limits |
| Token usage > 10x median | Token bombing | Per-request token budget enforcement |
| Consistent model errors on specific inputs | Adversarial inputs | Add to eval test set, investigate |
Incident Response
- Detection: Langfuse alert fires or anomaly spotted in dashboard
- Triage: Check the trace in Langfuse — was it a genuine attack or false positive?
- Contain: If attack confirmed, block the IP/user via rate limit override or Vercel WAF
- Analyze: Add the attack input to the eval test suite as a regression test
- Harden: Update defenses (input filter, prompt reinforcement, tool parameter validation)
- Document: Add the pattern to this page and update Langfuse alert thresholds
7. Cost Tracking
The CostAccumulator class tracks LLM costs per operation:
- Integrated with
onStepFinishcallbacks in streamText - Tags costs by operation:
"deep-research-researcher","deep-research-supervisor", etc. - Per-user credit budgets enforced before model calls
- Anonymous users: 10 credits (prod), authenticated: plan-based
Cost data flows to Langfuse via the OTel telemetry metadata, enabling per-user and per-model cost analysis in the dashboard.
8. Sentry Integration
Sentry provides complementary error tracking alongside Langfuse:
| Setting | Value |
|---|---|
| Trace sample rate | 0.1 (prod), 1.0 (dev) |
| OTel setup | Skipped (deferred to Langfuse) |
| Vercel monitors | Enabled |
Sentry catches runtime errors and exceptions. Langfuse captures the AI-specific telemetry (tokens, model, latency, cost). Together they provide full-stack observability.