Skip to main content

Upgrades & Migrations

This page details §11 of the architecture (ADR-010). One release train serves every cell, pooled and dedicated; dedicated cells simply sit in a later wave.

1. Release train​

Day (IST)Activity
ContinuousTrunk-based merges to main; every merge deploys to dev and staging automatically
Tuesday 12:00Release cut: the latest green staging digest becomes the release candidate rYYYY.WW (for example r2026.44). Release notes drafted from PR labels
Tuesday–WednesdayUAT smoke by QA; payroll-labelled changes get a payroll SME review
Wednesday 21:00Wave 0 (canary cell and canary tenants)
Thursday 21:00Wave 0 exit gate (24 h) → wave 1 (pooled cells, smallest first)
Saturday 21:00Wave 1 exit gate (48 h) → wave 2 (dedicated cells, each in its maintenance window over the following week)
MondayRelease retrospective if any wave was halted

Deploys happen at 21:00 IST because Indian SME traffic is lowest in the evening, while the team is still awake; night shifts still punch, so deploys must be zero-downtime regardless.

Change classes

ClassExamplesPath
StandardFeatures behind flags, UI changes, additive API changesWeekly train
Payroll-sensitiveAnything under packages/payroll-engine, pay module, statutory generators, attendance day computer, leave accrualWeekly train outside the payroll window only; golden tests mandatory; payroll SME approval
PatchBug fix with a small diff, including in the payroll windowPatch release rYYYY.WW.N through all waves with shortened soak (wave 0: 2 h, wave 1: 4 h) and a second reviewer
EmergencySecurity fix or SEV1 fixRelease manager may skip soaks; postmortem explains why
ConfigurationStatutory packs, flags, entitlementsNot code; published through the control plane (sections 8 and 9)

2. Expand/contract migration playbook​

Rule: at any moment, the running code version N and the previous version N−1 must both work against the current schema. No destructive change ships in the same release as the code that stops using the old structure.

PhaseReleaseSchemaCode
1. ExpandNAdd new columns or tables (nullable or with defaults), new indexes CONCURRENTLYOld code ignores them
2. Dual writeN (or N+1)—Write both old and new; read old
3. BackfillBackground job after N is fully rolled out—Batched job, idempotent, per tenant, throttled; progress visible in the admin console
4. Switch readsN+1—Read new (behind a flag per cell); still write both
5. ContractN+2 or later, after all cells run N+1 for at least one full weekDrop old columns, add NOT NULL constraints (validated with NOT VALID then VALIDATE CONSTRAINT)Stop writing old

Migration safety checklist (CI enforces the first four)

  • No DROP, RENAME or type change of a column still read by the previous release (a migration linter compares with N−1's Prisma schema).
  • Indexes on large tables are created CONCURRENTLY (in a separate non-transactional migration step).
  • New tenant-owned tables have tenant_id, an RLS policy, FORCE ROW LEVEL SECURITY, and composite foreign keys including tenant_id (the RLS coverage test fails otherwise).
  • lock_timeout set (for example 5 s) so a migration fails fast instead of blocking traffic.
  • Backfills run as jobs, never inside the migration.
  • Rollback note: what happens if this release is rolled back after the migration ran (answer must be "nothing breaks").

Worked example: encrypting PAN at the application level​

Today emp_employee.pan is plain text (protected by RDS encryption at rest only). Target: pan_enc (envelope-encrypted bytes), pan_last4 (for masked display and search) and pan_hash (keyed HMAC for duplicate detection). See security.

Release N: expand + dual write

-- migration 2026_11_04_01_pan_encryption_expand.sql
ALTER TABLE emp_employee
ADD COLUMN pan_enc bytea,
ADD COLUMN pan_last4 char(4),
ADD COLUMN pan_hash bytea;
-- migration 2026_11_04_02_pan_hash_index.sql (non-transactional)
CREATE INDEX CONCURRENTLY IF NOT EXISTS emp_employee_tenant_pan_hash_idx
ON emp_employee (tenant_id, pan_hash);

Code in N writes pan, pan_enc, pan_last4 and pan_hash on every create or update; reads still use pan.

Backfill job (after N is on all cells)

// runs per tenant with tenant context set; 500 rows per batch; resumable by cursor
for await (const batch of employeesWithoutPanEnc({ tenantId, batchSize: 500 })) {
const dek = await keyring.dataKey(tenantId);
await db.$transaction(batch.map((e) => db.emp_employee.update({
where: { tenant_id_id: { tenant_id: tenantId, id: e.id } },
data: { pan_enc: encrypt(dek, e.pan), pan_last4: e.pan.slice(-4), pan_hash: hmac(tenantId, e.pan) },
})));
}

Release N+1: switch reads. Reads use pan_enc (decrypt only when the caller holds emp.sensitive.read), lists use pan_last4. A verification job confirms every row with pan has a matching pan_enc (decrypt and compare a sample of 1% per tenant, all rows for small tenants). Writes still populate pan in case N+1 is rolled back.

Release N+2 (one week after N+1 is everywhere): contract

-- migration 2026_11_25_01_pan_encryption_contract.sql
UPDATE emp_employee SET pan = NULL WHERE pan_enc IS NOT NULL; -- batched by job in practice
ALTER TABLE emp_employee DROP COLUMN pan;

Because PAN plaintext also exists in older backups, the contract release notes record the date after which backups no longer contain plaintext PAN (35 days later, after PITR and snapshot age-out).

3. Payroll-window freeze​

RuleDetail
Window25th of the month 00:00 IST to the 5th of the next month 23:59 IST (architecture §11)
FrozenPayroll-sensitive change class (section 1); statutory generator code; migrations touching pay_*, att_* day tables or lve_* ledgers
AllowedStandard changes behind flags that are off for payroll paths; patches with payroll SME + release manager approval; statutory pack publishing (it is data with an effective date)
Tenant-specific windowsTenants with a non-standard payroll calendar (for example payroll on the 15th) set their window in the control plane; the orchestrator respects the union for their cell's payroll-sensitive changes
EnforcementThe release job checks the change class labels; the orchestrator refuses payroll-sensitive digests in the window unless an override with two approvals is recorded
Also frozenInfrastructure changes with downtime risk (instance class changes, major version upgrades, Stage A → B cutover), OS patching, DR drills

4. Per-cell rollout orchestration​

The control plane's rollout orchestrator is the only component that deploys to production cells.

  • Pre-checks: latest snapshot under 26 h old; no tenant payroll run in calculating state; cell error budget above 25%; maintenance window open (dedicated cells); no active SEV1/SEV2 in the cell.
  • Concurrency: one cell at a time in wave 1 until there are more than 5 pooled cells, then up to 2 in parallel. A halt in any cell halts the wave.
  • Payroll jobs during deploy: workers drain gracefully (finish current job, up to 10 minutes, then release the lease). Payroll calculation is deterministic and idempotent, so a retried job produces the same result.
  • Record: the cell's version, digest and migration version are written to the cell registry; tenants see "Updated to r2026.44" with release notes in-app.

5. Rollback​

SituationActionData impact
Bad code, migration was expand-onlyRedeploy previous digest through the orchestrator (rollback command), cell by cellNone
Bad backfillStop the job (flag), fix, re-run (idempotent); the old column is still authoritative until N+1None
Bad contract migrationShould not happen if the checklist was followed. Restore the dropped data from PITR into a scratch DB and re-populate (backup and DR)Possible; SEV1
Catastrophic data corruption in a cellFull-cell PITR (backup and DR runbook)Writes after the restore point must be replayed
Bad mobile OTA updateRepublish the previous update group to the channelNone
Bad mobile store buildHalt staged rollout; ship a fix; raise the minimum version only after the fix is liveNone
Bad statutory packPublish a corrected pack version with the same effective date; affected unlocked runs recalculate; locked runs get an adjustment in the next runCommunicated to payroll managers

Rule: we never roll back across a contract migration. Roll forward instead.

6. Mobile versioning​

ItemPolicy
Version schememajor.minor.patch (store version) + build number; EAS runtime version from the native fingerprint
Support windowThe API supports every app version released in the last 90 days and at least the last two minor versions
Minimum versionmobile.min_supported per platform in the control plane. Raised only after the replacement has been in stores for 14 days and adoption is over 90% (from telemetry)
Gate behaviourBelow minimum: 426 Upgrade Required with a store link, but offline punches already queued are still accepted if their payload schema version is supported. Between minimum and recommended: soft banner
OTAJS-only fixes; never changes to permissions, background location, native modules, or the offline queue schema
Kiosk devicesKiosk tablets are often unattended; MDM or Play managed updates are recommended. Kiosk builds get a 30-day longer support window

7. API deprecation​

  • The public API /v1 changes additively only: new endpoints, new optional fields, new enum values (clients must tolerate unknown values, as stated in the API overview).
  • A breaking change means /v2 for that resource; /v1 keeps working for at least 6 months after /v2 is generally available (12 months for Enterprise contracts that require it).
  • Deprecated endpoints return Deprecation and Sunset headers and a Link to the migration guide. The developer portal and a monthly email to API client owners list deprecated endpoints still in use by that tenant (from API logs).
  • Webhook payload versions follow the same rule; a subscription pins a payload version.
  • The internal web API ships with the SPA in the same deploy and has no external compatibility promise, but must still tolerate the previous SPA build for one release (users with an open tab).

8. Statutory-pack updates​

Statutory changes (for example a new ESI wage ceiling, a state PT slab revision, a budget change to TDS slabs) are published as pack versions, not code releases. The publishing workflow is in administration.

StepDetail
1. MonitorThe payroll SME tracks EPFO, ESIC and CBDT circulars, state gazettes and the Union Budget; customers can report changes
2. DraftNew pack version with effective date; values marked "verify" until reviewed
3. Golden testsAdd or update golden files for the change (for example an employee at the new ceiling) (testing strategy)
4. StagingRecalculate sample tenants' last run with the new pack; compare outputs
5. PublishEffective date set; tenants on "inherit" pick it up automatically from that date; tenants on "review" see a task
6. Retroactive changesIf the effective date is in the past (common with government notifications), the next payroll run computes arrears or adjustments; locked runs are never changed
7. CommunicateIn-app announcement to payroll managers and the change log

When a statutory change needs code (a new kind of rule, not new values, for example a new file format), it is a payroll-sensitive release, planned outside the payroll window, and the pack schema version is bumped so old runs keep using old evaluators.