A 10-person health-tech startup is building a clinical decision support agent. They handle PHI and need to ensure no protected health information leaks to the LLM or appears in responses. Manual redaction is error-prone and doesn't scale. They need an automated pipeline that redacts PII at both ingress (before sending to the LLM) and egress (before showing to the user), with configurable rules for HIPAA, GDPR, or PCI. The solution must be auditable and not slow down responses.
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.
This tutorial walks you through building a configurable PII redaction pipeline that strips protected health information, personally identifiable information, and payment card data from LLM calls and responses. You’ll build a dual-pipeline architecture — ingress redaction strips sensitive data before it reaches the LLM, and egress scanning catches anything that slipped through before it reaches the user. The system is regulation-configurable: switch between HIPAA, GDPR, and PCI compliance profiles at runtime, with audit logging for every event.
Prerequisites
Node.js 22+ and pnpm 10 installed on your machine
Basic familiarity with TypeScript and Next.js App Router
No API keys required — this recipe runs entirely offline with regex-based detection
Step 1: Scaffold the Next.js project
Create the project shell with Next.js 16, TypeScript, and pnpm.
Expected output: The pnpm add commands print saving to node_modules and the dependencies show up in package.json. All six @reaatech/* versions must be exact (no ^ or ~). Verify with:
terminal
pnpm list --depth=0 | grep reaatech
Step 2: Configure environment variables
Create the .env.example file that documents every environment variable your application reads. These set the regulation profile, redaction strategy, guardrail chain budget, and audit log path.
Expected output: A single file at src/types.ts exporting five interfaces and two type aliases.
Step 4: Create regulation profiles
Each regulation — HIPAA, GDPR, and PCI — defines its own set of regex patterns. These patterns are the heart of the redaction logic. Build them in a config module.
Expected output:src/config/regulation-profiles.ts exports REGULATION_PATTERNS, getPatternsForRegulation(), and a regulationProfileSchema Zod schema. HIPAA defines SSN, phone, email, MRN (7-digit medical record number), and date of birth. GDPR adds IP address and personal ID. PCI targets credit card numbers, CVV, and expiry dates.
Step 5: Build the PII detector utility
The PII detector utility handles three things: detecting PII categories in arbitrary text, implementing the Luhn algorithm for credit card validation, and providing helper functions for mask and remove strategies.
Expected output:src/lib/pii-detector.ts exports detectPIICategories(), luhnCheck(), redactWithMask(), and redactWithRemove(). The Luhn check prevents false positives on random digit sequences that happen to match the credit card pattern.
Step 6: Wire the configuration loader and validator
The config loader reads environment variables and feeds them into @reaatech/guardrail-chain-config’s loadConfig(). The validator uses both the chain config validator and @reaatech/mcp-gateway-validation’s schema system to validate redaction configuration at runtime.
ts
// src/config/loader.tsimport { loadConfig } from "@reaatech/guardrail-chain-config";import type { RedactionConfig, Regulation, RedactionStrategy } from "../types.js";function parseRegulation(value: string | undefined): Regulation { if (value === "gdpr" || value === "pci") return value; return "hipaa";}function parseRedactionStrategy(value: string | undefined): RedactionStrategy { if (value === "remove") return "remove"; return "mask";}export async function loadAppConfig(): Promise<RedactionConfig> { await loadConfig({ useEnv: true, envPrefix: "GUARDRAIL_CHAIN" }); return { regulation: parseRegulation(process.env.REGULATION), strategy: parseRedactionStrategy(process.env.REDACTION_STRATEGY), };}
// src/config/index.tsexport { REGULATION_PATTERNS, getPatternsForRegulation, regulationProfileSchema,} from "./regulation-profiles.js";export { type Regulation, type RedactionStrategy, type RedactionConfig, type PIIRedactionRequest, type PIIRedactionResponse,} from "../types.js";
Expected output: Three files in src/config/. The loader returns a merged RedactionConfig from the REGULATION and REDACTION_STRATEGY env vars, defaulting to hipaa and mask respectively. The validator runs config through three validation layers: validateConfigSafe, JSON Schema validation, and the MCP custom schema manager.
Step 7: Create the error classes
Define typed error classes so callers can distinguish configuration failures from redaction failures from budget violations.
Expected output:src/lib/errors.ts exports four error code constants and PIIRedactionError, a typed error class that carries a correlationId and code for audit trail correlation.
Step 8: Build the PII redaction service
This is the core of the recipe. The PIIRedactionService creates a dual pipeline of guardrail chains. The ingress chain uses PIIRedaction from @reaatech/guardrail-chain-guardrails to strip PII before it reaches the LLM. The egress chain uses PIIScan to catch any PII that the LLM might have generated or echoed back. Both chains use ChainBuilder with budget limits and error handling.
Expected output:src/services/pii-redaction-service.ts with three public methods: redact() (full ingress + egress pipeline), redactIngress() (ingress-only), and redactEgress() (egress-only). Each chain sets a budget of 500ms max latency and 4000 tokens, with 2 retries at 200ms intervals.
Step 9: Build the audit service
The audit service logs every redaction event to composable backends. It uses CompositeAuditLogger from @reaatech/mcp-gateway-audit to fan out to console logging and in-memory storage, and exposes a query API for the audit endpoint.
// src/index.tsexport { PIIRedactionService } from "./services/pii-redaction-service.js";export { AuditService } from "./services/audit-service.js";export * from "./types.js";
Expected output:src/services/audit-service.ts wraps CompositeAuditLogger with ConsoleAuditLogger and MemoryAuditStorage. Every redaction produces a tool.executed audit event. Querying supports filtering by event type, date range, and limit.
Step 10: Set up instrumentation and observability
Next.js instrumentation hooks into the server startup lifecycle. The register() function runs once at boot, guarded to only execute in the Node.js runtime (not Edge). Create the instrumentation file and its observability module.
ts
// src/instrumentation.tsexport async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { const { initObservability } = await import("./observability.js"); initObservability(); }}
ts
// src/observability.tsimport { setLogger, ConsoleLogger } from "@reaatech/guardrail-chain-observability";export function initObservability() { setLogger(new ConsoleLogger());}
The scaffold already generated next.config.ts. It starts empty — to enable instrumentation, add the experimental.instrumentationHook flag:
ts
// next.config.tsimport type { NextConfig } from "next";const nextConfig: NextConfig = {};export default nextConfig;
Expected output:src/instrumentation.ts exports register(), guarded to Node.js runtime only. src/observability.ts initializes the console logger via @reaatech/guardrail-chain-observability. The next.config.ts is a minimal TypeScript Next.js config with no experimental flags enabled. To activate instrumentation later, add experimental: { instrumentationHook: true } inside nextConfig.
Step 11: Create the API routes
The API exposes six endpoints. The main POST /api/redact endpoint runs the full ingress-plus-egress pipeline. Sub-routes let you run individual phases, view config, query the audit log, and check health.
// app/api/status/route.tsimport { NextResponse } from "next/server";export function GET() { return NextResponse.json({ status: "ok" });}
Expected output: Six route handler files under app/api/. Five use NextRequest/NextResponse and follow the App Router convention of exporting named functions for each HTTP verb.
Step 12: Create the home page and run the tests
Replace the scaffolded placeholder page with a minimal landing page that documents the available API endpoints.
tsx
// app/page.tsximport type { NextPage } from "next";const Home: NextPage = () => { return ( <main style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}> <h1>PII Redaction Service</h1> <p>Configurable PII redaction at ingress and egress for HIPAA, GDPR, and PCI compliance.</p> <h2>API Endpoints</h2> <ul> <li><code>POST /api/redact</code> — Redact PII from text (ingress + egress)</li> <li><code>POST /api/redact/ingress</code> — Redact PII at ingress only</li> <li><code>POST /api/redact/egress</code> — Scan/redact PII at egress only</li> <li><code>GET /api/redact/config</code> — View current configuration</li> <li><code>GET /api/redact/audit</code> — Query audit log</li> <li><code>GET /api/status</code> — Health check</li> </ul> </main> );};export default Home;
Now run the type checker and the full test suite:
terminal
pnpm typecheckpnpm test
Expected output:pnpm typecheck exits 0 with no errors. pnpm test runs all tests with vitest, reports zero failures, and shows coverage thresholds at 90% or above on src/ and app/**/route.ts files.
Next steps
Add a CachedGuardrail wrapper around the PIIRedaction guardrail to skip re-processing identical inputs within the TTL window, reducing latency on repeated queries.
Enable tamper-evident audit chains by wrapping the audit logger with TamperEvidentLogger from @reaatech/mcp-gateway-audit — each event gets a SHA-256 hash of the previous event, creating an integrity chain that detects tampering.
Add a configuration file loader to loadAppConfig() — the @reaatech/guardrail-chain-config package supports loading from JSON and YAML files, providing a richer config surface beyond individual environment variables.