The core library stays framework-agnostic. Integrations belong in small
companion packages that translate framework data into explicit
AuditLog.Context values.
Phoenix scope adapter
Build audit_log_phoenix as a separate package after the core API
stabilises.
Phoenix scopes are a good boundary for audit context because applications
already pass them explicitly to context functions. The adapter should automate
context construction at that boundary. It must not hide context in the process
dictionary, make record!/5 inspect the current connection, or infer business
actions from routes and HTTP methods.
Scope mapping
Phoenix scopes are application-defined and have no universal actor shape. The adapter should require an application mapper:
defmodule MyApp.AuditScope do
@behaviour AuditLogPhoenix.ScopeMapper
alias AuditLog.Actor
alias MyApp.Accounts.Scope
@impl true
def actor(%Scope{user: user}) do
Actor.new!("user", user.id, label: user.email)
end
@impl true
def metadata(%Scope{organization: organization}) do
%{organization_id: to_string(organization.id)}
end
endThe application must decide the actor type, stable ID, snapshot label, and tenant metadata. The adapter must not guess them from arbitrary structs.
Controller requests
A Plug should build one context per HTTP request and store it under a
library-prefixed conn.private key:
plug Plug.RequestId, assign_as: :request_id
plug AuditLogPhoenix.Plug,
scope_assign: :current_scope,
mapper: MyApp.AuditScope,
router: MyAppWeb.RouterControllers and application contexts would retrieve and pass it explicitly:
context = AuditLogPhoenix.context!(conn)
Accounts.correct_address(context, shareholder, attrs)The Plug automates collection, not event recording. The domain operation still chooses the action, subject, reason, and transaction boundary.
Safe request metadata
Collect a small allowlist by default:
- request method;
- matched route pattern rather than the raw path;
- controller and action;
- the validated Plug request ID; and
- metadata returned by the application scope mapper.
Do not collect parameters, request bodies, query strings, cookies, sessions, arbitrary headers, authorization values, or full URLs. IP addresses and user agents should be explicit opt-ins because they may be sensitive, misleading, or unbounded.
The Plug request ID belongs in metadata. It is not necessarily a UUID and therefore cannot automatically become the audit correlation ID. Generate one correlation UUID per request unless the application supplies a separately validated UUID.
LiveView
A LiveView mount is not one business operation. Reusing its context for the socket's lifetime would group unrelated actions under one correlation ID.
The first LiveView API should construct one context for each state-changing client event:
context =
AuditLogPhoenix.context!(socket,
event: "save"
)The context should use the actor from socket.assigns.current_scope, generate a
new correlation UUID, and record the LiveView module and client event in
metadata. Hook-based assignment can follow only if tests prove that context
never leaks between events.
Asynchronous follow-up work
Jobs must continue to use Context.child/4 with the service actor that performs
the work. They should preserve the originating correlation ID and record the
causing event ID. Attributing later service work to the initiating web user
would be false.
Delivery order
- Define and test the
ScopeMapperbehaviour. - Add Plug context construction and safe request metadata.
- Add controller integration tests for missing scopes and malformed IDs.
- Add per-event LiveView context construction.
- Test request, socket, task, and connection isolation.
- Publish the adapter independently from the core package.
Automatic event generation, action inference, parameter capture, background-job framework integrations, and ambient context remain out of scope.