Summary
Track the last login date for every account and drive a two-stage inactivity lifecycle:
- 12 months of inactivity → account deactivated
- 17 months → warning email sent ("your account will be deleted in 30 days")
- 18 months → account hard-deleted (all packs, inventories, and related data)
Deactivated accounts can self-reactivate by logging in with valid credentials.
Motivation
Without a last-login signal, there is no way to detect dormant accounts, which accumulates stale data and inflates user counts.
Design
Database — migration 000026
- Add
last_login_at TIMESTAMPTZ NULL to account (updated on every successful login; falls back to created_at via COALESCE for accounts that never logged in)
- Add
deletion_warned_at TIMESTAMPTZ NULL to account (stamped when warning email is sent; deletion guard — no account can be hard-deleted without this stamp)
- Add
'deactivated' to UserStatus enum (distinct from 'inactive' which is reserved for the email-confirmation flow)
Updated enum: pending | inactive | active | deactivated
Login flow changes (pkg/accounts/accounts.go)
- On successful login:
UPDATE account SET last_login_at = NOW().
- New status branch for
deactivated accounts: auto-reactivate (set status = 'active', clear deletion_warned_at, update last_login_at) and return tokens. Existing inactive/pending path is unchanged.
Cleanup binary (cmd/cleanup/)
Standalone binary invoked by an external scheduler (K8s CronJob / systemd). Three phases run in order:
| Phase |
Threshold |
Action |
| Deactivate |
12 months |
UPDATE account SET status = 'deactivated' |
| Warn |
17 months |
Send warning email + stamp deletion_warned_at |
| Delete |
18 months + warned |
DELETE FROM account (cascade) |
Business logic lives in pkg/accounts/ for unit testability. New make run-cleanup Makefile target.
Status flow
pending ──(email confirmed)──► active
inactive ──(email confirmed)──► active
active ──(12mo inactivity)──► deactivated
deactivated ──(valid login) ──► active ← reactivation
deactivated ──(18mo + warned) ──► [hard deleted]
Out of scope
- Admin UI to manually override account status
- Partial data retention on deletion
Summary
Track the last login date for every account and drive a two-stage inactivity lifecycle:
Deactivated accounts can self-reactivate by logging in with valid credentials.
Motivation
Without a last-login signal, there is no way to detect dormant accounts, which accumulates stale data and inflates user counts.
Design
Database — migration 000026
last_login_at TIMESTAMPTZ NULLtoaccount(updated on every successful login; falls back tocreated_atviaCOALESCEfor accounts that never logged in)deletion_warned_at TIMESTAMPTZ NULLtoaccount(stamped when warning email is sent; deletion guard — no account can be hard-deleted without this stamp)'deactivated'toUserStatusenum (distinct from'inactive'which is reserved for the email-confirmation flow)Updated enum:
pending|inactive|active|deactivatedLogin flow changes (
pkg/accounts/accounts.go)UPDATE account SET last_login_at = NOW().deactivatedaccounts: auto-reactivate (setstatus = 'active', cleardeletion_warned_at, updatelast_login_at) and return tokens. Existinginactive/pendingpath is unchanged.Cleanup binary (
cmd/cleanup/)Standalone binary invoked by an external scheduler (K8s CronJob / systemd). Three phases run in order:
UPDATE account SET status = 'deactivated'deletion_warned_atDELETE FROM account(cascade)Business logic lives in
pkg/accounts/for unit testability. Newmake run-cleanupMakefile target.Status flow
Out of scope