Skip to main content

guides/how-threadline-works.md

# How Threadline works

This is the crash-course guide. Read it when you want the mental model first and the API details second. For the exact seam contracts, keep using the [integration contracts](integration-contracts.md), [operator surface](operator-surface.md), [domain reference](domain-reference.md), and [Phoenix SaaS getting-started guide](getting-started-saas.md).

## The short version

Threadline is embedded audit infrastructure for Phoenix, Ecto, and PostgreSQL apps.

The memorable formula is:

`DB truth` + `app intent` + `operator tooling`

- `DB truth` = trigger-captured `AuditTransaction` + `AuditChange`
- `app intent` = semantic `AuditAction` records — normally via `Threadline.Audit.transaction/3`
  with `:action` (implemented by `Threadline.record_action/2` inside the helper)
- `operator tooling` = timelines, actor windows, incident bundles, exports, and the optional `/audit` surface

Threadline is:

- a library you embed into your app
- a read-heavy investigation surface with a small write-side semantic helper
- host-owned on auth, tenancy, and roles

Threadline is not:

- event sourcing
- a remote SaaS
- an auth framework
- a write-capable admin backend

The evidence plane stays just as narrow. Host apps write **host-written**
attestations deliberately via `Threadline.Evidence` `record_*` for closed
governance subjects such as redaction posture, trigger coverage, retention
runs, export delivery, and support-scope posture. See
[Evidence write boundary (host-written)](domain-reference.md#evidence-write-boundary-host-written)
for read surfaces, the six `record_*` entrypoints, and what Threadline does
not auto-populate.

Canonical public non-goals for the evidence plane:

- legal hold workflows
- immutable-storage guarantees beyond the host runtime/storage contract
- generic compliance packs
- vendor-specific reporting suites
- a Threadline-owned RBAC platform
- a Threadline-owned tenancy DSL
- approval workflows

## The flow

The common loop is:

1. A request enters the host app.
2. `Threadline.Plug` attaches request-scoped audit context and the host decides the actor.
3. Domain writes run inside `Threadline.Audit.transaction/3` — the **recommended audited write path** — so capture and optional semantics share one database transaction.
4. PostgreSQL triggers capture the physical row changes from that transaction.
5. When you pass `:action`, the helper records semantic intent via `Threadline.record_action/2` and links `audit_transactions.action_id` for correlation filters.
6. Operators inspect the result through the query APIs, Mix tasks, or the mounted `/audit` surface.

Request headers can populate audit context at the edge, but queryable correlation requires an audit_actions row linked in the same transaction — use Audit.transaction/3 with :action when filters must match intent. Omit `:action` or pass `capture_only: true` for capture-only writes.

That makes Threadline useful in both of these shapes:

```elixir
plug Threadline.Plug, actor_fn: &MyApp.Audit.current_actor/1

{:ok, %{post: post}} =
  Threadline.Audit.transaction(
    MyApp.Repo,
    [audit_context: audit_context, action: :post_created],
    fn ->
      case MyApp.Repo.insert(Post.changeset(%Post{}, attrs)) do
        {:ok, post} -> %{post: post}
        {:error, changeset} -> MyApp.Repo.rollback(changeset)
      end
    end
  )
```

## Architecture layers

### 1. Capture substrate

PostgreSQL triggers write the durable audit rows. The core facts are `AuditTransaction` and `AuditChange`; the audit tables are the source of truth for row-level history.

This layer answers:

- What actually changed?
- In which transaction?
- At what time?

It does not answer ownership or policy questions on its own.

### 2. Host-owned seams

Threadline intentionally keeps auth and tenancy on the host side.

- `Threadline.Plug` owns request-path capture context.
- `Threadline.Job` owns serialized job-path context.
- `Threadline.Integrations.*` owns soft-loaded reference adapters.
- `threadline_operator_surface/2` owns the mount boundary for the optional operator UI.

The host decides who the actor is, which request context matters, and whether support access is admin-only or read-only.

### 3. Investigation layer

These are the read APIs most adopters use first:

- `Threadline.history/3` for a row's history
- `Threadline.timeline/2` for an eager slice
- `Threadline.timeline_page/2` for larger windows
- `Threadline.incident_bundle/2` for a single-transaction drill-down
- `Threadline.as_of/4` for point-in-time reconstruction
- `Threadline.export_json/2` for a machine-friendly export

Rule of thumb: use the eager helpers when the window is small enough to read in one shot, and the paged helper when it is not.

### 4. Operator surface

The `/audit` surface is optional and lives in-tree for now. It gives you a host-mounted UI for the same investigation questions as the library APIs and Mix tasks.

That means the library and the UI answer the same questions:

- `timeline` and `/audit`
- `incident_bundle/2` and `mix threadline.incident`
- `export_json/2` and `mix threadline.export`
- `trigger_coverage/1` and `mix threadline.health.coverage`
- policy drift checks and `mix threadline.policy.show`

## The SaaS Builder's JTBD Map

If you are dropping Threadline into your SaaS, you are hiring it to do four very specific jobs.

### Job 1: The "Silent Witness" (Compliance & Baseline)
* **The Scenario:** A SOC2 auditor wants proof of data lineage, or a customer is screaming, "I never deleted that invoice!" You need to know that no matter what happens, the truth is recorded.
* **The Flow:** You run `mix threadline.gen.triggers` to attach PostgreSQL triggers to your tables. You don't touch your Elixir contexts. Even if a junior dev opens an `iex` shell and runs `MyApp.Repo.delete_all()`, the triggers catch it.
* **The JTBD:** *"Give me an airtight, DB-level audit trail without forcing me to rewrite my application code to use special `audit_insert` functions."*

### Job 2: The "Who Did This?" (Attribution & Intent)
* **The Scenario:** A database trigger only knows that the `postgres` database user modified a row. That’s useless for a SaaS. You need to know that `user_id: 42` did it via the `/billing/refund` endpoint.
* **The Flow:** You drop `plug Threadline.Plug` into your router and configure it to pull the current user from the session. Wrap business writes in `Threadline.Audit.transaction/3` with `:action` when intent and correlation matter — the helper records semantic intent and links `audit_transactions.action_id` in the same database transaction as the row changes.
* **The JTBD:** *"Bridge the gap between physical database mutations and human application semantics so the logs actually make sense."*

### Job 3: The "3 AM Support Ticket" (Investigation & Ops UI)
* **The Scenario:** A customer writes into Zendesk: "My dashboard looks weird since yesterday." Your ops team needs to figure out what state changed without bugging an engineer to write custom SQL.
* **The Flow:** You mount the `/audit` LiveView dashboard in your host app. Support staff log in (using your app's existing auth). They filter the timeline by the customer's `actor_id` or the specific `record_id` and get a visual diff of exactly what fields changed, when, and by whom.
* **The JTBD:** *"Give my support and ops team a safe, read-only window into historical data state so they can unblock customers autonomously."*

### Job 4: The "Data Handoff" (Egress & Reporting)
* **The Scenario:** Legal needs a CSV of every permission change in the `Enterprise` workspace over the last 30 days.
* **The Flow:** Your ops team uses the filter form on the LiveView timeline, hits "Export", and downloads the results. Alternatively, you run `mix threadline.export` in your deployment console.
* **The JTBD:** *"Get the data out of the system in a standard, machine-readable format quickly and safely."*

The library exists to make those personas overlap cleanly instead of forcing each one to build a different audit story.

## What already landed around the core

The core capture + semantics + investigation loop is no longer standing alone.
Threadline already ships the governance and operator-lifecycle layer that used
to sit on the near-term roadmap:

- **Lifecycle & Pruning:** retention admin with visible purge history and safe batched cleanup.
- **Async Export Delivery:** queued or scheduled exports with status visibility, expiry cleanup, and backend-aware delivery seams.
- **Operator Ergonomics:** saved views for repeated investigations without inventing a Threadline-owned auth model.

Those capabilities matter because they keep the investigation surface usable
once an adopter moves beyond a single incident and starts operating Threadline
as recurring support infrastructure.

## The Line of Diminishing Returns

A great library knows what it *isn't*. Threadline hits the point of diminishing returns—and starts turning into bloated software—if we cross these lines:

1. **Becoming a SIEM:** We are embedded infrastructure. We provide the facts. We will not build anomaly detection ML, chart builders, or pie-graph dashboards.
2. **Owning Auth/RBAC:** Threadline relies on the host app to say "this user is an admin." We will not build user tables or role-permission matrices.
3. **UI-Based Policy Mutation:** Security rules (like "don't log the `passwords` table" or "redact the `ssn` column") must live in code/config. We will not build a UI toggle to turn off logging, as that creates a vector for a rogue admin to disable logging, do something bad, and turn it back on.

## Public API surface

If you only remember one thing, remember this grouping:

### Write-side

- `Threadline.Audit.transaction/3`**recommended audited write path** (capture + optional semantics in one transaction)
- `Threadline.Plug` for request capture context
- `Threadline.Job` for serialized job-path context
- `Threadline.record_action/2` — semantic primitive; prefer `Audit.transaction/3` for new code

Manual `set_config` + `record_action/2` linkage recipes are deprecated — see [Integration contracts](integration-contracts.md) § Audited write path via `Threadline.Audit`.

### Read-side

- `Threadline.history/3`
- `Threadline.timeline/2`
- `Threadline.timeline_page/2`
- `Threadline.incident_bundle/2`
- `Threadline.as_of/4`
- `Threadline.export_json/2`

### Operator parity

- `mix threadline.incident`
- `mix threadline.export`
- `mix threadline.health.coverage`
- `mix threadline.policy.show`
- `threadline_operator_surface/2`

The read-side APIs are the stable core. The operator surface and Mix tasks are convenience layers on top of the same investigation model.

## Evolution so far

- `0.1.x` established the capture substrate and semantics layer.
- `0.2.x` hardened the query, continuity, and retention story.
- `0.3.x` brought the first serious host-integration seams and example-app path.
- `0.4.x` added the optional operator surface and its first investigation screens.
- `0.5.0` tightened the breadth story: honest support lanes, shared host-owned auth seams, and a clearer optional-in-tree position for the UI.
- `0.6.0` packaged the Evidence plane and `Threadline.Audit.transaction/3` as the recommended audited write path.

That evolution matters because the library did not start as a product-console project. It became one as the investigation path matured.

## Natural next work

The next chunks that feel naturally adjacent are:

- broader first-party host adapters beyond the current Sigra reference path
- deeper proof around how different hosts expose the support lane without widening Threadline into its own auth product
- extraction pressure checks for whether a separate UI package is ever worth the maintenance cost
- a `threadline_web` split only if objective extraction triggers show up

Those are the kinds of problems that usually belong in future releases once adopters start using the current surface for real.

## Where to go next

Adoption discovery order:

1. [README](../README.md) — top-level map and version quick start
2. This guide — mental model, formula, and flow
3. [Getting started with Phoenix SaaS](getting-started-saas.md) §6 — canonical runnable `Audit.transaction/3` snippet
4. [Domain reference](domain-reference.md) — vocabulary and API routing
5. [Integration contracts](integration-contracts.md) — host seams and audited write path contracts
6. [Operator surface](operator-surface.md) — mount, auth, and screens
7. [Support lanes and upgrade path](upgrade-path.md) — support matrix