Audit Logs for Multi-Tenant Products
SqlOS now gives AuthServer and host applications one structured audit trail for governance, support, and customer review.
By Ross Slaney
Most applications eventually grow a second kind of log.
The first log is for engineers. It says a request timed out, a SQL query was slow, or a handler threw.
The second log is for operators, security teams, support, and customers. It answers:
Who did what, to which resource, in which tenant, from which application, and when?
That second log is an audit log. SqlOS now has it as a first-class product surface.
Audit logs are not debug logs
Debug logs are intentionally noisy. They carry stack traces, timings, low-level request details, and implementation context. They are useful when an engineer is fixing a bug.
Audit logs need different properties:
| Property | Why it matters |
|---|---|
| Structured | Operators need to filter by organization, actor, action, and resource. |
| Durable | Events may be needed for support, security review, or customer reporting. |
| Bounded | Rows should carry enough context, not full request dumps. |
| Safe | Operators should not see secrets, tokens, cookies, or raw bodies. |
| Product-aware | The log should understand organizations, apps, users, agents, and resources. |
If you mix these concerns into application logs, the result is usually either too noisy for operators or too sparse for investigations.
SqlOS Audit Logs are designed for the second job.
One trail for AuthServer and the host app
Before this release, AuthServer events were audit-like, but host applications still had to invent their own activity trail.
Now both paths use the same central event model.
AuthServer compatibility events flow into the central audit log with:
Source = authserver
Host applications can record product events with:
Source = application
ApplicationKey = northwind-retail
That matters because real investigations rarely stop at the auth boundary.
For example:
- A dashboard admin disables a client.
- A user signs in through SSO.
- A service account updates inventory.
- A support operator exports matching events for one customer.
Those should be queryable from the same governance surface, with enough structure to separate AuthServer events from application events.
The event shape
Every audit event starts with a stable action:
retail.inventory_item.updated
Then it describes the actor:
new SqlOSAuditActor("user", userId, "Company Admin")
And the affected targets:
[
new SqlOSAuditTarget("location", locationId, "Walmart Supercenter #001"),
new SqlOSAuditTarget("inventory_item", itemId, "ProBook Laptop")
]
The event can also include request context:
SqlOSAuditContext.FromHttpContext(httpContext)
And safe metadata:
new Dictionary<string, object?>
{
["result"] = "success",
["sku"] = "LAPTOP-001",
["previousQuantity"] = 12,
["newQuantity"] = 18
}
That is enough for an operator to answer what happened without exposing access tokens, cookies, passwords, raw exceptions, or request bodies.
Idempotency belongs in the audit layer
Audit writes often happen near mutations. Mutations are retried. Workers can restart. Clients can resubmit.
Duplicate audit events make investigations worse because operators have to decide whether two rows represent two actions or one retried write.
SqlOS accepts an IdempotencyKey on audit writes. The key is hashed before storage. If the same key appears again, RecordAsync returns the original event instead of inserting a duplicate.
That lets application code build retry-safe audit writes from domain facts:
retail:inventory:{itemId}:updated:{operationId}
Dashboard-first, API-backed
The embedded dashboard now has a top-level Governance section for Audit Logs:
/sqlos/admin/audit/logs
Operators can filter by:
- organization
- application/client
- source
- action
- actor type and id
- target type and id
- result/status metadata
- free text
- date range
Selecting a row opens structured details for actor, targets, context, and metadata.
The same filters back CSV export. Exports are intentionally bounded for dashboard use: 5,000 rows and a maximum 366-day range. If no dates are supplied, the export defaults to the last 30 days.
Retail example
The Northwind Retail example now records application audit events for successful chain, location, and inventory mutations.
That makes the local demo concrete:
- Open the Retail app.
- Switch to
Company Admin. - Create or update a chain, store, or inventory item.
- Open
/sqlos/admin/audit/logs. - Filter by application key
northwind-retail.
You will see host-application events next to AuthServer governance events, without the frontend calling audit APIs directly.
Why this belongs in SqlOS
SqlOS already understands organizations, AuthServer users, client applications, FGA subjects, sessions, and the embedded admin dashboard.
Audit logs sit naturally on top of that foundation.
Instead of every host app building a one-off activity table, CSV exporter, dashboard page, metadata redaction rules, and auth-event bridge, SqlOS gives you a shared event model and admin surface.
You still decide which product actions matter. SqlOS handles the storage, dashboard filtering, export path, AuthServer compatibility, and common safety rules.
Start with product-critical actions
Do not audit everything.
Start with actions that matter to a customer or operator:
- sign-ins, logouts, and session changes
- client and application access changes
- organization and membership changes
- SSO and MFA configuration changes
- permission and grant changes
- business-resource mutations such as inventory, documents, invoices, or exports
Then keep the event names stable.
That is the practical difference between "we have logs somewhere" and "we can answer what happened."
Next reading: