Rovenue SDK
Guides

Data Retention Policies

How long each kind of data is actually kept on each plan, which tables age out and how a project's window is resolved, and what stays until a project is deleted.

A plan's retentionDays is not one number for all of a project's data. Each kind of data has its own window: some tables have legal or accounting floors longer than the plan's number, some are removed a whole month at a time, analytics follow their own per-plan window, and customer records are never removed by age at all. This page states what actually happens. The dashboard's Settings → Billing page shows the same table, computed from the same source (packages/shared/src/retention/effective.ts).

How long data is kept, per plan

DataFreeIndieStudioEnterprise
Webhook, delivery and Rovi logs; import files30 days90 days730 days1825 days
Audit log (in the database)30 days90 days730 days1825 days
MCP access log (ClickHouse)30 days90 days730 days1825 days
Revenue events and credit ledger (database)at least 365 daysat least 365 daysat least 730 daysat least 1825 days
Revenue and credit analytics events (ClickHouse)365 days365 days730 days1825 days
Funnel sessions and answers30 days90 days730 days1825 days
Product analytics events (ClickHouse: experiment exposures, SDK sessions, funnel events)90 days365 days730 days1825 days
Customers, purchases and entitlementsuntil deleteduntil deleteduntil deleteduntil deleted

What the figures mean:

  • Floors win. The plan's window is floored per table: the audit log and the MCP access log at 30 days (the free plan's own audit number is 7), revenue events and the credit ledger — in the database and in ClickHouse — at 365 days, product analytics at 90 days, funnel sessions and answers at 30 days. A plan, or an override, can never take a table below its floor.
  • Revenue and credit figures are minimums. Those two tables are partitioned by month and the partitions are shared by every project on the instance, so they are removed a whole month at a time, and only at the longest window any project on the instance resolves. On a shared instance with a studio or enterprise project, a free project's revenue events live up to that project's window plus one month. See Partition drops are instance-wide.
  • The audit log is archived, not erased. Entries past the window are moved out of the database into a proof bundle in the project's object storage, where they stay until the project is deleted. See What a checkpoint row means.
  • Analytics follow the plan. The nightly sweep deletes each project's ClickHouse rows past its window (see ClickHouse tables). Product analytics have their own plan number (analyticsDays: 90 / 365 / 730 / 1825); ClickHouse revenue and credit events follow retentionDays floored at 365, the MCP access log auditLogDays floored at 30. Every stored ClickHouse table also carries TTL <event time> + INTERVAL 1825 DAY as a backstop for the longest window.
  • Exposures of a running experiment are kept. An experiment that has not stopped (draft, running or paused) keeps all its exposures whatever their age; once it is completed, its old exposures go on the next nightly run.
  • A checkout in progress keeps its session — for 23 hours. A funnel session whose purchase went pending less than 23 hours ago (Stripe expires an unpaid subscription checkout after 23 hours), and its answers, are not removed by age. An older pending purchase is an abandoned checkout and protects nothing: the session and its answers go at their window like any other. A payment that still arrives later (a one-time PaymentIntent has no Stripe expiry) is recorded from the purchase row, which is never removed by age.
  • Plan changes apply on the next nightly run. Moving to a smaller plan shortens every window from the next run; moving to a larger plan cannot bring back what was already removed.
  • Customer records are never removed by age. Subscribers, purchases, entitlements (subscriber_access), funnel purchases and claim tokens, and every other table without a policy below stay until the project is deleted. One customer can be erased on request: see Erasure and project deletion.
  • Removal is nightly. The sweep runs at 03:00 UTC, so data can outlive its window by up to a day.
  • Backups. Removed data survives in backups until they expire. The backup service takes a daily pg_dump and ClickHouse BACKUP, encrypted, into a private bucket; at the start of each daily run it deletes every backup that would turn 30 days old before the next run (29 days or older), so no backup is ever older than 30 days and removed data leaves the backups within 30 days. Restoring a backup re-applies every erasure and project deletion completed after it (see Erasure and project deletion). A backup.sh snapshot taken by hand is kept until you delete it.

Which tables have a policy

The registry lives at packages/shared/src/retention/policies.ts (RETENTION_POLICIES). Each entry names a physical table, the column that ages it, a reclaim strategy, the tier field its window comes from, a floor, and — for some tables — a default used when a project has neither a plan nor an override:

TableAges onStrategyWindow fromFloorNo-plan, no-override default
audit_logscreatedAtCHECKPOINT_TRUNCATEauditLogDays30 daysnone — kept
credit_ledgercreatedAtDROP_PARTITION (monthly)retentionDays365 daysnone — kept
revenue_eventseventDateDROP_PARTITION (monthly)retentionDays365 daysnone — kept
outgoing_webhookscreatedAt, terminal rows onlyDELETE_ROWSretentionDays7 daysnone — kept
webhook_eventscreatedAtDELETE_ROWSretentionDays7 days90 days
copilot_messagescreatedAtDELETE_ROWSretentionDays7 days90 days
integration_deliveriescreatedAt, terminal rows onlyDELETE_ROWSretentionDays7 days30 days
funnel_answersansweredAt, not while the session's purchase went pending < 23 h ago; always with its session (FK cascade)DELETE_ROWSretentionDays30 days548 days
funnel_sessionslastActivityAt, not while its purchase went pending < 23 h agoDELETE_ROWSretentionDays30 days548 days
import_jobs (files)finishedAt, terminal jobs onlyEXTERNAL_WORKERretentionDays7 days7 days
raw_revenue_events (ClickHouse)eventDateCLICKHOUSE_DELETEretentionDays365 daysnone — kept to the TTL
raw_credit_ledger (ClickHouse)createdAtCLICKHOUSE_DELETEretentionDays365 daysnone — kept to the TTL
raw_exposures (ClickHouse)exposedAt, not while the experiment has not stoppedCLICKHOUSE_DELETEanalyticsDays90 daysnone — kept to the TTL
raw_sdk_session_events (ClickHouse)occurredAtCLICKHOUSE_DELETEanalyticsDays90 daysnone — kept to the TTL
raw_funnel_events (ClickHouse)occurredAtCLICKHOUSE_DELETEanalyticsDays90 daysnone — kept to the TTL
raw_mcp_access (ClickHouse)occurredAtCLICKHOUSE_DELETEauditLogDays30 daysnone — kept to the TTL

outgoing_webhooks expires only terminal rows (SENT, DEAD, DISMISSED) and integration_deliveries only succeeded, skipped and dead_letter — a row still owed a delivery is never removed for being old. Partitioned DELETE_ROWS tables have their emptied partitions dropped afterwards.

The defaults exist only where a table was already removed for every project before this registry existed (a retired worker or pg_partman's own retention); the registry carries that behaviour forward and invents no new window.

How a project's window is resolved

Self-hosted instances (HOST_MODE=self) follow the enterprise windows. Plan limits do not apply to self-hosting, so every project resolves against the enterprise row of billing_tier_limits — 1825 days everywhere — whatever its own billing row says (every dashboard-created project carries a free one). An operator shortens a window with an override. On Rovenue Cloud each project follows its own plan; only a project without a billing row — one inserted outside the dashboard, or whose row was deleted — falls through to rules 2–4.

For every (project, table) pair, the sweep applies these rules, in order:

  1. A project with a plan and a matching billing_tier_limits row resolves normally: the plan's window (retentionDays, auditLogDays or analyticsDays, depending on the table), clamped down by any override, floored at the table's minimum. On a self-hosted instance the plan is always enterprise.
  2. A project with no plan but an explicit override uses the override, floored only.
  3. A project with neither falls back to the table's default, if it has one (see the table above), floored.
  4. A project with neither, on a table with no default, is skipped for that table: it keeps everything. It is not defaulted to the free plan's window.
  5. A project whose (tier, cycle) has no billing_tier_limits row — a reference-data gap — is skipped with its own reason and a warning, never treated as "no plan".

An override can only shorten a window, never lengthen it. There is no dashboard or API surface for overrides yet: an operator inserts a row into project_retention_overrides (projectId, tableName, retentionDays).

Partition drops are instance-wide

credit_ledger and revenue_events are partitioned by month, and a partition holds every project's rows for that month. A partition cannot be dropped for one project only, so:

  • A partition is dropped only when every project on the instance has resolved a window for that table in this run. One project without a resolved window (rules 4 and 5) blocks the drop for everyone — those rows are then kept until it is resolved or the project is deleted.
  • When every project has resolved a window, the drop uses the longest of them, and a partition goes only once its whole month is older than that window. A row therefore lives at least its own project's window, and up to the longest window on the instance plus one month.
  • Rows that land in a table's DEFAULT partition (no monthly partition existed for their date) are never dropped by age.

Each dropped partition writes one audit row into every affected project's chain before the DROP TABLE runs, recording that project's own row count.

ClickHouse tables

The six stored ClickHouse tables are swept by the same nightly job, per project:

  • Every project's window is resolved with the rules above. Projects that resolve the same window for a table are deleted together — DELETE … WHERE projectId IN (those projects) AND <event time> < cutoff — one monthly partition at a time, so the first run on years of data is a series of partition-sized deletes rather than one table-wide one. A project with no resolved window is never in a group: its rows stay until the 1825-day TTL.
  • The deletes run as the delete-only eraser user. Five tables take a lightweight DELETE FROM (the eraser holds ALTER UPDATE(_row_exists) on each of them, the row-mask column, and nothing else); raw_revenue_events carries a projection, which ClickHouse 24.3 refuses a lightweight delete on, so it takes ALTER TABLE … DELETE IN PARTITION. verify-clickhouse-eraser checks every statement at install/upgrade.
  • If ClickHouse is down or refuses a delete, that table is counted as a failure (rovenue_retention_sweep_skipped_total{reason="error"}, a log line, the projects counted failed in the sweep's report) and nothing is recorded as done — the next night repeats the whole delete. Without ClickHouse configured at all the policies are skipped with reason clickhouse-unconfigured.
  • Dashboard charts are clamped to these windows: a range longer than the project keeps (the Charts page's All, a 365-day overview range on a 90-day analytics plan) is cut to the window, and the view says so.

What a checkpoint row means

audit_logs is an append-only hash chain, so expiring it cannot be a plain delete. CHECKPOINT_TRUNCATE does this, for every project whose window has a segment old enough to reclaim:

  1. Export the aging segment as a proof bundle — the same assembly the /proof dashboard endpoint uses.
  2. Write that bundle to object storage and confirm the write.
  3. Only then delete the segment's rows.
  4. Chain a retention.audit_checkpointed row recording the cutoff, the number of rows deleted, the last deleted row's id and hash, and the bundle's storage key.

The bundle lives in the private import bucket (IMPORT_STORAGE_BUCKET) at audit-checkpoints/<projectId>/<checkpointId>.json. Nothing sweeps that prefix: the archived entries stay there until the project is deleted, when the purge removes the whole prefix. If object storage is not configured, CHECKPOINT_TRUNCATE fails closed and the audit rows stay in the database.

Import files

Uploaded RevenueCat/Adapty import files and their reports are removed by a separate nightly worker (workers/import-retention.ts) once the job's window has passed since it finished. It resolves its window through the same registry entry (import_jobs), deletes the objects, and stamps filesDeletedAt so a re-run never selects the job again. The import_jobs row itself stays.

Other stores

DataKept forWhat removes it
Published outbox rows72 hours after publishinghourly outbox cleanup
DSAR export filesdownloadable 30 days; the file stays until the subscriber is erased or the project is deletederasure / project deletion
Dashboard accounts, sessions and sign-in methodsuntil the account is deletedaccount deletion

Erasure and project deletion

Erasing a customer (dashboard Anonymize, or POST /v1/dsar/erasure) replaces their appUserId with a pseudonym, clears their attributes, deletes their rows from the ClickHouse tables keyed by subscriber, and deletes their DSAR export files. Their purchases, entitlements and credit-ledger entries stay, attached to the pseudonymous subscriber row. See DSAR.

Deleting a project removes its Postgres rows (funnel answers included — they cascade with their sessions), its ClickHouse rows and its storage objects (assets, imports, DSAR exports, audit checkpoint bundles). Its audit log entries stay, with the project link cleared, until the project's audit window (resolved at deletion; 1825 days on a self-hosted instance) has passed; the nightly sweep then removes them.

After a restore. Each completed erasure and project deletion leaves a marker in the private import bucket (deletion-markers/: the kind, the project id, the subscriber's or purge's internal id and the completion time — no app user id, attribute or email), kept for 37 days (the 30-day backup retention plus a margin). A database restore does not touch object storage, so after restoring a backup the operator runs replay-deletions, which re-applies every deletion completed since that backup was taken. See docs/operations/backup-restore.md, "Replay deletions".

The sweep itself is a nightly BullMQ repeatable job (workers/retention-sweep.ts, 03:00 UTC), safe to run on multiple API replicas — every reclaim is either scoped to the projects it names or, for a partition drop, gated on every project having resolved a window first.

On this page