MAR and Recovery Deployment Preflight
Operator checks, migration order, runtime security, activation gates and forward recovery for PR 2757.
Candidate And Authority
Application candidate: 445910c9917225c8d003042b7f30e0079a6856e4, PR 2757.
Later documentation-only commits do not replace that application checkpoint.
If the application source changes, record a new candidate and repeat affected checks.
This runbook does not authorize deployment, merge, policy installation or clinical use.
Simmi owns deployment. Record the actual QA and clinical reviewers in the
staging handoff.
The repair record contains local verification.
Local CI and synthetic browser success do not establish staging or pilot acceptance.
The docs, Admin, Patient and Platform Pages checks passed on the application
candidate. The old Workers Builds: kulcare-docs check still failed despite the
reported Worker deletion. Its owner must inspect the remaining integration and
required-check configuration. Do not disable unrelated checks or treat the PR as green.
Comparable procedures: Recovery deployment checks and the existing release acceptance record. Do not use the test-server runners, synthetic policy injection or test-role grants on staging.
Two Separate Gates
| Gate | Required result |
|---|---|
| A: deploy with clinical policies off | Approved deployment, safe schema, runtime RLS/RBAC, real HTTP, truthful unavailable states and no effect on existing care |
| B: approved synthetic nursing QA | Named clinical approval for exact facility-scoped policy content, explicit actor grants/assignments, D5 capacity mapping and an approved synthetic test scope |
Gate A does not permit a full nursing journey. Keep Gate B blocked if approval or
policy is absent. Production policy files are empty by design:
backend/config/hospital_mar_policies.yml and
backend/config/hospital_recovery_policies.yml.
An approval marker in YAML is not proof that a qualified person approved it.
Do not copy a TEST-ONLY-NOT-APPROVED fixture into deployed configuration.
Operator Intake
- Record environment, database identity, backend/frontend revisions and release artifact.
- Record migration identity, runtime identity and approved all-tenant audit identity separately.
- Confirm merge or candidate-preview deployment authority. This PR remains draft until its release conditions are resolved.
- Confirm backup identity and a successful restore rehearsal on an isolated database.
- Rehearse all pending migrations at representative volume, using an approved data copy and access controls.
- Record measured locks, total transaction duration and approved session limits. Do not infer the total duration from a statement timeout.
- Approve a real write pause for affected API, job and other source writers, with a clinical continuity plan. A feature flag is not a write pause.
- Check every pending migration, not just the eight below. The moving base can add other migrations with separate prerequisites.
- Preserve nullable legacy encounters, original performers, unclassified beds and unlinked medication rows. Do not invent data to pass validation.
- Resolve the leftover Workers check through its owner. Keep the working Pages app and unrelated settings unchanged.
Use the established deployment process and its secret-managed connections. Do not put passwords, bearer tokens, patient rows or database URLs in the PR. No command below has been run against staging as part of this handoff.
The reviewed base also contains 20261019000005_backfill_encounter_patient_id.rb.
Its tenant-aware backfill has separate existing-data checks. If an older candidate
has already recorded Recovery care while that backfill is still pending, rehearse
their interaction before deployment: source-identity guards can reject changes to
an encounter used by Recovery. Do not disable those guards or fill identity by
hand. An incompatible partial rollout needs a reviewed forward-recovery plan.
Migration Order
Run from the approved backend release with its supported Ruby and locked bundle.
Set APP_RAILS_ENV to the verified Rails environment for that deployment; a staging
host does not imply that Rails uses an environment named staging.
RAILS_ENV="${APP_RAILS_ENV:?Set the approved Rails environment}" bin/rails db:migrate:status| Version | Effect | Operator condition |
|---|---|---|
20261019120001 | Nullable MAR scope/provenance columns and concurrent unique indexes | Inspect existing objects and any invalid concurrent index before retry |
20261019120002 | MAR versions, occurrences, commands, case links and immutable-history guards | Preserve existing nullable rows; verify new RLS, constraints and grants |
20261019120003 | Validate eight existing-table MAR constraints | Each required validation must succeed; a version row alone is not evidence |
20261020150001 | Recovery capacity, stays, commands, evidence and occupancy guards | Legacy preflight below must pass before its first application; migration locks shared capacity/admission tables |
20261020190001 | manage_recovery permission with no default role grants | Existing catalog row must have the exact expected category, level and defaults |
20261021010001 | Preserve nullable canonical encounter identity and protect active source care | Existing Recovery source identity must agree; no active stay may depend on a closed source encounter |
20261021010002 | Index Recovery source encounters | Verify actual index validity, not only migration status |
20261021170001 | Protect Recovery observation corrections and assessment evidence | Verify the final function and assessment constraint definitions |
Recovery migrations set five-second lock and 60-second statement limits where specified in their source. These are not a total deployment deadline. MAR scope indexing runs outside a DDL transaction. Have the operator approve its session limits through the deployment process; do not change applied migration files.
After approvals and preflight, the deployment operator runs the ordinary migration step. It applies every pending migration; do not run it with unreviewed pending work.
RAILS_ENV="${APP_RAILS_ENV:?Set the approved Rails environment}" bin/rails db:migrateNever use db:reset, db:setup, db:schema:load or test-role provisioning on staging.
Read-Only Baseline
Use an approved all-tenant audit connection, not the runtime connection. Execute
SQL with error-stop enabled. row_security = off does not grant bypass; it makes
hidden-row access fail. Any RLS or privilege error is a stop signal, not zero rows.
Do not grant a bypass role to make the audit pass.
Capture these aggregates at T0 before migration and T1 after migration, while writers remain paused. Expect identical existing-care counts and bed statuses. Save only aggregates in the release evidence. QA writes happen after this comparison.
BEGIN READ ONLY;
SET LOCAL row_security = off;
SELECT 'requests' AS source, count(*) FROM clinical.medication_requests
UNION ALL SELECT 'administrations', count(*) FROM clinical.medication_administrations
UNION ALL SELECT 'observations', count(*) FROM clinical.observations
UNION ALL SELECT 'active_admissions', count(*) FROM clinical.admissions WHERE discharged_at IS NULL;
SELECT status, count(*) FROM clinical.facility_beds GROUP BY status ORDER BY status;
ROLLBACK;Stop on an unexplained delta. Do not delete records or change occupancy to make the counts agree. If these tables were absent before this feature, establish and review that schema prerequisite first.
Legacy Capacity Preflight
Run these three queries only before the first application of 20261020150001.
They mirror that migration's preflight method. Each count must be zero.
Use the same approved audit identity and paused-writer window as the baseline.
If Recovery is already deployed, these legacy queries are not the current occupancy test: a valid Recovery occupant need not have an admission. Use the post-deploy ownership function instead. Do not classify valid Recovery care as a legacy admission defect.
Run all three inside BEGIN READ ONLY; SET LOCAL row_security = off; and finish
with ROLLBACK;. A failure stops the sequence.
Invalid Beds
SELECT count(*) FROM clinical.facility_beds b
LEFT JOIN clinical.facility_rooms r ON r.id = b.facility_room_id
LEFT JOIN clinical.facility_wards w ON w.id = b.facility_ward_id
LEFT JOIN clinical.encounters e ON e.id = b.current_encounter_id
LEFT JOIN clinical.episodes_of_care p ON p.id = b.current_episode_of_care_id
WHERE r.id IS NULL OR w.id IS NULL OR b.status IS NULL OR b.is_active IS NULL
OR (b.status = 'occupied' AND NOT EXISTS
(SELECT 1 FROM clinical.admissions WHERE facility_bed_id = b.id AND discharged_at IS NULL))
OR (b.tenant_id,b.facility_id,b.facility_ward_id) IS DISTINCT FROM (r.tenant_id,r.facility_id,r.facility_ward_id)
OR (b.tenant_id,b.facility_id) IS DISTINCT FROM (w.tenant_id,w.facility_id)
OR (b.current_encounter_id IS NOT NULL AND (e.id IS NULL OR
(b.tenant_id,b.facility_id) IS DISTINCT FROM (e.tenant_id,e.facility_id)))
OR (b.current_episode_of_care_id IS NOT NULL AND (p.id IS NULL OR
(b.tenant_id,b.facility_id) IS DISTINCT FROM (p.tenant_id,p.facility_id)));Invalid Admissions
SELECT count(*) FROM clinical.admissions a
LEFT JOIN clinical.facility_beds b ON b.id = a.facility_bed_id
LEFT JOIN clinical.encounters e ON e.id = a.encounter_id
LEFT JOIN clinical.episodes_of_care p ON p.id = a.episode_of_care_id
LEFT JOIN clinical.patient_facilities r ON r.id = a.patient_facility_id
WHERE e.id IS NULL OR p.id IS NULL OR r.id IS NULL
OR (a.tenant_id,a.facility_id) IS DISTINCT FROM (e.tenant_id,e.facility_id)
OR (e.patient_facility_id IS NOT NULL AND e.patient_facility_id <> a.patient_facility_id)
OR COALESCE(e.patient_id,(SELECT patient_id FROM clinical.patient_facilities WHERE id = e.patient_facility_id))
IS DISTINCT FROM r.patient_id
OR (a.tenant_id,a.facility_id,r.patient_id) IS DISTINCT FROM (p.tenant_id,p.facility_id,p.patient_id)
OR (a.tenant_id,a.facility_id) IS DISTINCT FROM (r.tenant_id,r.facility_id)
OR (a.facility_bed_id IS NOT NULL AND
(b.id IS NULL OR (a.tenant_id,a.facility_id) IS DISTINCT FROM (b.tenant_id,b.facility_id)
OR (a.discharged_at IS NULL AND (b.status <> 'occupied' OR b.is_active IS NOT TRUE
OR NOT EXISTS (SELECT 1 FROM clinical.facility_rooms WHERE id = b.facility_room_id AND is_active)
OR NOT EXISTS (SELECT 1 FROM clinical.facility_wards WHERE id = b.facility_ward_id AND is_active)
OR (b.current_encounter_id,b.current_episode_of_care_id)
IS DISTINCT FROM (a.encounter_id,a.episode_of_care_id)))));Invalid Rooms
SELECT count(*) FROM clinical.facility_rooms r
LEFT JOIN clinical.facility_wards w ON w.id = r.facility_ward_id
WHERE w.id IS NULL OR (r.tenant_id,r.facility_id) IS DISTINCT FROM (w.tenant_id,w.facility_id);Post-Deploy Structure And Ownership
- Require all eight migration versions above, and reconcile any other pending versions.
- Compare actual constraints, functions, triggers, indexes and policies with the rehearsed migration-built schema and the candidate SQL schema.
- Require all eight MAR existing-table validations and all new-table constraints to be valid.
- Require the final nullable encounter guards, deferred occupancy guards, correction-source function and assessment contract.
- Compare T1 baseline aggregates with T0 before allowing new care writes.
Run the following under the approved audit identity after all migrations. Expect
zero invalid indexes, no ownership exception, and one exact permission row:
hospital_operations, manage, [], true.
BEGIN READ ONLY;
SET LOCAL row_security = off;
SELECT indexrelid::regclass FROM pg_index
WHERE indrelid IN (
'clinical.medication_requests'::regclass, 'clinical.encounters'::regclass,
'clinical.medication_administrations'::regclass,
'clinical.hospital_recovery_stays'::regclass)
AND (NOT indisvalid OR NOT indisready);
DO $$ BEGIN
PERFORM clinical.check_recovery_bed_owner(id) FROM clinical.facility_beds;
END $$;
SELECT feature_category, access_level, default_enabled_for_roles, display
FROM reference.permissions WHERE feature_name = 'manage_recovery';
ROLLBACK;Also inspect the new MAR/Recovery table indexes against the rehearsed inventory. The query above is a quick check of shared/concurrent/source indexes, not a complete replacement for object comparison.
Runtime Connection Checks
Run through the same configured database identity used by application requests,
not the migration login or kulcare_rls_test. Do not change its grants here.
Compare the actual role memberships and privileges with the approved runtime
matrix. Test grants in an isolated database are not deployment evidence.
BEGIN READ ONLY;
SELECT current_database(), current_user, rolsuper, rolbypassrls
FROM pg_roles WHERE rolname = current_user;
WITH required(name, needs_update) AS (VALUES
('clinical.medication_mar_versions', false),
('clinical.medication_dose_occurrences', true),
('clinical.medication_dose_commands', false),
('clinical.hospital_case_medication_links', false),
('clinical.hospital_recovery_stays', true),
('clinical.hospital_recovery_commands', false),
('clinical.hospital_recovery_events', false),
('clinical.hospital_recovery_observation_links', false),
('clinical.hospital_recovery_escalation_events', false),
('clinical.hospital_recovery_assessments', false)
)
SELECT t.name, c.oid IS NOT NULL AS present,
c.relrowsecurity, c.relforcerowsecurity,
has_schema_privilege(current_user, 'clinical', 'USAGE') AS schema_usage,
has_table_privilege(current_user, c.oid, 'SELECT') AS can_select,
has_table_privilege(current_user, c.oid, 'INSERT') AS can_insert,
t.needs_update, has_table_privilege(current_user, c.oid, 'UPDATE') AS can_update
FROM required t LEFT JOIN pg_class c ON c.oid = to_regclass(t.name)
ORDER BY t.name;
ROLLBACK;Expect neither superuser nor bypass, exactly ten table rows, all present, both
RLS flags true, schema usage, SELECT and INSERT available, and UPDATE available
where needs_update is true. These are necessary checks, not a full grant list.
Inspect shared source, Identity, Reference and Audit access and required function
privileges through the approved matrix. Do not copy broad test grants or grant
UPDATE/DELETE to immutable history to resolve an error. Check policy definitions
and real allowed/denied requests; flags alone do not prove tenant isolation.
Staging Smoke And Clinical Activation
Use the authenticated MFA shell in real HTTP mode. Record actual backend and frontend revisions; a frontend Pages preview alone does not deploy the backend.
For an authorized facility viewer, inspect these GET requests in browser Network:
/api/clinical/facilities/:facility_id/hospital/mar_capability/api/clinical/facilities/:facility_id/recovery/capability
With no approved policy, MAR reports write_available: false; Recovery reports
policy_status: unavailable and no clinical clearance. These are successful
Gate A outcomes, not missing implementation. Resource/stay/observation actions
have their own permission and source rules; policy absence is not a blanket
claim that every Recovery write is disabled. Use only the approved QA scope.
For Gate B, the clinical owner must approve policy content, facility IDs, actor categories, timing/freshness rules and its exact content hash. The release owner must separately approve installation of those policy files. Select the installed facility policy through supported configuration. Keep an evidence reference; do not publish its private clinical content or fabricate an approval identifier. Use shell RBAC for explicit grants and current staff/practitioner assignments. Use Admin for app flags. A flag or permission alone does not grant clinical authority.
Run the staging handoff. Keep signed home discharge unavailable. Recovery continuing-care handoff is not an ABDM discharge report. Programme U8 remains separate implementation work.
Stop And Forward Recovery
Stop on an audit outage, ownership exception, unexplained baseline delta, unvalidated constraint, invalid index, missing runtime privilege, unsafe role, duplicate clinical effect or lost ability to continue existing care.
If a concurrent MAR index build fails, inspect its exact name and definition. Only an approved operator may remove that identified invalid index and retry. Do not drop a valid index or mark a failed migration as applied.
Do not run destructive down migrations, erase receipts, reset a database or automatically deploy old code after managed care exists. Code rollback requires compatibility proof for current source/history and active care. Prefer a reviewed forward repair. A restore requires an approved care/data reconciliation plan; it can remove care recorded after the backup.
If intake must stop, preserve authorized active-care writes and exact-key receipt recovery. Agree the actual control with the operator and clinical owner. Disabling the whole app, changing a flag or emptying a policy file is not a complete rollback.
Monitoring And Handoff
Before writers resume, record named owners and actual log/dashboard locations:
| Signal | Stop or investigate |
|---|---|
| Occupancy or clinical-history constraint failure | Stop new intake; preserve active care and investigate |
| Required audit failure | Confirm fail-closed result and no committed effect |
| Deadlocks or lock waits | Compare with rehearsed limits; investigate repeated failures |
| Unexpected 403/404 or unavailable source | Check current identity, scope, grants and source; do not grant broad access |
| Unresolved command | Retain original key; reconcile before any replacement |
| Baseline delta or duplicate clinical effect | Stop and reconcile privately |
Record the deployment decision, writer-resume approval and evidence links in the staging handoff. Deployment, QA acceptance, clinical approval and pilot acceptance are four separate decisions. Keep the tracking issue open for QA.