logicspike/docs

Legal

Data retention schedule — DRAFT (internal detail; a summary is published in the Privacy Policy)

Read this first. The Privacy Policy draft promises retention periods. This table says what each period should be and what the code actually does today. As of 2026-10-07 most rows have no automatic purge. A promise in the public policy must be backed by a job, or the promise must change. The periods below are decided (owner, 2026-10-05); the jobs are not built yet. "Code today" was checked against apps/communication, apps/newsletter-service and apps/mailbox (no retention or purge job exists in any of them); rows marked [confirm] need a check I could not make from the code.

Data Where Intended retention Code today Action
Account and workspace records core DB (Neon) while the account is active; a deleted workspace is erased 90 days after deletion (the old "30 days exportable, 60 archived" was never implemented and is dropped) Since 2026-10-06: deleting a workspace sets status='deleted', stamps deleted_at once, revokes its API keys and repoints members' default workspace. Still true until deployed: nothing is purged. Built 2026-10-22, not deployed: cancelling billing on delete (S4); no mail sent or received from the moment of deletion (S5, P3); and 90 days after deletion its data in comms, newsletter, mailbox (including stored mail files) and contacts is erased (S5). The other services are S6. Account deletion works; only a workspace that still exists blocks it (G27) Gap-log G26 (billing cancel), E3 (a per-service purge after 90 days)
Billing and invoice records core DB 8 years (Indian tax law) kept; never deleted Fine, state it
Email credentials (login codes, password-reset links, team-invite links), tenant system comms DB comms_message_logs.content never stored; any already stored are cleared was stored forever (G25): the invite URL is a bearer token and nothing reads the column. Fixed and deployed 2026-10-06 for new mail: platform mail stores a body only if its type is known harmless (an allow-list, so a new secret-bearing type stores nothing by default) The retention pass (S1, built 2026-10-21, not deployed) clears any stored after 24 hours once RETENTION_ENFORCE is on, so the one-off scripts/clear-system-mail-content.mjs is no longer needed
Email message content (bodies, attachments metadata) per recipient comms DB comms_message_logs.content 90 days (set to NULL; queued rows excluded) no purge — kept indefinitely (audit finding CM-21). No code reads content built 2026-10-21 (S1, lib/retention.ts), not deployed; ships counting only (RETENTION_ENFORCE off); it runs in the sweeper's 6-hour alarm
Email metadata (sender, recipient, subject, status, timestamps) comms DB 13 months (rows deleted; logs by created_at, events by their own occurred_at, since events carry the provider message id, not a foreign key to logs) no purge Same retention pass
Delivery-event raw payloads comms DB comms_message_events.raw_payload 30 days, aged by occurred_at no purge. Do not age by received_at: migration 0012 added it NOT NULL DEFAULT now(), so every older row carries the migration time (G36) Same retention pass
Suppression records (bounces, complaints, unsubscribes) comms DB; newsletter DB as long as needed to keep honouring them. Hard-bounce platform-wide rows expire after 180 days (implemented). Complaints and unsubscribes are kept. hard-bounce expiry implemented: an expired row is ignored, not deleted, so the address stays in the table; the rest is kept State it
Newsletter subscriber events (audit trail of consent and status changes) newsletter DB nl_subscriber_events not time-purged. It is the proof of consent for subscribers who still exist; deleting it by age (an earlier draft said 365 days) would remove the evidence that justifies mailing them. Erased with the subscriber (address replaced by a keyed hash) or with the workspace built 2026-10-22 (S2, newsletter lib/erase.ts + comms /internal/erase-recipient), not deployed: an erase replaces the address with a keyed hash and clears the row's details Tenant purge (E3)
Campaign send rows (one per recipient) newsletter DB nl_campaign_sends tied to the campaign; deleted when a subscriber is erased and when the workspace is purged built 2026-10-22 (S2, newsletter lib/erase.ts + comms /internal/erase-recipient), not deployed: deleted on erase, with the subscriber's link clicks and re-permission entries E3
Subscribers and contacts newsletter DB, contacts service until the customer removes them or the workspace is deleted; erase on a verified data-subject request (via the customer) newsletter: built 2026-10-22 (S2, newsletter lib/erase.ts + comms /internal/erase-recipient), not deployed. Deleting a subscriber now erases them: the address leaves every newsletter table and comms' copies of the newsletter mail sent to it; only a keyed hash in the event log and any suppression entry remain. Before this, the delete kept the address in the event log (G35); an erase also cleans those old rows. Contact-intelligence "delete" is still a soft delete that keeps phone ids and an LLM profile (G28) Hard-delete contacts (G28)
Mailbox messages and attachments mailbox DB + R2 while the mailbox is active; deleted with the workspace kept; no message, thread or attachment delete and no R2 delete anywhere (G30) Per-tenant R2 prefix purge, then rows (E3)
Forms submissions forms DB until the customer deletes them or the workspace is deleted per-submission delete exists; a deleted workspace's forms, submissions, deliveries and view counts are erased 90 days after deletion (built 2026-10-22, data-lifecycle S6, not deployed). Deleting one form only hides it, so its submissions stay (G31) Uploaded files: the media step
Comment IP address and browser string engagement DB eng_comments.source_ip, user_agent 30 days, then cleared; the comment, name and email stay built 2026-10-22 (S3), not deployed: the billing ticker calls /cron/purge-comment-ips daily; it only counts until engagement's RETENTION_ENFORCE is "true". Before this the job existed but nothing called it (G32), so the public engagement privacy page's "30 days" was not yet true Deploy, read a day of counts, switch on
Flow run history (trigger payload, each step's input and output) flows DB automation_runs, automation_run_steps 90 days; runs deleted with their steps built 2026-10-22 (S3), not deployed: daily via the billing ticker (/cron/purge-runs), in batches; only counts until flows' RETENTION_ENFORCE is "true". Before this it hung off a per-minute cron that was never enabled (G32) Deploy, read a day of counts, switch on
Chat transcripts chat DB customer-configured; default [decide] [confirm] Decide
Server and edge logs (IP, route, status, latency) Cloudflare / Worker logs whatever the plan keeps; the policy says "kept briefly" until the real period is read (G64) set by Cloudflare's log retention for the plan in use (days, not necessarily 30); not controlled by us. Worker log lines can contain recipient addresses Read the real retention; state it
Rate-limit counters manager DB rate_limit_buckets hours pruning [confirm] Confirm the cleanup runs
Backups Neon point-in-time recovery per Neon plan window [confirm]. Erased data persists in backups until they age out of that window managed by Neon Confirm the window; state it in the Privacy Policy

Not yet covered by any row above

Each of these holds personal data and has no schedule yet. Add a row (period, code today, action) when its owner decides one:

  • Blog and page comments: the commenter's name, email and comment text, kept until the workspace is purged (the IP and browser string have their own row above).
  • Contact-intelligence profiles and memories (mood, churn and other derived fields), beyond the soft delete in G28.
  • The AI knowledge base: text chunks and embeddings (brain service).
  • Connected-account tokens for social publishing (content-engine).
  • Media files: abandoned uploads, archived private files and never-sent mail attachments are cleaned every 6 hours by media's janitor (live since 2026-10-12); every other file stays until its workspace is purged.
  • The core database's waitlist and feedback tables, and one-time codes in verifications.
  • The internal operations audit log (it stores an IP address).
  • comms_message_logs.recipient for platform mail, which the retention pass will delete only at 13 months.

Rules

  1. The public Privacy Policy states a period only if the row above has a job that enforces it (or the work is scheduled with a date).
  2. A deleted account's data is deleted from every service, not only the core DB. Today that is unverified.
  3. When a customer is the controller (their subscribers, their form submitters), a data-subject request is handled with the customer, and we delete from every service once they instruct us to.
  4. Never extend a retention period silently; changing one needs a Privacy Policy update and notice.
  5. Cancelling a plan and deleting a workspace are different events. Cancelling deletes nothing. Deleting takes the workspace out of the dashboard, revokes its API keys and starts a 90-day clock (tenants.deleted_at) after which each service purges it. Billing and tax records are never purged (8 years).
  6. The consent audit trail is not purged by age (see its row); it leaves only with its subscriber or its workspace.

Build order (decided 2026-10-05; the periods are the owner's: content 90 days, metadata 13 months, raw event payloads 30 days)

The sequencing and gates are in the SES-readiness gap plan (section 17 of the plan file) and tracked in ../communication/ses-production-access/gap-log.md.

  1. No credentials in mail logs (G25), then the retention pass: payloads at 30 days, content at 90 days, rows at 13 months (G14), behind RETENTION_ENFORCE.
  2. A real erase path for a subscriber with a hashed trail (E1, G35). Built 2026-10-22 (data-lifecycle S2), not deployed.
  3. A deleted_at clock (E2), then a per-service purge of a workspace 90 days after deletion (E3), one tenant per pass, behind TENANT_PURGE_ENFORCE.
  4. Schedule the three retention jobs that exist but never run (G32). Built 2026-10-22 (data-lifecycle S3), not deployed; media's was already running.

These use Durable Object alarms; the Cloudflare account is at its cron and queue limits, so do not assume a new cron or queue.

Legal