Threadline exposes a small set of concrete integration seams. This guide is the canonical breadth contract for those seams.
The contract is intentionally code-shaped:
Threadline.Plugowns request-path capture context.Threadline.Jobowns serialized job-path context.Threadline.Integrations.*owns soft-loaded reference adapters.threadline_operator_surface/2owns the operator-surface mount boundary, withauthorize_fnand optionalexport_authorize_fncovering the LiveView and HTTP export faces.
Threadline does not introduce a separate adapter behaviour or umbrella protocol here. These are the existing supported seams.
Adoption lanes and integration seams
Threadline adoption maps to four lane IDs in canonical order. Each lane names which seams from this guide you need:
capture-only—Threadline.Plugand core APIs only; stop after the request-path section below.phoenix-surface— optional Phoenix deps plusthreadline_operator_surface/2; the operator-surface mount is required.phx-gen-auth-reference— generated session auth (mix phx.gen.auth) wired intoThreadline.Plug; proof path inguides/integrations/phx-gen-auth.md.sigra-reference—Threadline.Integrations.Sigracomposed intoThreadline.Plug; proof path in the example app andguides/integrations/sigra.md.
For claim types, proof commands, version lifecycle, and the full matrix, see
guides/upgrade-path.md — do not duplicate that table
here.
Request path via Threadline.Plug
Threadline.Plug attaches %Threadline.Semantics.AuditContext{} to
conn.assigns[:audit_context].
plug Threadline.Plug,
actor_fn: &MyApp.Audit.actor_ref_from_conn/1,
context_overrides_fn: &MyApp.Audit.audit_context_overrides/1The request-path contract is:
actor_fnis the only actor-authority callback. It decidesaudit_context.actor_refand may return anActorRefornil.context_overrides_fnis additive-only. It may return a map containing only:request_idand:correlation_id.Threadline.Plugextractsrequest_id,correlation_id, andremote_ipfirst, then fills only missingrequest_id/correlation_idfields fromcontext_overrides_fn.- Unknown override keys and non-map returns fail closed with
ArgumentError. - Proxy-aware IP normalization stays host-owned. Normalize
conn.remote_ipbeforeThreadline.Plugruns if your deployment needs it.
This seam does not provide a second actor channel. Additive metadata cannot
replace actor identity or remote_ip.
Capture-only adopters on the capture-only lane can stop here.
Threadline.Plug plus the core APIs are the strongest supported lane, and
mix verify.compile_no_optional proves that surface without optional Phoenix UI dependencies.
Job path via Threadline.Job
Threadline.Job keeps background-job propagation explicit and serializable. It
is not a callback mini-framework and does not couple Threadline to any
particular job runner.
args = %{
"actor_ref" => Threadline.Semantics.ActorRef.to_map(actor_ref),
"correlation_id" => correlation_id,
"job_id" => job.id
}
with {:ok, actor_ref} <- Threadline.Job.actor_ref_from_args(args) do
opts = Threadline.Job.context_opts(args)
Threadline.record_action(:member_synced, [actor: actor_ref, repo: Repo] ++ opts)
endThe job-path contract is:
"actor_ref"storesThreadline.Semantics.ActorRef.to_map/1output.Threadline.Job.actor_ref_from_args/1reads that serialized map back into anActorRef.Threadline.Job.context_opts/2extracts stable context keys from the args map, currently"correlation_id"and"job_id".- Any broader worker-framework integration belongs in an adapter module if the pattern repeats; it is not standardized in core today.
Audited write path via Threadline.Audit
Threadline.Audit.transaction/3 is the recommended helper for domain writes that need
capture and optional semantic action linkage in one database transaction.
Threadline.Plug and Threadline.Job remain edge context producers — call the helper
from Phoenix context modules (or workers) after resolving %Threadline.Semantics.AuditContext{}
and %Threadline.Semantics.ActorRef{}.
Threadline.Audit.transaction(
Repo,
[
audit_context: audit_context,
action: :post_created_via_api,
transaction_meta: %{"organization_id" => org_id}
],
fn ->
Repo.insert!(Post.changeset(%Post{}, attrs))
end
)Contract bullets:
:actionpresent → correlation-ready — the helper callsThreadline.record_action/2and linksaudit_transactions.action_idso strict:correlation_idfilters onThreadline.timeline/2match.:actionabsent → capture-only — row capture andactor_refonaudit_transactionsstill work; strict:correlation_idtimeline/export filters will not match (document this at code review).actor_refrequired unlessallow_missing_actor: trueon capture-only paths (non-recommended for multi-tenant SaaS).- Callback must not call
set_configforthreadline.actor_ref,Threadline.record_action/2, or nestedRepo.transaction/1— the helper owns GUC, action recording, and linkage.
Reference integrations via Threadline.Integrations.* (sigra-reference lane)
Threadline.Integrations.* modules are reference adapters for the
sigra-reference lane. They translate host or framework state into the
existing Threadline-native seams above.
Threadline.Integrations.Sigra is the current model:
- soft dependency gating stays inside the integration module via
Code.ensure_loaded? - absent host dependencies return neutral defaults rather than forcing hard coupling into core
- helpers stay direct and composable, such as
actor_ref_from_conn/1,audit_context_overrides_from_conn/1, andactor_fn/0
plug Threadline.Plug,
actor_fn: Threadline.Integrations.Sigra.actor_fn(),
context_overrides_fn: &Threadline.Integrations.Sigra.audit_context_overrides_from_conn/1These modules are reference adapters, not framework ownership claims. A
Threadline.Integrations.* module should adapt host state into Threadline.Plug
or Threadline.Job; it should not redefine those contracts.
Operator-surface composition via authorize_fn and export_authorize_fn
The operator surface is required for the phoenix-surface lane and for
reference lanes that mount threadline_operator_surface/2. It is one breadth
contract with two transport faces:
- LiveView mount/auth via
authorize_fn - scoped investigation queries via optional
scope_query_fn - mounted evidence access via optional
evidence_authorize_fn - HTTP export auth via optional
export_authorize_fn
The host still owns authentication and authorization semantics. Threadline standardizes where those hooks plug in; it does not define who the user is, which roles exist, or how tenancy is modeled.
That same boundary applies to the evidence plane. Evidence writes are
host-owned via Threadline.Evidence record_*; Threadline does not
auto-populate attestations from retention, health, or export paths. The
evidence plane captures governance and support-scope posture the host chooses
to record, but it does not introduce a Threadline-owned RBAC system, tenancy
DSL, approval workflow, legal-hold flow, or vendor-reporting suite.
When a host returns {:ok, scope} from authorize_fn, keep that scope
host-owned and pair it with scope_query_fn if you want mounted reads to narrow
by tenant, organization, or another local concept. scope_query_fn is the
query seam for timeline, actor, transaction, export, or any future surface your
host explicitly proves. Threadline carries the scope through; it does not
invent a policy DSL around it.
Apply that same host-owned rule to evidence_authorize_fn: it gates the mounted
evidence capability, but it does not define a Threadline role model, tenant
policy, or blanket permission vocabulary.
Secure-by-default mount boundary
threadline_operator_surface/2 is the supported mount boundary. It requires one
of these conditions at compile time:
- mount inside a router scope that already has
pipe_through - provide
:authorize_fn - explicitly acknowledge unauthenticated mounting
Anything outside that boundary is outside the supported surface story.
When that mount also receives actor_fn, the standard route path
auto-installs Threadline.OperatorSurface.SessionPlug before the LiveView
routes. That keeps actor-owned saved views and similar UI features on the same
ActorRef contract as request-path capture without extra adopter wiring.
Shared authorization vocabulary
authorize_fn is the canonical operator-surface callback. It is invoked
directly as a 1-arity function. The recommended callback shape is one shared
host-owned function that accepts %{assigns: assigns} so it can authorize both
the LiveView socket and the export fallback mirror without transport-specific
function heads.
threadline_operator_surface "/audit",
repo: MyApp.Repo,
authorize_fn: &MyApp.Audit.authorize_operator/1,
scope_query_fn: &MyApp.Audit.scope_operator_query/3def authorize_operator(%{assigns: assigns}) do
case assigns[:current_user] do
%{role: :admin} ->
:ok
%{role: :support, organization_id: org_id} ->
{:ok, %{access: :support_read_only, organization_id: org_id}}
_ ->
{:error, :unauthorized}
end
endThreadline.OperatorSurface.Auth treats these results as the public contract:
:okortruegrants access{:ok, scope}grants access and storesscopein:threadline_scope- any other result denies access
- raised errors deny access
That scope is opaque and host-owned. Threadline carries it as data; it does
not define a role enum, permissions DSL, tenancy DSL, or page-level
authorization language around it.
If both session actor data and a scope fallback actor are present, session actor wins. Scope fallback stays compatibility-only, and mismatches emit observable telemetry rather than silently replacing the session-owned actor identity. In other words: session actor wins, scope fallback does not silently override it.
export_authorize_fn is optional and should stay an advanced override. When
present, it is called with conn directly for export requests:
threadline_operator_surface "/audit",
repo: MyApp.Repo,
authorize_fn: &MyApp.Audit.authorize_operator/1,
export_authorize_fn: &MyApp.Audit.authorize_operator_export/1When export_authorize_fn is absent, export auth deliberately falls back to
authorize_fn through a synthetic mirror:
mirror = %{assigns: conn.assigns}
authorize_fn.(mirror)That fallback is part of the public contract. If you want one host-owned
authorization function to cover both transport faces, write it against
%{assigns: assigns} rather than LiveView-only helpers. If you need a stricter
export posture than the mounted surface, provide export_authorize_fn as a
deliberate override rather than teaching two primary authorization vocabularies.
Both transport faces share the same telemetry event
[:threadline, :operator_surface, :authorize], the same granted/denied/error
result vocabulary, and the same :threadline_scope assign semantics.
For background exports, keep one actor-owned Threadline download route keyed by
the export job ID. Local storage resolves to send_file behind that boundary;
adapter-backed storage resolves download_url/2 only after authorization. This
preserves one operator action while letting the host keep ownership of Oban
supervision and external storage infrastructure.
Canonical references
- Request path and additive override validation:
lib/threadline/plug.ex - Job-path serialized context helpers:
lib/threadline/job.ex - Soft-loaded reference adapter model:
lib/threadline/integrations/sigra.ex - Secure operator-surface mount boundary:
lib/threadline/operator_surface/router.ex - LiveView auth hook:
lib/threadline/operator_surface/auth.ex - HTTP export auth parity and fallback mirror:
lib/threadline/operator_surface/export_auth_plug.ex
Use this guide when you need the host/framework breadth contract. Use
guides/operator-surface.md for the screen-level mount walkthrough and
guides/integrations/sigra.md for the current first-party reference adapter.