Skip to main content

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".

ConceptCurrent (Supabase)Rebuild
Master flagprofiles.is_disabled = true + disabled_at + disabled_reasonSame columns; AuthService rejects login when is_disabled
Auth blockauth.users ban ban_duration: 876600h (~100 yrs)Drop the ban hack. Reject token issuance/refresh in AuthService based on is_disabled
Workforce statusnurses.is_active=false, status='deactivated', deactivated_atSame
Review counterprofiles.review_request_countSame

๐Ÿ” Migration note โ€” remove the front-end race-condition workarounds. The current React app uses an isSelfDeactivating ref, a sessionStorage('self_deactivating') flag, and "navigate to /login before logout()" 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, receives 204, 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)โ€‹

Originaldeactivate-staff Edge Function { staff_user_id, action: "ban" | "unban" }
NestJSPOST /admin/staff/:id/deactivate ยท POST /admin/staff/:id/reactivate
Guard@Roles('admin')

Server transaction on deactivate:

  1. Verify caller has admin in user_roles.
  2. profiles: is_disabled = true, disabled_at = now(), disabled_reason = 'admin_action'.
  3. nurses/doctors: is_active = false, status = 'deactivated', deactivated_at = now().
  4. Reassign future visits: visits SET nurse_id = NULL, status = 'pending_admin_assignment' where assigned to this nurse and not yet started.
  5. Revoke sessions: invalidate refresh tokens (delete from the refresh-token store / add access-token jti to a Redis denylist until expiry).
  6. Emit staff.deactivated event (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")โ€‹

Originalnurse types "DELETE" โ†’ updates nurse row โ†’ high-severity admin_alerts โ†’ self-deactivate-account Edge Function
NestJSPOST /auth/account/deactivate (uses caller's JWT, body { confirmation: "DELETE" })
GuardJwtAuthGuard (self)

Single server transaction:

  1. Validate confirmation === "DELETE".
  2. Set nurses.is_active = false, status = 'deactivated', deactivated_at = now().
  3. Set profiles.is_disabled = true, disabled_reason = 'self_deactivation'.
  4. Insert high-severity admin_alerts (alert_type = 'staff_self_deactivation').
  5. Revoke the caller's sessions (Redis denylist + refresh-token purge).
  6. 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)โ€‹

LayerCurrentRebuild
Token issuanceSupabase ban blocks login/refreshAuthService.login() / refresh() reject if profiles.is_disabled
Active-request checkProfile flag read on session loadGlobal DisabledUserGuard: rejects any authenticated request when is_disabled โ†’ 403 ACCOUNT_DISABLED
Pre-login detectioncheck-email-exists flags disabled accountsPOST /auth/email/check returns { exists, disabled }; client routes to /account-disabled before asking for a code
OAuth pathLogin.tsx parses banned error in URL hashOAuth 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โ€‹

Originalrequest-reactivation Edge Function (no JWT)
NestJSPOST /auth/reactivation-requests (@Public(), body { email }) and POST /account/reactivation-requests (JWT)

Server logic:

  1. Validate the email exists and is_disabled = true.
  2. Enforce rolling 7-day window: max 3 requests (count reactivation_requests where created_at > now()-7d). Over limit โ†’ 429/limit_reached.
  3. Reject if a pending request already exists โ†’ 409 CONFLICT.
  4. Insert reactivation_requests (status = 'pending').
  5. Increment profiles.review_request_count.
  6. Insert admin_alerts (disabled_user_review_request, severity medium).
  7. Insert a notifications row 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_applications row of type reactivation, reusing the existing identity (look up by email instead of auth.admin.listUsers() โ€” query the users/profiles table 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 NULL constraints (Data Model ยง8), purge becomes "delete the users row and let cascades fan out", except the deliberate visits.nurse_id โ†’ SET NULL step 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โ€‹

TableFieldsPurpose
profilesis_disabled, disabled_at, disabled_reason, review_request_countMaster disable flag + review counter
nursesis_active, status, deactivated_atWorkforce status
reactivation_requestsuser_id, email, status, created_atReview-request tracking + limits
admin_alertsalert_type, severity, related_*Admin notification
notificationsuser_id, type, messageIn-app alerts to admins
user_rolesuser_id, roleAdmin authorization for ban/unban
staff_applicationsemail, user_id, typeRe-signup as reactivation

9. Endpoint summaryโ€‹

EndpointAuthReplaces
POST /admin/staff/:id/deactivateadmindeactivate-staff (ban)
POST /admin/staff/:id/reactivateadmindeactivate-staff (unban)
POST /auth/account/deactivateself (JWT)self-deactivate-account
POST /auth/reactivation-requestspublic (email)request-reactivation
POST /account/reactivation-requestsself (JWT)request-reactivation
POST /auth/email/checkpubliccheck-email-exists
POST /admin/staff/purgesuper_admin / cronpurge-deleted-accounts

10. Rebuild checklistโ€‹

  • Drop the 876600h auth-ban mechanism; reject login/refresh on is_disabled in AuthService.
  • Drop the client isSelfDeactivating ref + sessionStorage flag + pre-logout navigation; deactivation is one server call.
  • Global DisabledUserGuard returns 403 ACCOUNT_DISABLED for 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 @Cron for the 15-day window; purge job honours FK order / cascades.
  • Preserve patient-visit history by nulling visits.nurse_id rather than deleting visits.

End of Account Lifecycle & Deactivation.