Tailrace
Guides

Block secrets in an Express app

Mount OpenAI-compatible middleware so chat requests and SSE responses respect Tailrace policy before they reach the model.

Use @tailrace/express to enforce data policy on OpenAI-compatible chat routes - in-process, with no proxy.

Prerequisites

Step 1: Install

pnpm add @tailrace/core @tailrace/express express

Step 2: Mount the middleware

import { createTailrace } from "@tailrace/core";
import { tailraceExpress } from "@tailrace/express";
import express from "express";

const app = express();
const tailrace = createTailrace();

app.use(
  "/v1",
  express.json(),
  tailraceExpress(tailrace, {
    agent: (req) => String(req.headers["x-agent-id"] ?? "default"),
    workflowId: (req) => String(req.headers["x-workflow-id"] ?? "default"),
  }),
);

// Your upstream proxy / chat handler on /v1/...

express.json() (or equivalent) must run before Tailrace so the chat body is available as parsed JSON.

How it works

LocationTailrace boundaryDirection
Chat request messages{ kind: "model", provider } (model field as-is)Outbound to upstream
JSON chat completionsame model boundaryInbound
SSE text/event-streamsame; carry-buffer across chunksInbound

When policy resolves to block, the middleware returns 422 with { error: { type: "policy_violation", entity, rule } } - never the raw value. That maps from PolicyViolationError.

For SSE, Tailrace cancels the upstream stream, emits one error data: event, and closes (abort-only; no streamBlockBehavior in v0.1).

Only mode: "openai-compatible" is supported. Shared helpers live in @tailrace/http.

Common variations

Scope agent and workflow ID

Pass agent and workflowId as functions of Request (or a static string for workflowId) so policy and vault tokens match your multi-tenant layout. Defaults are "default" when omitted.

By default, workflowId scopes tokens per request. For multi-turn agents that need tokens to resolve consistently across turns, see Tokenization and the vault.

Verify with a synthetic key

Send a chat completion body whose messages include sk_test_…FAKE. Expect 422 and an empty upstream call. An example.com email in the same body should become a vault token in the forwarded payload when the default policy tokenizes PII.

Audit decisions

tailraceExpress(tailrace, {
  onDecision: (decisions) => {
    // hashes + entity classes only - never log raw values
  },
});

See also

On this page