Audit warehouse
Every query Moʻolelo serves writes a row to the audit warehouse — a separate Iceberg catalog dedicated to compliance evidence. The audit warehouse lives in its own bucket, on its own Nessie reference, with its own catalog name. Operators can grant a compliance auditor read-only access to the audit warehouse without exposing customer content.
This page covers what's in the warehouse, how to register it as a Trino catalog, and the queries you'll run most often.
What's in there
Three tables, all partitioned by event_date (UTC day). Every row carries tenant_id so a single shared warehouse cleanly serves multiple tenants.
audit_queries— one row per served search query. Actionquery.served. Captures the requesting caller, the query payload (indetails), the outcome (served/denied/error), latency, and how many results matched.audit_tokenizations— one row per tokenization or detokenization operation when the privacy substrate is enabled. Actiontokenize.batch/detokenize.batch. Captures the kind of token created, the field name, and the outcome — but never the cleartext.audit_access_decisions— one row per authorization decision the ACL evaluator emitted. Actionaccess.decision. Captures the caller, the resource (details.resource_id), whether access was authorized, and the OpenFGA decision id when applicable.
Tenant separation is by partition + row predicate (tenant_id = '<id>'), not by namespace. This is intentional — auditors usually need cross-tenant queries ("how many denied access decisions in the last 30 days across all tenants?"), and a single namespace makes that one SQL statement instead of N.
Connecting Trino
The audit warehouse is a separate Nessie reference and catalog. Register it as its own Trino catalog:
CREATE CATALOG audit USING iceberg
WITH (
"iceberg.catalog.type" = 'rest',
"iceberg.rest-catalog.uri" = 'https://<your-nessie-host>:19120/iceberg/audit-main',
"iceberg.rest-catalog.warehouse" = 'moolelo-audit-warehouse'
);The URI's path segment (audit-main) is the Nessie reference name; the warehouse is the bucket holding the audit Parquet data. Both are configured by the operator at deploy time and are independent of the ingest warehouse.
If your warehouse is on AWS S3, add s3.region, s3.access-key-id, s3.secret-access-key. The credentials may be different from your ingest warehouse credentials — many operators give the audit warehouse a more restrictive IAM role (read-only for the auditor, write-only for the audit-iceberg-sink service).
Once the catalog is registered, every namespace under it is audit.tenant_<id>:
USE audit.tenant_default;
SHOW TABLES;
-- audit_queries
-- audit_tokenizations
-- audit_access_decisionsCommon queries
How many queries did each tenant serve last week?
SELECT tenant_id, count(*) AS queries
FROM audit.tenant_default.audit_queries
WHERE event_date >= current_date - INTERVAL '7' DAY
GROUP BY tenant_id
ORDER BY queries DESC;For a multi-tenant environment, replace audit.tenant_default with the tenant you want or use a UNION ALL across tenant namespaces.
Show every denied access decision in the last 24 hours
SELECT
event_timestamp,
tenant_id,
caller,
details['resource_id'] AS resource_id,
decision_id
FROM audit.tenant_default.audit_access_decisions
WHERE authorized = false
AND event_timestamp > current_timestamp - INTERVAL '24' HOUR
ORDER BY event_timestamp DESC;Audit a specific user's activity
SELECT
event_timestamp,
action,
outcome,
details
FROM audit.tenant_acme.audit_queries
WHERE caller = 'user:jane.doe@example.com'
AND event_date BETWEEN DATE '2026-04-01' AND DATE '2026-04-30'
ORDER BY event_timestamp;Tokenization volume per field
Useful when sizing the privacy substrate or investigating an unexpected surge:
SELECT
field_name,
token_kind,
count(*) AS ops
FROM audit.tenant_default.audit_tokenizations
WHERE event_date >= current_date - INTERVAL '30' DAY
GROUP BY field_name, token_kind
ORDER BY ops DESC;Reconstruct what was served at a point in time
The audit warehouse uses Iceberg snapshot semantics, so you can pin a query to a snapshot taken at the moment a request was served:
SELECT *
FROM audit.tenant_default.audit_queries
FOR TIMESTAMP AS OF TIMESTAMP '2026-04-15 09:00:00 UTC'
WHERE query_id = '<the-query-id-being-investigated>';This is the canonical way to answer "what did the system know when it answered this query?" — combine it with a snapshot-pinned read of the ingest warehouse to reconstruct the exact corpus state.
Schema reference
The fields you'll use most often:
| Column | Type | Notes |
|---|---|---|
event_timestamp | timestamptz | When the event was emitted by the originating service. |
event_date | date | Partition key. Always equals event_timestamp::date in UTC. |
tenant_id | string | The tenant the event belongs to. |
service | string | The emitting service (e.g. query-coordinator). |
region | string | The region the event originated in. |
action | string | Per-table fixed (e.g. query.served). |
caller | string | The authenticated caller, format <scheme>:<id>. |
outcome | string | One of served, denied, error, or per-table specifics. |
details | map<string,string> | Free-form per-action payload (query text, resource id, etc.). |
audit_queries adds query_id. audit_tokenizations adds token_kind and field_name. audit_access_decisions adds authorized (boolean) and decision_id.
The full schema of every table is discoverable from your query engine — DESCRIBE audit.tenant_default.audit_queries in Trino prints column names and types.
Tamper evidence
Iceberg tables are immutable at the snapshot level — every write produces a new snapshot, and snapshots are not edited in place. Combined with object-store object-lock (S3 Object Lock, GCS Bucket Lock, or the equivalent on your storage), the audit warehouse is a write-once ledger: an attacker with write credentials cannot rewrite history without leaving a snapshot trail that operators can detect.
Operators that need stronger guarantees should:
- Configure the audit-warehouse bucket with object-level retention (compliance mode, retention period matching the regulatory window).
- Configure Nessie to use a separate signing key for the
audit-mainreference, with that key held by an account that has no other access. - Replicate the bucket cross-region for disaster recovery.
The audit pipeline writes are idempotent on (event_timestamp, tenant_id, query_id), so a redelivery from Pulsar after a sink restart cannot create a duplicate row.
Permissions
The audit warehouse does not enforce per-user permissions on its own — anyone with read access to the catalog can read every row across every tenant. Restrict access at the catalog/bucket level:
- Read-only for compliance auditors: a Trino role with
SELECTon theauditcatalog and no other catalog. - Write-only for the sink service: an IAM role for the
audit-iceberg-sinkPod that can write to the warehouse bucket and append to the Nessie reference, but not read. - Cross-tenant queries: typically restricted to the operator's compliance team. End-tenant operators see only their own namespace.

