> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withboundary.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security & Data Handling

> Local-first, inspectable, opt-in — what Boundary sees and what it doesn't

Boundary is built so you can deploy it in regulated, customer-data-handling environments without rewriting your trust boundaries.

<Card>
  Boundary wraps your code, not your network. Your LLM provider relationship is unchanged.
</Card>

## Two products, two trust models

Boundary ships as two separate npm packages with different network behavior. You choose how much of the stack you adopt.

| Package                  | What it does                                                                       | Network behavior                                                                                                              |
| ------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `@withboundary/contract` | Validates, retries, and repairs structured LLM output using your schema and rules. | **None.** Pure local validation. No fetch, no telemetry, no analytics. The only runtime peer is `zod`.                        |
| `@withboundary/sdk`      | Optional observability — emits structured events about each contract run.          | **Opt-in.** Only sends data if you construct a logger with an `apiKey` or a custom `write` sink. Ships nothing automatically. |

If you only install `@withboundary/contract`, no data leaves your process.

## Data flow

```mermaid theme={null}
flowchart LR
    app[Your app] -- prompt --> llm[LLM provider]
    llm -- raw output --> app
    app -- validate / repair --> contract[@withboundary/contract]
    contract -- result --> app
    app -. event .-> sdk[@withboundary/sdk]
    sdk -. only if apiKey or write set .-> sink[Boundary cloud or your sink]
```

The dashed path only exists when you construct a logger. The solid path runs entirely in-process.

## What Boundary cloud can see

When you wire the SDK with an `apiKey`, every contract run produces a `BoundaryLogEvent`. Three optional buckets control whether sensitive content rides along — defaults are conservative:

| Bucket    | Contains                                              | Default |
| --------- | ----------------------------------------------------- | ------- |
| `repairs` | Repair messages generated to nudge the model on retry | **on**  |
| `inputs`  | The raw prompt you sent to the model                  | **off** |
| `outputs` | The raw model response                                | **off** |

Everything else is structural and always sent: contract name, environment, run id, attempt count, duration, `ok` flag, model name, the rule names that failed, the failure category, and SDK attribution. Without those, Boundary can't show you the run at all.

Raw prompts and completions are the most sensitive payload in an LLM pipeline — they stay off until you explicitly opt in. See [Capture policy](/sdk/capture-policy) for the exact field-to-bucket mapping.

### Why you might still want them on

Turning `inputs` or `outputs` on is a debugging convenience for your team, not a feature Boundary needs. When a contract fails, the dashboard's failure detail page can show you the exact prompt that was sent and the exact response the model returned — usually the fastest way to figure out *why* a rule rejected an output. Without these buckets you still get the failure category, which rule failed, and the repair messages — enough to track acceptance rates and spot patterns, just not enough to read the original payload.

Boundary does not analyze, train on, or repurpose captured payloads. They are stored so you can see them in the dashboard. If your data-handling policy precludes sending raw prompts, leave both off and rely on the always-on metadata + rule failures.

```typescript theme={null}
import { createBoundaryLogger } from "@withboundary/sdk";

const logger = createBoundaryLogger({
  apiKey: process.env.BOUNDARY_API_KEY,
  environment: "production",
  capture: {
    inputs: false,   // default
    outputs: false,  // default
    repairs: true,   // default
  },
});
```

## Three transport modes

You decide where events go — or whether they go anywhere at all.

| Mode                 | How                                                                                                          | What leaves your process                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| **No logger**        | Don't construct a logger, or construct one without `apiKey` and without `write`. The factory returns `null`. | Nothing. The contract still runs; it just has no observability hook. Safe default for local dev. |
| **Custom transport** | Pass a `write(events)` function.                                                                             | Only what you forward. Events go to your Datadog / Honeycomb / collector. No Boundary egress.    |
| **Boundary cloud**   | Pass an `apiKey`.                                                                                            | Events POST to `https://api.withboundary.com/v1/ingest`.                                         |

```typescript theme={null}
// Custom sink — events stay inside your infrastructure
createBoundaryLogger({
  environment: "production",
  write: (events) => myCollector.emit(events),
});
```

When the Boundary cloud transport is active, the API key travels in the `Authorization: Bearer …` header. It is never placed in URLs, query strings, or request bodies. The transport retries with exponential backoff and a circuit breaker so a temporary outage doesn't turn into a retry storm.

## Redaction

Even when you opt into capturing inputs or outputs, you can scrub data before it leaves the process. Three composable layers:

```typescript theme={null}
createBoundaryLogger({
  redact: {
    fields:   ["ssn", "email", "apiKey"],     // 1. exact key names, deep
    patterns: [/\b\d{3}-\d{2}-\d{4}\b/],      // 2. regex over string leaves
    custom:   (value, path) => transform(value, path),  // 3. last-chance per leaf
  },
});
```

Order of operations:

```mermaid theme={null}
flowchart LR
    event[Event] --> capture[Capture policy]
    capture --> redact[Redact]
    redact --> beforeSend[beforeSend]
    beforeSend --> transport[Transport]
```

Capture runs first — anything it strips can't be resurrected later. Redaction walks every leaf of the event, so PII in nested objects, repair messages, and any `input` / `output` you've opted into is covered. The walker is cycle-safe.

See [Redaction](/sdk/redaction) for path semantics, dropping leaves with `undefined`, and full examples.

## API key handling

* **Source:** `options.apiKey` argument or the `BOUNDARY_API_KEY` environment variable.
* **Transport:** sent only as `Authorization: Bearer ${apiKey}` on requests to the ingest endpoint. Never in URL, query, or body.
* **Logging:** never written to console, never echoed in error messages.
* **Absent key:** if neither `apiKey` nor `write` is configured, `createBoundaryLogger` returns `null`. You can leave the call in place across environments without guarding it — local dev runs with no observability and no errors.

## No hidden behavior

Boundary does **not**:

|                                    |                                                                                                                                                                                                 |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Modify your prompts silently       | The repair instructions appended on retry are generated from your schema and rules — visible in `attempt.instructions`, deterministic, and emitted via the logger when `repairs` capture is on. |
| Call additional models or services | Only your `RunFn` makes LLM calls. `@withboundary/contract` never makes network requests.                                                                                                       |
| Persist state between runs         | Each `.accept()` call is independent. No global cache, no cross-run state.                                                                                                                      |
| Auto-enable telemetry              | You explicitly construct a logger and pass it. No default key, no default sink.                                                                                                                 |

Every attempt, every failure, every repair instruction is also available to your code via the `ContractResult`:

```typescript theme={null}
if (!result.ok) {
  for (const attempt of result.error.attempts) {
    console.log(attempt.raw);       // raw model output
    console.log(attempt.issues);    // validation failures
    console.log(attempt.category);  // failure category
  }
}
```

Nothing is hidden behind the SDK. Anything Boundary cloud sees, your code can also see locally.

## Control stays with you

You define:

* The **schema** — what structure the output must have.
* The **rules** — what "correct" means for your domain.
* The **retry policy** — how many attempts, what backoff.
* The **capture policy** — which buckets of data leave the process.
* The **redaction rules** — what is stripped before transmission.
* The **transport** — Boundary cloud, your sink, or no sink at all.

Boundary only enforces what you specify. Nothing more.

## See also

<CardGroup cols={2}>
  <Card title="Capture policy" icon="filter" href="/sdk/capture-policy">
    Field-to-bucket mapping and opt-in recipes
  </Card>

  <Card title="Redaction" icon="eye-slash" href="/sdk/redaction">
    Fields, patterns, and per-leaf scrubbing
  </Card>

  <Card title="beforeSend" icon="code" href="/sdk/before-send">
    Last-chance per-event transform or drop
  </Card>

  <Card title="BoundaryLogEvent" icon="file-code" href="/sdk/boundary-log-event">
    Full wire format reference
  </Card>
</CardGroup>
