Jarby

Data Retention & Deletion Policy

Effective: 2026-10-31 · Version 1.1 · Revised: October 2026 · Reviewed annually — see the retention review log, kept by the operator

Scope

This policy describes how Jarby retains and deletes consumer data collected through the application, including data sourced from Plaid (transactions, balances, credit-card liability records) and data generated by the application's own functionality (recurring items, rewards ledger, feedback conversations, MCP audit log).

The current operational model is an invite-only closed-beta deployment operated by a single individual (the operator) for a small invited group. Retention windows below reflect the operating reality of that scope; they will be tightened before public-beta expansion. The Privacy Policy is the single statement of what is collected and who receives it; this policy covers how long each class is kept.

Consumer rights

Consumers (today: invited beta users) have the following rights, exposed in-app at /settings/privacy and accessible without contacting support:

RightHow to exerciseReference
Access — receive a copy of the records linked to your account (accounts, transactions, cards, rewards, recurring items, payees, feedback, the raw Plaid archive with access secrets removed, stored AI request copies, and activity and audit logs; not included: computed card-value and recurring-detection caches, rebuilt from those records); ask the operator for anything missingClick Export my data in /settings/privacy, or GET /api/me/exportCCPA §1798.100 (functional equivalent)
Deletion — permanently delete account and all associated data, except an encrypted bank token we hold only to end the bank connection (a deleted account's token copy, PlaidRemoveOrphan: Plaid's Item id and the encrypted token (none once the token has ended), no user id), except in the Plaid sandbox, where it is dropped: the token at most 60 days from its first decrypt failure or, if none, from when it was kept, and the record (Plaid's Item id, no token) at most 90 days. Backup copies follow the Backups row: about 30 days once older hand-made backups are cleared; until then some are kept longer.Click Delete my account in /settings/privacy, or DELETE /api/me (with confirmation header)CCPA §1798.105 (functional equivalent)
Correction — edit or correct data held about youMost of your data is editable in-app via the relevant pages; ask the operator to correct anything that is notCCPA §1798.106 (functional equivalent)
Portability — receive your data in a machine-readable formatThe export endpoint returns JSONCCPA §1798.130
Disconnect — sever a connected Plaid Item without deleting your accountClick Disconnect on /settings/connections for the relevant institution. Syncing stops. If the user chooses Keep, the institution's accounts and transaction history stay, shown as closed; if the user chooses Delete, they are deleted. Raw records are deleted when you disconnect. If that deletion fails, they stay until it is retried or your account is deleted. After Keep, reconnecting the same bank brings that history back onto the accounts it matches, and any account it can't match stays under closed accounts. The user chooses Keep or Delete for that bank's accounts and transactions; a Plaid Portal revoke deletes them, and so does Plaid telling us the connection no longer exists (ITEM_NOT_FOUND). When several connections get that answer, or one gets it, with no recent successful Plaid call on your connections, as from a wrong Plaid environment or key, we hold the deletion, unless another of your bank connections that Jarby checks in the same run works normally, which shows the problem is with that connection and not with Jarby's setup; then we delete them. While a deletion is held, the hold is recorded (the held-delete row below) and the background retry job checks again with Plaid for up to 60 days, deleting once Plaid confirms the connection is gone. If Plaid cannot confirm it, the delete may not happen automatically: after day 60 the check stops, nothing is deleted, and it stays in Settings → Connections, with the data we already have, until you delete it there. If that revoke delete fails, an alert is raised, the bank stays shown as closed, and the background retry job re-runs the delete for up to 60 days from the failed delete (an alert from day 45; after day 60 the retry stops with an alert and the bank stays shown as closed); the user can also delete it with Delete history, or delete the account.—

Export and delete requests are honored immediately in-app (self-service); there is no manual review queue.

A user whose invite is revoked keeps both rights: they can sign in, with Google 2-Step, for up to one hour per sign-in, to export or delete only (every other route refuses them). A self-delete also deletes their BetaInvite row and ends the session.

Retention windows by data class

Data classRetained forTrigger to purgeNotes
Active user data — Account, Transaction, UserCard, RecurringItem, Payee, Category, MerchantRule, BalanceAdjustment, StatementCycle, PayOverTimePlan, RewardsLedger, RedemptionLog, BillOccurrenceWhile the account is activeDELETE /api/me cascades through every user-owned table in a single transactionConsumer-controlled lifetime. Accounts and transactions of a disconnected Plaid Item stay here, shown as closed, if Keep was chosen
Plaid access_tokenWhile the Plaid Item is connectedDisconnect (DELETE /api/plaid/items/[itemId]) deletes the local row in one transaction, then, only after the raw-payload purge succeeds, calls Plaid's /item/remove with a copy of the token; account deletion (DELETE /api/me) calls /item/remove for each live Item before the local deleteStored encrypted at rest using AES-256-GCM (per-row IV + auth tag); see lib/encryption.js. If Plaid's release fails or the token will not decrypt on Disconnect, the copy waits in the pending-removal row below. If the release fails at account deletion, a copy with no user id waits in the deleted-account row below. A token that fails to decrypt (a wrong key or damaged data), at Disconnect, at account deletion or in the lifecycle release, is not deleted when it fails to decrypt: the encrypted copy is kept and retried, except in the Plaid sandbox, where it is dropped. Each failure is logged with the error code and the local row id, and an alert carrying only the code is raised at most once a day per code. Limit for a kept token copy: the token at most 60 days from the first decrypt failure (else from when the copy was kept), then it is deleted. On a pending-removal row (PlaidRemoveRetry) or deleted-account row (PlaidRemoveOrphan) the row then holds the record (Plaid's Item id, no token), at most 90 days in total. On the Item's own row (PlaidItem) the token ends at day 60 and the row stays, with the bank's history, until the user deletes it (Disconnect) or the account (see the kept-token row below). Backup copies follow the Backups row: about 30 days once older hand-made backups are cleared; until then some are kept longer.
Plaid sync cursor + Item metadata (PlaidItem row)While the Plaid Item is connectedDisconnect deletes the row; account deletion cascades itDeleted at disconnect with the access token. If Keep was chosen, its accounts stay as closed history, detached from the Item. A token that cannot be released or read is kept as the kept-token row below says.
Lapsed trial or ended subscriptionBank connections released after 24 h; all data deleted 30 days after lapseDaily billing lifecycle job, once switched on (deletion is switched on per environment by the operator). It is off in production today (BILLING_LIFECYCLE_ENABLED unset), so no lapse release or delete runs there yetResubscribing within 30 days restores access; banks must be re-linked. A failed release is retried on the next run, and one still failing 7 days after the lapse raises an alert. A token that will not decrypt is kept, encrypted, with no Plaid call, except in the Plaid sandbox, where it is dropped; each failure is logged with the error code and the local Item id, an alert carrying only the code is raised, and the next run retries it. A failed release does not hold the 30-day delete: account deletion makes a last release attempt for each Item and then deletes the account (see the Plaid access_token row), and an alert names each Item it could not release. At the 30-day delete a still-kept token moves to a deleted account's token copy (PlaidRemoveOrphan, no user id), keeping its first-failure date: at most 60 days for the token and 90 for the record from that date.
Plaid access_token kept after a decrypt failure or a failed release (on the Item's own row, PlaidItem; a pending-removal row, PlaidRemoveRetry; or a deleted account's token copy, PlaidRemoveOrphan: Plaid's Item id and the encrypted token (none once the token has ended), no user id)Kept and retried, except in the Plaid sandbox, where it is dropped. The token: until a retry releases it, and at most 60 days from the first decrypt failure (else from when the copy was kept); on day 60 it is deleted, after an alert from day 45. The record in a pending-removal or deleted-account row (Plaid's Item id, no token): deleted by the operator within 30 days after that, at most 90 days in total; an alert carrying only a count is raised for a record past 90 days or a token past 60. On the Item's own row, the token is deleted at day 60 and the connection shows as needing reconnect; the row stays as an Active connection, with the bank's history, until the user deletes it (Disconnect) or the account (or the lapse delete deletes it). No 90-day limit or alert applies to that row; the operator's request to Plaid support has a day-90 target, checked by hand. If the account is deleted after day 60, a deleted-account row with Plaid's Item id and no token is written, keeping the first-failure date; the 90-day record limit and alert above apply to it. Backup copies follow the Backups row: about 30 days once older hand-made backups are cleared; until then some are kept longer. Encryption-key snapshots that can read a backup are kept indefinitely (see below).The background retry job and the daily lifecycle job retry the release; after day 60 the operator asks Plaid support to remove the Item by item_id and deletes the recordEach failure is logged with the error code and the local row id; alerts carry only a code or a count. Separate rule: SEC3 D9 hold rows unreadable at day 30 are deleted by design
Raw Plaid payloads (PlaidRawSnapshot rows: what Plaid sent for an Item's accounts, transactions and liabilities)While the Plaid Item is connectedDeleted when that Item is disconnected (either choice) or revoked, or with the account.Purged before the Plaid release; a failed purge is logged with the Item id and leaves a pending-removal row for every disconnect, a bank whose connection had already ended at Plaid included. The background retry job re-runs the purge automatically, with backoff, until it succeeds, and the user's retry also re-runs it for a bank still connected at Plaid; DELETE /api/me cascades. If Keep was chosen, the parsed accounts and transactions stay under Active user data. The Plaid release waits for a successful purge; the pending-removal row below lists when its token copy is deleted
Disconnected Item, Keep chosenUntil the user deletes it or account deletionthe in-app "Delete history" action or the DELETE /api/me cascadeNo new data after disconnect
Pending-removal token (PlaidRemoveRetry row: a copy of the Item's encrypted access token, or no token for a bank whose connection had already ended at Plaid)The token copy: until Plaid confirms the removal, and at most 60 days from the first decrypt failure, else from when the row was created. On day 60 the background retry job deletes the token copy, whatever its last error (a failed retry does not move that date); an alert is raised from day 45 and again when the copy is deleted. The row then holds no token and is not retried again: after day 60 we can no longer remove the Item at Plaid ourselves. The row keeps Plaid's Item id so the operator can ask Plaid support to remove the Item by item_id, and the operator then deletes the row within 30 days (or it is deleted with the account). A row with no token from the start: until its raw-payload purge succeeds. A delete-pending row (no token and no Plaid Item id; it holds the user id, the bank name, the local ids of the accounts whose delete failed and, for a one-account revoke, that account's Plaid account id) is kept until the delete it carries succeeds, and at most 60 days from when the row was created; an alert is raised from day 45 and when the row is deleted on day 60. The accounts it named then stay shown as closed until the user deletes them with Delete history or deletes the account. The 60 days covers this row only: backups keep a copy for about 30 days more (see Backups), and a still-connected Item's own token (PlaidItem) follows the rows abovePlaid confirms the removal (or reports the Item already gone); for a row with no token, its raw-payload purge succeeds; DELETE /api/me (one last Plaid release attempt first for a row with a token; if it fails, the copy moves to a deleted account's token copy below, keeping its dates, except in the Plaid sandbox, where a token that will not decrypt is dropped); or a reconnect that Plaid answers with the same Item idKept when the snapshot purge or the Plaid release fails; deleted once the purge and (if it has a token) the release succeed. A row with no token is kept only to finish the raw-payload deletion or, for a delete-pending row, the history delete: it is never sent to Plaid and not shown in the app's pending-removal list. A token copy uses the same AES-256-GCM encryption as the Item's token and is never logged or returned to a client
Deleted account's token copy (PlaidRemoveOrphan row: Plaid's Item id, a copy of the encrypted access token and the last error code; no user id)The token copy: until Plaid confirms the removal, and at most 60 days from the first decrypt failure, else from when the copy was first kept (the account deletion, or the earlier failed disconnect it came from). On day 60 the background retry job deletes the token copy; an alert is raised from day 45 and again when the copy is deleted. After day 60 we can no longer remove the Item at Plaid ourselves. The row itself is not deleted by code: the operator asks Plaid support to remove the Item by item_id and then deletes the row, no later than 30 days after that alert (an operator step in the runbook). The 60 days covers this row only: backups keep a copy for about 30 days more (see Backups)Written by account deletion (DELETE /api/me, or the operator's user delete) when the last /item/remove fails; the background retry job retries the release with backoff and deletes the row once Plaid confirms the removal (or reports the Item already gone)Holds no user id or email. Same AES-256-GCM encryption as the Item's token; never logged or returned to a client. A token that fails to decrypt at account deletion is kept here too, except in the Plaid sandbox, where it is dropped
Held ITEM_NOT_FOUND delete (PlaidItemNotFoundHold row: the local Item id, why the delete was held, the number of re-checks and the last error code; no token, no Plaid id, no bank name; linked to the user only through the Item)Until the hold is resolved, and at most 60 days from when it was writtenThe background retry job deletes the row when Plaid answers for the Item (nothing else is deleted), when the Item's delete goes through (the row is deleted with the Item), when the Item has no token or was released, and on day 60 (the Item and its data stay; an operator alert from day 45 and on day 60). It is deleted with the Item when the user disconnects the bank, and with the account (DELETE /api/me cascade)Written only when the ITEM_NOT_FOUND delete guard holds a delete; logs and alerts carry ids and counts only
Feedback conversations (Feedback)While the account is activeDELETE /api/me cascadeConsumer-submitted; covered by deletion right
MCP audit log (AgentAuditLog)While the parent ApiKey existsCascades when the ApiKey row is deleted, which happens on DELETE /api/meInternal operational records
API keys (ApiKey)Until revoked or account deletedRevoke via /settings/api-keys (sets revokedAt); permanent delete via DELETE /api/meToken hashes only — plaintext shown once at creation
Alert dismissals (AlertDismissal)While the account is activeDELETE /api/me cascadeOperational state, not consumer-submitted
Sign-in metadata (User.email, createdAt, updatedAt)While the account is activeDELETE /api/me cascade
Anthropic and Plaid API audit rows (UsageEvent)While the account is activeDELETE /api/me cascadeEndpoint, model, token counts, status, duration and a short error message; no request or response content
Webhook receipts from Plaid and Stripe (WebhookEvent, sources plaid and stripe)90 days from receipt by default (WEBHOOK_EVENT_RETENTION_DAYS); never less than 30 days, and a lower or invalid setting falls back to 90A daily job, first run 5 minutes after the server starts, deletes every receipt older than the window, processed or notSource, event type, code, a hash of the body, the Plaid item id where there is one, and the processing result; not linked to a user, so not removed by DELETE /api/me
Stripe billing recordWhile the account is activeDELETE /api/me asks Stripe to delete the customer Jarby created (best effort; if the call fails the Jarby data is still deleted); Stripe's own retention then appliesBilling runs in Stripe's test mode today
Server logs (stdout/stderr from the application server)Kept on Jarby's server, not sent to any third party; retention period (to confirm)(to confirm)Plaid access tokens and bank credentials are never written to logs
Development and test copies of the databaseToday: copies made from the live data are kept on equipment the operator owns, so they hold the operator's own data. Before anyone else is invited: none made from live dataBefore anyone else is invited, the operator deletes the existing copies made from live data, and no new ones are madeFrom then on, development and test copies hold generated test data only
Backups (nightly database dumps; from the move to DigitalOcean, daily backup files are encrypted by us with age before upload to DigitalOcean Spaces, and the database is encrypted at rest by DigitalOcean's managed PostgreSQL)The last 30 nightly copies, about 30 daysEach copy is deleted as it ages out (KEEP_LAST=30, scripts/backup/nightly-backup.sh; in Spaces a 30-day lifecycle rule; on the NAS the pull keeps the newest 30); an account deleted today therefore remains in older backups until they roll offStored on the same server today; from the move to DigitalOcean also in Spaces and in a daily NAS pull outside DigitalOcean; DigitalOcean point-in-time recovery for 7 days (to verify). Whether the backup tasks are running today is (to confirm)
Encryption-key snapshots (weekly; needed to read a restored backup)Kept indefinitely todayNo automatic removal yetStored on the same server as the backups
Admin records — FeedbackBrief (synthesized digests of consumer feedback)Indefinite while admin retains themAdmin manual delete via DB queryNot consumer data — derived analytical records owned by the operator
Waitlist emails (WaitlistEntry: email, joined date)Until invited or removed on requestOperator deletes the row on /admin/waitlistNo IP address or user agent stored; never shared

Compliance reference

This policy is modelled on the consumer-rights provisions of the California Consumer Privacy Act (CCPA) as adapted for an individual non-business operator. The application is not a registered business and does not process consumer data at scale that would trigger CCPA's business-applicability thresholds; the controls above are implemented voluntarily as best-practice baseline.

The application does not collect or process data subject to GDPR (no EU/UK consumers). When public-beta expansion enables non-US consumers, this policy will be revised to add GDPR-specific provisions (lawful basis disclosures, DPA contacts, supervisory authority references, breach notification windows).

Review cadence

This policy is reviewed annually by the operator. Each review is recorded in the retention review log, kept by the operator, with date, reviewer, outcome, and the next scheduled review date.

The application's alerts engine surfaces a retentionReviewDue alert 30 days before the next scheduled review, and escalates the severity once the review date has passed without an updated log entry.

A review may be triggered out-of-cycle whenever any of the following occur:

  • A new data class is added to the schema
  • A new third-party processor is engaged (today: Plaid, Anthropic, Google, Stripe)
  • A breach or near-miss incident requires policy revision
  • An external consumer-rights request reveals a gap

Out-of-band requests

For requests that cannot be completed via the in-app self-service surfaces (e.g., correction of records the consumer cannot edit themselves, deletion of a specific record while preserving others), contact: bradley@ewers.us

Responses are best-effort within 7 calendar days during the closed-beta phase. Formal SLAs will be defined before public-beta expansion.

Document control

FieldValue
OwnerOperator (Bradley Ewers)
Initial publication2026-04-25
Most recent reviewSee the retention review log (kept by the operator)
Next scheduled reviewSee the retention review log (kept by the operator)
Public URL/legal/data-retention