Skip to main content

Audit and compliance

QRY's compliance posture rests on three pillars:

  1. Audit logs — every privileged action is recorded.
  2. Soft-delete with retention — deletes are recoverable for a finite window.
  3. Data-subject support — admins can fulfil GDPR-style requests (access, rectification, erasure) from the admin panel.

This page is for the admin who has to answer "who accessed table X last week?" or "a former employee invoked their right to erasure, what do I do?".

Audit log

What's recorded

Every action that affects another user, another user's data, or tenant configuration:

  • User management (create, role change, deactivate).
  • Datasource config (add, edit, remove, credential change).
  • RBAC / ABAC / DAC grant changes.
  • License events (validation, key rotation).
  • Admin overrides (DAC override, conversation read by admin).
  • ABAC enforcement decisions (Layer 2, both enforce blocks and audit_only would-block events).
  • Scheduled task creation / edit / pause / delete.
  • API gateway calls against data products.
  • Forge wave deploys.

What's not recorded in the audit log (these have separate, structured logs):

  • Routine LLM inference calls.
  • Routine database queries by users in normal use.
  • Conversation content (live in the conversation; visible to admins via DAC override).

Where to read it

Admin > Audit log. Filter by:

  • Time range.
  • Actor (user id).
  • Action type.
  • Affected resource.

Each entry shows: timestamp, actor, action, target, before/after where applicable, source IP. Export to CSV for downstream audit tooling.

Retention

Default: 2 years for audit log entries. Configurable in Admin > System Settings > Compliance > Audit retention.

Audit log entries are never purged by user removal — they reference user ids that stay in the database for audit purposes. This is the correct behaviour for compliance: a removed user's actions remain attributable.

Soft-delete model

Almost everything that "deletes" in QRY is soft-deleted with a deleted_at timestamp, then permanently removed by a cleanup cronjob after a retention window.

AssetRetentionRecovery
Conversations30d (auto-checkpoint) / 60d (manual checkpoint) — see Checkpoints and rewindAdmin can restore within window
Notebooks90dAdmin
Dashboards30d (default)Admin
Workspaces30dAdmin
Scheduled tasks30dAdmin
Files (uploaded to a workspace)30d in workspace storage backendRe-upload from source
Users90dAdmin
Audit log entries2y (no soft-delete; hard retention)n/a

The cleanup cronjob runs nightly. After the retention window, recovery is impossible.

Eager-load gotcha

The soft-delete model means eager loads must filter on deleted_at IS NULL (typically via .and_(Message.deleted_at.is_(None))). Skipping the filter surfaces "deleted" rows in user-facing pages — which is wrong. This is a backend invariant; if you write reports that read directly from the DB, respect it.

GDPR / data-subject requests

Right to access

A user (or authorised third party) requests "what data do you hold about me?"

In Admin > Users > {user} > Data export. Generates a tarball with:

  • Every conversation, notebook, dashboard, scheduled task, and saved query they created.
  • Every memory entry scoped to them personally.
  • Their account metadata (name, email, role, group memberships).
  • Audit log entries where they were the actor.

The tarball is downloadable for 7 days, then auto-deleted.

Right to rectification

A user wants their name or email changed. Edit in Admin > Users > {user}. Audit log entry recorded.

Right to erasure

A user requests "delete everything you have about me". The pattern:

  1. Confirm the request is legitimate (admin step — QRY doesn't authenticate the request).
  2. Reassign or delete owned content — workspaces / dashboards / notebooks the user owned. Either transfer ownership or delete.
  3. Erase the user accountAdmin > Users > {user} > Erase (GDPR). This is more aggressive than Remove — it overwrites name / email with random tokens, hard-deletes personal memory entries (workspace memory stays for the team's continuity), and triggers immediate cleanup of soft-deleted conversations and files.
  4. Audit — the erasure itself is logged (with the random-token id, not the original).

After erasure, the user's actions in the audit log are still attributable to a user-id-with-erased-pii, which is the typical GDPR compromise: identification is gone, accountability for past actions remains.

Right to data portability

The export from Right to access is in JSON / CSV / standard formats — directly portable to other tools. No proprietary lock-in.

Tenant-level data isolation

For multi-tenant deployments, each tenant has its own:

  • Database (qrydb_<id>).
  • Memorystore Valkey instance.
  • Namespace.
  • License.

Cross-tenant data leakage is impossible at the data layer (no shared tables; query routing enforces tenant scoping). The only cross-tenant resource is the GKE cluster itself + the Cloud SQL Postgres instance (separate DBs per tenant).

For data-residency-sensitive tenants, a separate cluster (e.g. EU-only) is the path; multi-tenant in one cluster shares the cluster's residency.

Encryption at rest

  • PostgreSQL — Cloud SQL has encryption at rest by default. ixenlab uses Harvester's storage which encrypts at rest as well.
  • Datasource credentials — Fernet-encrypted in the DB, key derived from JWT_SECRET_KEY via SHA-256.
  • Backups / dumps — encrypt before archiving outside the cluster.

Encryption in transit

  • All public traffic via Traefik with cert-manager-issued Let's Encrypt certificates.
  • Internal cluster traffic — Kubernetes service-to-service is plain HTTP; rely on the cluster's network policy to prevent eavesdropping. For tenants with stricter requirements, mTLS sidecars (Istio / Linkerd) can be enabled.

Common issues

An auditor needs proof that user X had access to table Y on date Z. Audit log filtered by (actor = X) AND (action = "query") AND (target ~ Y) and time-range Z. Export to CSV for the auditor.

A user's data export tarball is missing some content. Workspace conversations they participated in but didn't create are not in their export — those are workspace-owned, not user-owned. The export captures personal artifacts only.

ABAC audit_only mode is logging hits but the user reported they couldn't query. Different mechanism: audit_only doesn't block; the failure was at a different layer (RBAC or DAC). Check the user's Effective permissions.

Cleanup cronjob hasn't run. celery-beat not scheduling, or the task is failing silently. Check qry_scheduled_task_failures_total for the cleanup task name.

A removed user is still listed as the author of conversations. Correct behaviour. Remove ≠ erase. For GDPR-mandated erasure, use Erase (GDPR) which overwrites the name.

See also

QRYA product of IXEN.