# Block secrets in an Express app

URL: https://tailrace.dev/docs/guides/block-secrets-in-express

> 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 [#prerequisites]

* [Quickstart](/docs/get-started/quickstart) or equivalent [`createTailrace`](/docs/get-started/quickstart) setup
* Express `>=4`

## Step 1: Install [#step-1-install]

```bash
pnpm add @tailrace/core @tailrace/express express
```

## Step 2: Mount the middleware [#step-2-mount-the-middleware]

```ts
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 [#how-it-works]

| Location                | Tailrace boundary                                   | Direction            |
| ----------------------- | --------------------------------------------------- | -------------------- |
| Chat request messages   | `{ kind: "model", provider }` (`model` field as-is) | Outbound to upstream |
| JSON chat completion    | same model boundary                                 | Inbound              |
| SSE `text/event-stream` | same; carry-buffer across chunks                    | Inbound              |

When policy resolves to `block`, the middleware returns **422** with
`{ error: { type: "policy_violation", entity, rule } }` - never the raw value. That maps from
[`PolicyViolationError`](/docs/reference/errors/POLICY_VIOLATION).

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`](/docs/reference/http).

## Common variations [#common-variations]

### Scope agent and workflow ID [#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](/docs/concepts/tokenization-and-the-vault).

### Verify with a synthetic key [#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 [#audit-decisions]

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

## See also [#see-also]

* [Express integration](/docs/integrations/express)
* [Express reference](/docs/reference/express)
* [Hono](/docs/integrations/hono) - same openai-compat contract on a different host
* [Boundaries](/docs/concepts/boundaries)
* Spec: [`docs/integrations.md`](https://github.com/TailraceHQ/tailrace/blob/main/docs/integrations.md) §10
