Skip to content

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. Action query.served. Captures the requesting caller, the query payload (in details), 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. Action tokenize.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. Action access.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:

sql
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>:

sql
USE audit.tenant_default;

SHOW TABLES;
-- audit_queries
-- audit_tokenizations
-- audit_access_decisions

Common queries ​

How many queries did each tenant serve last week? ​

sql
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 ​

sql
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 ​

sql
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:

sql
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:

sql
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:

ColumnTypeNotes
event_timestamptimestamptzWhen the event was emitted by the originating service.
event_datedatePartition key. Always equals event_timestamp::date in UTC.
tenant_idstringThe tenant the event belongs to.
servicestringThe emitting service (e.g. query-coordinator).
regionstringThe region the event originated in.
actionstringPer-table fixed (e.g. query.served).
callerstringThe authenticated caller, format <scheme>:<id>.
outcomestringOne of served, denied, error, or per-table specifics.
detailsmap<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:

  1. Configure the audit-warehouse bucket with object-level retention (compliance mode, retention period matching the regulatory window).
  2. Configure Nessie to use a separate signing key for the audit-main reference, with that key held by an account that has no other access.
  3. 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:

  1. Read-only for compliance auditors: a Trino role with SELECT on the audit catalog and no other catalog.
  2. Write-only for the sink service: an IAM role for the audit-iceberg-sink Pod that can write to the warehouse bucket and append to the Nessie reference, but not read.
  3. Cross-tenant queries: typically restricted to the operator's compliance team. End-tenant operators see only their own namespace.

Moʻolelo — self-hosted federated enterprise knowledge platform.