Audit and compliance
QRY's compliance posture rests on three pillars:
- Audit logs — every privileged action is recorded.
- Soft-delete with retention — deletes are recoverable for a finite window.
- 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
enforceblocks andaudit_onlywould-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.
| Asset | Retention | Recovery |
|---|---|---|
| Conversations | 30d (auto-checkpoint) / 60d (manual checkpoint) — see Checkpoints and rewind | Admin can restore within window |
| Notebooks | 90d | Admin |
| Dashboards | 30d (default) | Admin |
| Workspaces | 30d | Admin |
| Scheduled tasks | 30d | Admin |
| Files (uploaded to a workspace) | 30d in workspace storage backend | Re-upload from source |
| Users | 90d | Admin |
| Audit log entries | 2y (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:
- Confirm the request is legitimate (admin step — QRY doesn't authenticate the request).
- Reassign or delete owned content — workspaces / dashboards / notebooks the user owned. Either transfer ownership or delete.
- Erase the user account — Admin > 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.
- 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_KEYvia 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
- Users and groups — user lifecycle including erase-vs-remove.
- Monitoring and health — log topology and operational signals.
- License management — what the license guarantees about retention.
- ABAC — audit-mode policies as a soft-rollout tool.