Skip to main content

Data Retention

This page documents what Align stores, for how long, and how it is deleted. It is written to be audited rather than to reassure: where a retention path is longer than our "process, don't hoard" intent, it is listed here explicitly.

What Align stores

DataStored?Retention
Extracted decisions (title, rationale, status, links, metadata)YesFor the life of the tenant, until deleted by an admin or by user-deletion request. This is the product.
Raw text on the decision record (decision_snapshots.decision_json.raw_text)BrieflyWritten at capture, then cleared by a background job once analysis completes. See the caveat below.
Captured source payload on the decision record (decision_json.context.source.payload)No (since 2026-07-26)Snapshots previously embedded the whole sources row, payload included, which the scrub did not clear. Snapshots now store the source identity only, and migration 087 clears existing records.
Connector capture buffer (sources.payload)No (since 2026-07-26)Records provenance only (tenant, platform, source reference, timestamp). The column is always {} and a database CHECK constraint enforces it, so captured content cannot be written here even by mistake.
Historical import batches (import_job_batches.items_json)YesFull fetched content, including meeting transcripts, held to make imports resumable. Purged with the job after 33 days.
Import suggestions (import_suggestions.raw_content)NoForced empty and enforced by a database CHECK constraint, not just application code.
S3 raw-event archive (optional)Only if you enable itDisabled unless you set S3_RAW_EVENTS_BUCKET. Writes raw fetched items to a bucket you own. Align applies no lifecycle policy - retention is governed by your bucket rules.
OAuth tokens for connectorsYes (encrypted)Until the connector is disconnected; then deleted.
Audit logs (mutations, access denials)YesRetained for the tenant. No purge job currently touches them.
Embeddings (vector representations for search)YesLifecycle follows the decision they describe.

What "processed, not hoarded" does and does not mean

The decision record genuinely drops the verbatim captured message: raw text is cleared in a finally block after extraction. Two caveats a security reviewer should know, because we would rather state them than have them discovered:

  • That clear runs as an unawaited background task with no retry. If the process restarts between the initial write and the clear, that row keeps its raw text. There is no reconciliation sweep yet.
  • Until 2026-07-26 the clear was also incomplete: it rewrote raw_text but not the captured payload embedded alongside it at context.source, so a second copy of the message survived every capture. The write path now stores the source identity only, and migration 087_scrub_source_payload_from_snapshots.sql clears already-written records. We record it here rather than amend quietly, because the verification query we had published checked only the cleared field and would have reported a false all-clear.
  • The decision record is not the only place captured content lands. See below.

Known retention gaps

These are open engineering items, stated plainly rather than papered over:

  1. Historical import batches hold raw content for up to 33 days after the job completes, including Slack/Teams threads and Zoom meeting transcripts.
  2. The optional S3 archive has no built-in lifecycle policy. If you enable it, set an expiry rule on the bucket.

Both sit inside infrastructure you control, and you can purge them at any time.

Closed 2026-07-26: the connector capture buffer (sources) previously retained the full captured payload with no purge. It now stores provenance only, enforced by a database constraint rather than by application code.

Verify it yourself

-- Capture buffer must never hold content (enforced by CHECK; expected: 0)
SELECT count(*) FROM sources WHERE payload <> '{}'::jsonb;

-- Decision rows still holding raw captured text in EITHER location (expected: 0)
SELECT count(*) FROM decision_snapshots
WHERE (decision_json ? 'raw_text' AND decision_json->>'raw_text' <> '')
OR (jsonb_typeof(decision_json->'context'->'source') = 'object'
AND decision_json->'context'->'source' ? 'payload');

-- Import suggestions must never hold raw text (enforced by CHECK; expected: 0)
SELECT count(*) FROM import_suggestions WHERE raw_content <> '';

Deletion

  • Connector disconnect removes that connector's stored OAuth credentials.
  • User deletion (GDPR Article 17) removes the user and, where not under legal hold, their associated records. A full export (Article 20) is available first via the admin export endpoint.
  • Tenant offboarding removes tenant data on request.

Admins can place a legal_hold_until timestamp on decision snapshots. While a hold is active, user-erasure requests against those records are rejected with a 409 legal_hold_active rather than partially applied, so data required for litigation or investigation is preserved.

Scope note: the audit_logs table carries the same column, but nothing currently reads or writes it, so legal hold does not yet apply to audit records. In practice no purge job touches audit_logs either, so audit history is retained regardless - but do not rely on legal hold as the mechanism there.

Backups

Infrastructure backups (RDS automated backups, AWS Backup vault lifecycle) follow their own schedule and are encrypted at rest. Deletion of production records does not immediately purge them from point-in-time backups, which age out per the backup retention policy.