HappierDocs

Server retention policies

Configure automatic cleanup for self-hosted Happier servers without changing the default keep-forever behavior.

Happier server retention is optional and disabled by default.

If you do not set any retention env vars, the server keeps sessions and other retained records forever.

What retention currently covers

Retention is implemented as one server-side sweep worker backed by one domain registry. Each registry entry owns its domain id, environment keys, policy shape, and deletion rule, so configuration parsing, execution, and public disclosure cannot silently maintain different domain lists. It can clean up:

  • Sessions
  • Session messages inside stale inactive sessions
  • Sidechain messages, with an independently configurable shorter lifetime
  • Account changes
  • Voice session leases
  • User feed items
  • Session share access logs
  • Public share access logs
  • Terminal auth requests
  • Account auth requests
  • Auth pairing sessions
  • Repeat keys
  • Global locks
  • Automation runs
  • Automation run events

How session retention works

The session rule is intentionally conservative.

When HAPPIER_SERVER_RETENTION__SESSIONS__MODE=delete_inactive, the server deletes an entire session tree only if all of the following are true:

  • the session updatedAt is older than the configured cutoff
  • the session lastActiveAt is older than the configured cutoff
  • the persisted session active flag is false
  • the runtime does not currently observe the session as active in memory
  • the final delete transaction rechecks the same cutoff guard before anything is removed

This means the server does not need to inspect or decrypt stored transcript content to apply the current session rule.

How session message retention works

The session message rule is separate from whole-session deletion. When HAPPIER_SERVER_RETENTION__SESSION_MESSAGES__MODE=delete_older_than, the server only prunes messages that are all of the following:

  • older than the configured message cutoff
  • owned by a session whose active flag is false
  • owned by a session whose updatedAt and lastActiveAt are older than the same cutoff
  • below the session read/ready watermarks and outside the preserved tail
  • owned by a session that the runtime does not currently observe as active in memory

The final delete rechecks the same old-inactive session guard. The rule treats message content as opaque JSON, so encrypted and plaintext-at-rest sessions use the same retention path.

Sidechain message retention

Sidechain retention is an additional policy for subagent, nested-tool, and workflow-agent transcripts. It does not change the main transcript and does not require the parent session to be inactive. A sidechain becomes eligible only when its newest stored message is older than the configured cutoff, so a recently active sidechain is retained in full. Eligible rows are removed in bounded transactions.

The general SESSION_MESSAGES policy keeps its existing meaning and can still prune both main and sidechain rows in stale inactive sessions. Configure SESSION_MESSAGES=keep_forever with a finite SESSION_SIDECHAIN_MESSAGES policy when main transcripts should remain available indefinitely while old sidechains expire.

The released v1 server capability does not represent message-level retention. General and sidechain message policies therefore remain operator-visible configuration and are omitted from the v1 response so independently upgraded servers remain parseable by released strict v1 clients.

Default behavior

Retention only becomes effective when both of these are true:

  • HAPPIER_SERVER_RETENTION__ENABLED=1
  • at least one retention domain is configured with a finite policy

If retention is disabled, or if every domain is still set to keep_forever, the effective policy remains keep-forever for every domain.

Global retention env vars

These env vars control the retention worker itself:

HAPPIER_SERVER_RETENTION__ENABLED=1
HAPPIER_SERVER_RETENTION__INTERVAL_MS=3600000
HAPPIER_SERVER_RETENTION__BATCH_SIZE=500
HAPPIER_SERVER_RETENTION__MAX_DELETES_PER_RULE_PER_RUN=100000
HAPPIER_SERVER_RETENTION__MAX_CANDIDATES_PER_RULE_PER_RUN=10000
HAPPIER_SERVER_RETENTION__SWEEP_TIME_BUDGET_MS=10000
HAPPIER_SERVER_RETENTION__DRY_RUN=0
  • HAPPIER_SERVER_RETENTION__ENABLED
    • 0 or unset: retention is effectively off
    • 1: retention worker can run if at least one domain has a finite policy
  • HAPPIER_SERVER_RETENTION__INTERVAL_MS
    • sweep interval in milliseconds
    • default: 3600000 (1 hour)
  • HAPPIER_SERVER_RETENTION__BATCH_SIZE
    • maximum number of rows selected or deleted in one database page
    • default: 500
  • HAPPIER_SERVER_RETENTION__MAX_DELETES_PER_RULE_PER_RUN
    • hard cap per rule for actual deletions in one sweep
    • default: 100000
  • HAPPIER_SERVER_RETENTION__MAX_CANDIDATES_PER_RULE_PER_RUN
    • hard cap on candidate records examined by one rule in one sweep
    • default: 10000
  • HAPPIER_SERVER_RETENTION__SWEEP_TIME_BUDGET_MS
    • wall-time budget shared by the complete sweep
    • default: 10000 (10 seconds)
  • HAPPIER_SERVER_RETENTION__DRY_RUN
    • 1: log what would be deleted without deleting it
    • 0 or unset: apply deletions normally

The retention worker reads these values at server startup, runs one sweep immediately, then continues on the configured interval. Restart the server after changing retention configuration. Within a sweep it gives each enabled domain one bounded database page in registry order, then starts another round for domains that still have work. It stops when the data is exhausted or a row, candidate, or wall-time budget is reached. The next scheduled sweep resumes sidechain discovery from a persisted cursor. Successful runs log per-rule delete counts, candidates examined, database pages, stop reasons, and whether the run was a dry run.

The one-hour default keeps expiry reasonably current and gives a backlogged server up to 24 bounded cleanup opportunities per day. Lower intervals are supported, but keep the interval comfortably above the sweep time budget. The worker skips overlapping runs in one process and uses a database lock to prevent multiple server replicas from sweeping at the same time.

Session policy env vars

Sessions use a dedicated policy shape:

HAPPIER_SERVER_RETENTION__SESSIONS__MODE=delete_inactive
HAPPIER_SERVER_RETENTION__SESSIONS__INACTIVITY_DAYS=30

Supported session modes:

  • keep_forever
  • delete_inactive

HAPPIER_SERVER_RETENTION__SESSIONS__INACTIVITY_DAYS must be a positive integer and is required when the mode is delete_inactive.

Age-based domain policy env vars

All non-session retention domains use the same two-key pattern:

HAPPIER_SERVER_RETENTION__<DOMAIN>__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__<DOMAIN>__DAYS=30

Supported age-based modes:

  • keep_forever
  • delete_older_than

Supported domain names:

  • SESSION_MESSAGES
  • SESSION_SIDECHAIN_MESSAGES
  • ACCOUNT_CHANGES
  • VOICE_SESSION_LEASES
  • USER_FEED_ITEMS
  • SESSION_SHARE_ACCESS_LOGS
  • PUBLIC_SHARE_ACCESS_LOGS
  • TERMINAL_AUTH_REQUESTS
  • ACCOUNT_AUTH_REQUESTS
  • AUTH_PAIRING_SESSIONS
  • REPEAT_KEYS
  • GLOBAL_LOCKS
  • AUTOMATION_RUNS
  • AUTOMATION_RUN_EVENTS

Example:

HAPPIER_SERVER_RETENTION__ACCOUNT_CHANGES__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__ACCOUNT_CHANGES__DAYS=30

Start conservatively:

  1. Enable only the rules you actually need.
  2. Turn on HAPPIER_SERVER_RETENTION__DRY_RUN=1 first.
  3. Begin with session retention and one or two short-lived operational domains such as auth requests or access logs.
  4. Review the server logs to confirm the expected records would be deleted.
  5. Switch DRY_RUN back to 0 only after the dry-run output matches your intended policy.

For many self-hosted setups, a good first production policy is:

HAPPIER_SERVER_RETENTION__ENABLED=1
HAPPIER_SERVER_RETENTION__INTERVAL_MS=3600000
HAPPIER_SERVER_RETENTION__BATCH_SIZE=500
HAPPIER_SERVER_RETENTION__MAX_DELETES_PER_RULE_PER_RUN=100000
HAPPIER_SERVER_RETENTION__MAX_CANDIDATES_PER_RULE_PER_RUN=10000
HAPPIER_SERVER_RETENTION__SWEEP_TIME_BUDGET_MS=10000
HAPPIER_SERVER_RETENTION__SESSIONS__MODE=delete_inactive
HAPPIER_SERVER_RETENTION__SESSIONS__INACTIVITY_DAYS=30
HAPPIER_SERVER_RETENTION__SESSION_MESSAGES__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__SESSION_MESSAGES__DAYS=30
HAPPIER_SERVER_RETENTION__SESSION_SIDECHAIN_MESSAGES__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__SESSION_SIDECHAIN_MESSAGES__DAYS=14
HAPPIER_SERVER_RETENTION__ACCOUNT_CHANGES__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__ACCOUNT_CHANGES__DAYS=30
HAPPIER_SERVER_RETENTION__TERMINAL_AUTH_REQUESTS__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__TERMINAL_AUTH_REQUESTS__DAYS=7
HAPPIER_SERVER_RETENTION__ACCOUNT_AUTH_REQUESTS__MODE=delete_older_than
HAPPIER_SERVER_RETENTION__ACCOUNT_AUTH_REQUESTS__DAYS=7

What users see in the app

Happier surfaces the server's complete retention policy so users understand what it keeps and what it eventually deletes. The UI renders the server-provided domain list generically, including domain ids a newer server adds before the client has a dedicated label.

Today that information is exposed in:

  • the active server settings screen
  • the login and registration entry screen, before authentication
  • session info for sessions affected by finite session retention
  • saved server rows

Clients first read the unauthenticated GET /v2/retention-policy endpoint. It returns a complete array of effective domains. Older servers fall back to GET /v1/features, under capabilities.server.retention; that legacy response is explicitly treated as partial because v1 cannot represent message-level policies.

Operational notes

  • Session retention is based on server-side metadata such as updatedAt, lastActiveAt, and active, not by decrypting stored messages.
  • Session message retention does not parse message envelopes; it applies the same session/watermark guard to encrypted and plaintext content.
  • Account change retention can advance the server changesFloor. After old account changes are pruned, very old /v2/changes cursors can become invalid and return 410 cursor-gone.
  • Retention deletes are permanent. There is no built-in quarantine or archive stage before deletion.
  • Like any timestamp-based cleanup system, retention depends on accurate server time and correct stored timestamps. If you manually corrupt timestamps in the database or run with a badly wrong system clock, the policy can make wrong decisions.

SQLite maintenance after pruning

Light-server SQLite deployments run two background maintenance loops when HAPPIER_DB_PROVIDER=sqlite:

  • WAL checkpointing every HAPPIER_SQLITE_WAL_CHECKPOINT_INTERVAL_MS milliseconds, default 60000, using PRAGMA wal_checkpoint(TRUNCATE).
  • Incremental vacuum every HAPPIER_SQLITE_INCREMENTAL_VACUUM_INTERVAL_MS milliseconds, default 21600000 (6 hours), using PRAGMA incremental_vacuum(N) with HAPPIER_SQLITE_INCREMENTAL_VACUUM_PAGES pages per batch, default 1000.

Both loops use a separate maintenance database client and the SQLite busy timeout controlled by HAPPIER_SQLITE_WAL_CHECKPOINT_BUSY_TIMEOUT_MS, default 5000. Set an interval to 0 to disable that loop. The server intentionally does not run full VACUUM in the live process because it rewrites the database and can block writes for too long; use offline full VACUUM only during an explicit maintenance window.

Incremental vacuum can return pages to the filesystem only when the database was created with SQLite incremental auto-vacuum enabled. Otherwise deleted pages become reusable inside the existing database file, but reducing the file itself still requires an offline full VACUUM maintenance window.

Keep-forever example

If you want to keep the current behavior explicitly, either do not set any retention env vars, or set:

HAPPIER_SERVER_RETENTION__ENABLED=0

Dry-run example for 30-day inactive sessions

HAPPIER_SERVER_RETENTION__ENABLED=1
HAPPIER_SERVER_RETENTION__DRY_RUN=1
HAPPIER_SERVER_RETENTION__SESSIONS__MODE=delete_inactive
HAPPIER_SERVER_RETENTION__SESSIONS__INACTIVITY_DAYS=30

On this page