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

# The policy-scan pipeline model

> How the pre-send content scanner attaches to the send path as a pre-handler, what the pass/warn/block verdict means, how the strict-warn-off mode matrix maps to the transport, and why the X-Policy-Violations header contract carries a size guard.

# The policy-scan pipeline model

The [pre-send policy scanner](/compliance/policy-scanner) page gives you the
checklist view: which rules fire, how to read the violation response, how to
flip your mode. This page is the plumbing model behind it — where the scan
sits in the request lifecycle, how the verdict rides the transport, and where
the findings land after the verdict, so you can reason about the pipeline when
you integrate against it.

<Note>
  The scanner is a **tenant-owned control**. Your organization picks the mode;
  Orbit does not gate outbound globally on it and never forces a mode on you.
  This page documents the plumbing of a control you own — it is not legal
  advice.
</Note>

***

## 1. Where the scan lives

Every scan happens in a single Fastify `preHandler` — one hook that runs
*before* the route handler on any send route opted into scanning. A send route
opts in by setting `policy: true` (or `policy: { channel: "sms" }` and
optionally a mode) in its route config, and the hook extracts the fields it
needs — channel, recipient, content body, optional sender name, scheduled time,
media URLs, recipient country, subject, and traffic-lane metadata — straight
off the request body.

Two consequences of that placement matter to you as an integrator:

* **A body with no recipient or no content skips the scan cleanly.** No
  recipient → the hook returns without scanning. A channel with no applicable
  rule → the scanner returns an immediate `pass`. The pipeline never throws on
  partial input.
* **The scan runs and decides before your handler touches the message.** An
  intercepted `POLICY_VIOLATION` rejection arrives before anything was stored
  or queued — you never have a stored-but-rejected artifact to reconcile.

Direct `enforcePolicy` calls made inside service code follow the identical
verdict → mode → enforcement chain; the hook is just the transport-attaching
wrapper around that chain.

***

## 2. The verdict vocabulary

Every matched rule collapses to one of three verdicts:

* `pass` — no rule fired. The send proceeds.
* `warn` — advisory rule(s) fired. The send proceeds; findings ride the
  transport as described below.
* `block` — a blocking rule fired (SHAFT content, spam score ≥ 80, DLP
  sensitive data, unregistered-sender country gate) or TCPA quiet-hours
  promoted to enforceable in strict mode.

The verdict does not change what was found; it changes whether the finding is
significant enough to gate a send. Whether a `block` verdict actually gates
the send is a mode question, which is the next section.

***

## 3. The mode matrix — verdict at the transport layer

Your organization's **policy scan mode** decides exactly what a verdict does.
The mode resolution order is: **explicit per-route override > org-level
setting > `warn` default** — a route that pins its mode beats your org
setting, and your org setting beats the fallback.

| Mode             | `pass` verdict        | `warn` verdict                    | `block` verdict                                                                                    |
| ---------------- | --------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------- |
| `off`            | Scan skipped entirely | Scan skipped entirely             | Scan skipped entirely                                                                              |
| `warn` (default) | Send proceeds         | Send + finding surfaced in header | Send + finding surfaced in header                                                                  |
| `strict`         | Send proceeds         | Send + finding surfaced in header | Reject `POLICY_VIOLATION`: 400 for content-class findings, 422 for TCPA quiet-hours in strict mode |

The three rows decode to a single rule: the mode is a **transport decision**
about what to do with the verdict — `off` short-circuits the scan itself,
`warn` maps every verdict to "send + surface the finding," and `strict` maps
a `block` verdict to a synchronous request rejection.

**Why the 400/422 asymmetry exists.** Content-class blocks (SHAFT, spam
score, DLP, sender gate) reject with 400 because the rendered client already
branches on `POLICY_VIOLATION`-400 to show an inline toast. A violation whose
severity is `enforce` — the TCPA quiet-hours promotion from warn in strict
mode — instead raises 422 so it lines up with the send-gates guard's own
`TCPA_QUIET_HOURS` 422. The hook derives this from the severity attached to
each violation, so you can tell from the status code whether you violated a
content rule or a timing rule without parsing the body.

**Fail-closed on lookup failure.** If the org-mode lookup throws and the hook
cannot resolve your mode, the send path fails closed (`503
POLICY_SCAN_MODE_LOOKUP_FAILED` / `POLICY_SCANNER_UNAVAILABLE`) rather than
sending unscanned. Retry; those are scanner-outage codes, not policy verdicts
and they never imply one.

***

## 4. The header contract and its size guard

Every violation the scanner produced — in every mode, block or pass — is
appended to the response's `X-Policy-Violations` header as a **JSON array of
finding objects**, one per rule, each carrying `rule`, `severity` (`enforce` /
`block` / `warn`), a message, and a suggestion. This is what the dashboard
compose surface uses to show warnings when nothing blocked.

Three transport realities govern the header contract:

* **HTTP headers are ASCII-only (RFC 7230); the scanner's messages are not.**
  Em-dashes, curly quotes, and non-ASCII category labels would crash the
  server with `ERR_INVALID_CHAR`. Before transport, every non-ASCII byte is
  converted to a `\uXXXX` escape inside the JSON string, so the header rides
  the wire as pure ASCII and your client still `JSON.parse()`s it back to the
  original text.
* **The header has a bounded budget: 6,144 bytes.** Fastify's default
  \~8 KiB header limit means an oversized findings array must not drown the
  response. When the full payload fits within the budget it ships whole; when
  it exceeds the budget, the header keeps the first three violations and
  appends a synthetic marker object with `rule: "_truncated"` telling you how
  many more were dropped. On overflow, look at logs or the message metadata
  for the full list — the header tells you it happened rather than silently
  serving a partial list with no signal.
* **`JSON.parse()` the header, don't substring it.** The escaped form is the
  transport form, not the finding itself — parse, then read each object's
  `rule` / `severity` fields.

***

## 5. The audit trail — findings in metadata, blocks in the audit log

The transport header is how you see a finding **live**; the audit trail is
how you see it **after**:

* **Stored-on-the-message metadata.** Findings are stamped onto the message's
  own metadata (the message record carries the violation list), so a
  dashboard that renders a message, or a downstream webhook consumer that
  receives that message record, can re-render its policy findings without
  touching a separate lookup surface.
* **Immutable block events.** Every strict-mode block writes an audit event —
  `policy.message_blocked` — to the tamper-evident audit chain with the
  channel and the violation rules. This is the irreplaceable leg of the
  trail: a hook-level `info` log that lives in log-aggregator retention is
  fine for operators, but a content-moderation block needs a record that
  outlives the window, so the hook emits it before it rejects the send.

Consume them accordingly: dashboards and webhooks read message metadata;
compliance-review workflows read the audit log; live client surfaces read the
headers.

***

## 6. The rule catalog

Each rule in the catalog applies to specific channels, and channels with no
applicable rule return a clean `pass`. The [policy scanner
checklist](/compliance/policy-scanner) enumerates them; at the pipeline level
the catalog covers:

* **TCPA quiet hours** — marketing sends outside recipient-local 08:00–21:00.
  Transactional traffic is exempt via the traffic-lane hint the hook
  resolves; in strict mode this rule's severity is promoted from `warn` to
  `enforce`.
* **SHAFT content** — sex, hate, alcohol, firearms, tobacco/cannabis
  keywords on US SMS.
* **Missing opt-out** — promotional content without an opt-out phrase.
* **Short URLs** — public shorteners carriers aggressively filter.
* **Spam-keyword score** — SpamAssassin-style 0–100 score; ≥ 80 blocks,
  60–79 warns.
* **GDPR sender identity** — email to EU recipients must name a From display
  name and a postal address.
* **Country registration & sender gate** — unregistered or mismatched sender
  on countries that require registration.
* **DLP — sensitive data** — card / PAN, US SSN, IBAN. Findings carry offsets
  and category only, never the matched text.

Because the catalog is channel-scoped per rule, the hook can invoke the full
scanner across all channels without every rule firing on every channel —
per-rule branch decisions are the scanner's own business, not the hook's.

***

## 7. Why the mode is tenant-owned

The mode lives on **your organization record** and is picked by an owner in
your workspace — every send resolves it before the verdict is enforced.
Orbit does not force a platform mode on you and does not gate outbound
globally; the mode control is tenant-owned by deliberate design, unlike the
one platform-global carve-out on the compliance map (the TCPA federal voice
guard — see [policy-scanner tenancy posture](/compliance/policy-scanner)).
When you change the mode, the change applies on the very next send.

***

## 8. Where this page is one of three views

| View                       | Page                                                                                  | What it answers                                                               |
| -------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Checklist                  | [Pre-Send Policy Scanner & DLP](/compliance/policy-scanner)                           | Which rules, how to read the violation response, how to flip the mode         |
| Pipeline model (this page) | —                                                                                     | Where the scan attaches, how verdicts ride the transport, where findings land |
| Compliance framing         | [CTIA Messaging Principles as an ordered schema](/concepts/ctia-messaging-principles) | Why the rule catalog exists and which pillar each rule maps to                |

Read it with [Tenant compliance defaults — the opt-in
envelope](/concepts/tenant-compliance-defaults-model) (the sibling
opt-in-envelope model for org-wide defaults) and [Send
gates](/compliance/send-gates) (the sibling gate chain on the same send
path).
