CI: Runs on GitHub Actions on
main (verify-format, verify-credo, verify-test, plus verify-docs, verify-hex-package, verify-release-shape). Reproduce locally with mix ci.all — see CONTRIBUTING.md.
Threadline is an open-source audit platform for Elixir teams using Phoenix, Ecto, and PostgreSQL. It combines PostgreSQL trigger-backed row-change capture, rich action semantics (actor, intent, correlation), and operator-grade exploration via plain SQL queries — without opaque blobs or a separate event bus.
- Elixir ~> 1.15
- PostgreSQL (trigger support required)
- Ecto and Phoenix (recommended; Plug integration included)
Add threadline to your dependencies:
# mix.exs
def deps do
[
{:threadline, "~> 0.1"}
]
endRun the installer to generate the base migration:
mix threadline.install
mix ecto.migrateRegister audit triggers on the tables you want to audit:
mix threadline.gen.triggers --tables users,posts,comments
mix ecto.migrate# lib/my_app_web/router.ex
pipeline :browser do
plug Threadline.Plug, actor_fn: &MyApp.Auth.to_actor_ref/1
endYour actor_fn receives the Plug.Conn and returns a Threadline.Semantics.ActorRef:
defmodule MyApp.Auth do
alias Threadline.Semantics.ActorRef
def to_actor_ref(conn) do
case conn.assigns[:current_user] do
nil -> ActorRef.anonymous()
user -> ActorRef.user("user:#{user.id}")
end
end
endInside the same database transaction as your audited writes, set the
transaction-local GUC before the first row change. This tells the PostgreSQL
trigger which ActorRef produced the mutation:
alias Threadline.Semantics.ActorRef
actor_ref = ActorRef.user("user:123")
json = ActorRef.to_map(actor_ref) |> Jason.encode!()
MyApp.Repo.transaction(fn ->
MyApp.Repo.query!("SELECT set_config('threadline.actor_ref', $1::text, true)", [json])
MyApp.Repo.insert!(%MyApp.Post{title: "Ship audit logs"})
end)Record semantic intent separately with Threadline.record_action/2 (atom name,
:repo, and :actor_ref / :actor options) when you want an AuditAction row.
Every query helper requires an explicit :repo option.
alias Threadline.Semantics.ActorRef
Threadline.history(MyApp.Post, post.id, repo: MyApp.Repo)
actor_ref = ActorRef.user("user:123")
Threadline.actor_history(actor_ref, repo: MyApp.Repo)
Threadline.timeline([table: "posts"], repo: MyApp.Repo)Threadline is safe under PgBouncer transaction-mode pooling. The Threadline.Plug stores request metadata in conn.assigns only — it never issues SET or SET LOCAL on the database connection outside of a transaction.
Actor information is propagated to the PostgreSQL trigger using set_config('threadline.actor_ref', $1::text, true), where the third argument true makes the GUC transaction-local: it is automatically cleared when the transaction ends and never leaks to a different connection or pooled session.
For the full SQL bridge pattern and PgBouncer safety explanation, see Threadline.Plug.
- Full API reference: hexdocs.pm/threadline
- Domain model: guides/domain-reference.md
- Contributing: CONTRIBUTING.md