Parivar โ Account Lifecycle & Deactivation
Companion to the Technical Architecture blueprint. Specifies deactivation, reactivation, and purge for the NestJS rebuild, and removes the Supabase-specific workarounds the current implementation relies on.
The platform uses a soft-deactivation model: accounts are never hard-deleted on the spot. They are disabled at both the data layer and the auth layer so clinical data and audit trails survive. This doc rebuilds that behaviour cleanly on NestJS + Postgres + Redis, and flags the front-end hacks that should not be carried over.
1. States & the single source of truthโ
Model account state explicitly instead of relying on a 100-year auth "ban".
| Concept | Current (Supabase) | Rebuild |
|---|---|---|
| Master flag | profiles.is_disabled = true + disabled_at + disabled_reason | Same columns; AuthService rejects login when is_disabled |
| Auth block | auth.users ban ban_duration: 876600h (~100 yrs) | Drop the ban hack. Reject token issuance/refresh in AuthService based on is_disabled |
| Workforce status | nurses.is_active=false, status='deactivated', deactivated_at | Same |
| Review counter | profiles.review_request_count | Same |
๐ Migration note โ remove the front-end race-condition workarounds. The current React app uses an
isSelfDeactivatingref, asessionStorage('self_deactivating')flag, and "navigate to/loginbeforelogout()" to dodge an AuthContext redirect race. None of this should be ported. In the rebuild, deactivation is a single server transaction; the client simply calls the endpoint, receives204, clears its token, and routes to/account-disabled. There is no race because the client holds no DB-derived disable state mid-flight.
2. Deactivation methodsโ
2a. Admin-initiated (ban / unban)โ
| Original | deactivate-staff Edge Function { staff_user_id, action: "ban" | "unban" } |
| NestJS | POST /admin/staff/:id/deactivate ยท POST /admin/staff/:id/reactivate |
| Guard | @Roles('admin') |
Server transaction on deactivate:
- Verify caller has
admininuser_roles. profiles:is_disabled = true,disabled_at = now(),disabled_reason = 'admin_action'.nurses/doctors:is_active = false,status = 'deactivated',deactivated_at = now().- Reassign future visits:
visits SET nurse_id = NULL, status = 'pending_admin_assignment'where assigned to this nurse and not yet started. - Revoke sessions: invalidate refresh tokens (delete from the refresh-token store / add access-token jti to a Redis denylist until expiry).
- Emit
staff.deactivatedevent (audit + notification).
On reactivate: clear is_disabled/disabled_at/disabled_reason; set nurses.is_active = true, status = 'available', clear deactivated_at; mark pending reactivation_requests as approved.
2b. Self-deactivation ("Delete Account")โ
| Original | nurse types "DELETE" โ updates nurse row โ high-severity admin_alerts โ self-deactivate-account Edge Function |
| NestJS | POST /auth/account/deactivate (uses caller's JWT, body { confirmation: "DELETE" }) |
| Guard | JwtAuthGuard (self) |
Single server transaction:
- Validate
confirmation === "DELETE". - Set
nurses.is_active = false,status = 'deactivated',deactivated_at = now(). - Set
profiles.is_disabled = true,disabled_reason = 'self_deactivation'. - Insert high-severity
admin_alerts(alert_type = 'staff_self_deactivation'). - Revoke the caller's sessions (Redis denylist + refresh-token purge).
- Return
204. Client clears its token and navigates to/account-disabled.
Self-deactivation starts the 15-day reactivation window (reactivation_deadline = deactivated_at + 15d).
3. Enforcement layers (rebuild)โ
| Layer | Current | Rebuild |
|---|---|---|
| Token issuance | Supabase ban blocks login/refresh | AuthService.login() / refresh() reject if profiles.is_disabled |
| Active-request check | Profile flag read on session load | Global DisabledUserGuard: rejects any authenticated request when is_disabled โ 403 ACCOUNT_DISABLED |
| Pre-login detection | check-email-exists flags disabled accounts | POST /auth/email/check returns { exists, disabled }; client routes to /account-disabled before asking for a code |
| OAuth path | Login.tsx parses banned error in URL hash | OAuth callback checks is_disabled and redirects to /account-disabled |
| Session denylist | โ | Access-token jti denylist in Redis until natural expiry; refresh tokens deleted |
Because access tokens are short-lived, the Redis denylist only needs entries until each token's exp โ keep TTLs aligned to token lifetime.
4. The Account-Disabled page (/account-disabled)โ
A public route handling two cases.
Authenticated disabled user (token still valid but is_disabled): show status + "Request Review"; calls POST /account/reactivation-requests (JWT). Enforces the 3-request / rolling-7-day limit.
Sessionless banned user (arrived via pre-login check, email in client state): same UI; calls POST /auth/reactivation-requests with { email } (no JWT). States: idle โ submitted | pending | limit_reached.
5. Reactivation request flowโ
| Original | request-reactivation Edge Function (no JWT) |
| NestJS | POST /auth/reactivation-requests (@Public(), body { email }) and POST /account/reactivation-requests (JWT) |
Server logic:
- Validate the email exists and
is_disabled = true. - Enforce rolling 7-day window: max 3 requests (count
reactivation_requestswherecreated_at > now()-7d). Over limit โ429/limit_reached. - Reject if a
pendingrequest already exists โ409 CONFLICT. - Insert
reactivation_requests(status = 'pending'). - Increment
profiles.review_request_count. - Insert
admin_alerts(disabled_user_review_request, severitymedium). - Insert a
notificationsrow for every admin user.
Admin side: sees the alert / a "Review Request" badge on the staff list, clicks Enable โ ยง2a reactivate path โ pending requests marked approved.
๐ Rebuild note. Rate-limit the public
{ email }endpoint hard (per-IP + per-email) in Redis to prevent enumeration/abuse, and respond uniformly whether or not the email exists (avoid leaking account existence) while still applying the internal checks.
6. 15-day window & re-signupโ
- Deactivated โค 15 days: the person may re-sign-up, which creates a
staff_applicationsrow of typereactivation, reusing the existing identity (look up by email instead ofauth.admin.listUsers()โ query theusers/profilestable directly). Admins can also re-enable directly from the staff list. - Deactivated > 15 days: records are purged (see ยง7); treated as a fresh registration.
A daily @Cron job computes the window from deactivated_at.
7. Purge (hard delete) โ orderingโ
purge-deleted-accounts (super-admin / cron) hard-deletes accounts past the 15-day window. Honour FK order (this is the live cascade order; in the rebuild, prefer DB-level ON DELETE CASCADE per the Data Model doc ยง8 so most of this collapses to a single delete):
-- 1. Clinical & audit data
DELETE FROM nurse_certifications WHERE nurse_id IN (...);
DELETE FROM nurse_document_signatures WHERE nurse_id IN (...);
DELETE FROM nurse_locations WHERE nurse_id IN (...);
DELETE FROM sos_alerts WHERE nurse_id IN (...);
DELETE FROM assessment_versions WHERE nurse_id IN (...);
DELETE FROM escalations WHERE nurse_id IN (...);
DELETE FROM location_audit_log WHERE nurse_id IN (...);
DELETE FROM email_notifications WHERE assessment_id IN (SELECT id FROM assessments WHERE nurse_id IN (...));
DELETE FROM assessments WHERE nurse_id IN (...);
DELETE FROM first_visit_assessments WHERE nurse_id IN (...);
DELETE FROM clinical_modules WHERE nurse_id IN (...);
DELETE FROM wound_care_forms WHERE nurse_id IN (...);
-- 2. Visit reassignment (preserve patient history)
UPDATE visits SET nurse_id = NULL, status = 'pending_admin_assignment' WHERE nurse_id IN (...);
-- 3. Communication & session data
DELETE FROM staff_applications WHERE email IN (...);
DELETE FROM reactivation_requests WHERE user_id IN (...);
DELETE FROM notifications WHERE user_id IN (...);
-- 4. Identity cleanup
DELETE FROM user_roles WHERE user_id IN (...);
DELETE FROM profiles WHERE email IN (...);
DELETE FROM nurses WHERE email IN (...);
DELETE FROM users WHERE email IN (...); -- own users table, not auth.users
๐ Migration note. With proper
ON DELETE CASCADE/SET NULLconstraints (Data Model ยง8), purge becomes "delete theusersrow and let cascades fan out", except the deliberatevisits.nurse_id โ SET NULLstep that preserves patient-visit history. Keep clinical records that must be retained for compliance out of the cascade and anonymise instead, if a retention policy requires it.
8. Tables involvedโ
| Table | Fields | Purpose |
|---|---|---|
profiles | is_disabled, disabled_at, disabled_reason, review_request_count | Master disable flag + review counter |
nurses | is_active, status, deactivated_at | Workforce status |
reactivation_requests | user_id, email, status, created_at | Review-request tracking + limits |
admin_alerts | alert_type, severity, related_* | Admin notification |
notifications | user_id, type, message | In-app alerts to admins |
user_roles | user_id, role | Admin authorization for ban/unban |
staff_applications | email, user_id, type | Re-signup as reactivation |
9. Endpoint summaryโ
| Endpoint | Auth | Replaces |
|---|---|---|
POST /admin/staff/:id/deactivate | admin | deactivate-staff (ban) |
POST /admin/staff/:id/reactivate | admin | deactivate-staff (unban) |
POST /auth/account/deactivate | self (JWT) | self-deactivate-account |
POST /auth/reactivation-requests | public (email) | request-reactivation |
POST /account/reactivation-requests | self (JWT) | request-reactivation |
POST /auth/email/check | public | check-email-exists |
POST /admin/staff/purge | super_admin / cron | purge-deleted-accounts |
10. Rebuild checklistโ
- Drop the 876600h auth-ban mechanism; reject login/refresh on
is_disabledinAuthService. - Drop the client
isSelfDeactivatingref +sessionStorageflag + pre-logout navigation; deactivation is one server call. - Global
DisabledUserGuardreturns403 ACCOUNT_DISABLEDfor any disabled user's authenticated request. - Redis access-token denylist (TTL = token exp) + refresh-token purge on deactivation.
- Public reactivation endpoint: per-IP/email Redis rate-limit, uniform responses, 3-per-7-day rule, single-pending rule.
- Daily
@Cronfor the 15-day window; purge job honours FK order / cascades. - Preserve patient-visit history by nulling
visits.nurse_idrather than deleting visits.
End of Account Lifecycle & Deactivation.