Skip to content

Operational Controls Cutover

Applies to: All subscriptions — deployments upgrading from a build that had the GSTIN Operational Matrix

Purpose

Take an existing deployment to the company-default model without changing how any registration behaves.

Three facts decide the whole procedure:

  1. The order is fixed: database migrations, then the build, then the reconciliation. The build refuses to start without the new tables. The reconciliation needs the new build.
  2. Nothing changes for any registration at any step. Until the reconciliation runs, every value resolves exactly as before. The reconciliation itself is proved, for every registration and every control, to leave the value in force unchanged — and undoes itself if the proof fails.
  3. If you must roll the build back after the reconciliation, restore the data first. An older build reads per-GSTIN settings only. Against reconciled data it would ignore every company default and silently change module behaviour.

Audience

Whoever deploys Complifly: infrastructure teams and database administrators.

Prerequisites

  • Shell access to the application server, in the gst-backend directory, with the application's database configuration in place. Every command below runs there.
  • A database login that may create tables and security policies, for step 2.
  • A database backup taken immediately beforehand — see Backup and Restore.
  • A maintenance window is not required for steps 2–5. Run the reconciliation (steps 6–8) at a quiet time: it holds a short transaction on two small tables.

Reference

Deployment sequence

# Step Command Verify before continuing
1 Back up the database Your backup procedure The backup restores
2 Apply the schema, in this order node scripts/run_migration.js migrations/20261002a_sys_company_master.sql, then 20261002b_company_feature_config_and_history.sql, 20261002c_descope_legacy_table_fks.sql, 20261003a_feature_migration_tracking.sql, 20261003b_sys_feature_definition.sql, 20261003c_feature_definition_write_role.sql Each prints its own success lines and no error. All are safe to re-run
3 Check every registration has a company node scripts/validate_company_orphans.js Reports no orphaned company
4 Deploy the build and start it Your deployment procedure The start-up log contains catalogue in sync with DEFAULT_FEATURES, and /api/health returns 200
5 Confirm nothing changed Sign in as a Super Admin, open Admin & Logs > Operational Controls > Effective View for a registration you know Every value is what that registration had before. Every source reads GSTIN or Platform — there are no company defaults yet
6 Preview the reconciliation node scripts/opcontrols_phase3.js plan --by <your name> It prints every decision it would make and ends with the proof PASSED and "rolled back — nothing was changed"
7 Run it node scripts/opcontrols_phase3.js migrate --batch <batch id> --by <your name> All three proofs report PASS. Keep the report it prints
8 Spot-check Effective View for three registrations named in the report Values unchanged; sources now include Company

Choose a batch id you will recognise later, for example P3-PROD-20261015. It labels the backup, the report and every history row the reconciliation writes.

Steps 6–8 need not follow step 5 immediately. The build runs correctly on an unreconciled database for as long as you need.

If the build does not start at step 4

Start-up message Cause Action
sys_feature_definition could not be read Step 2 was not completed Run the migration it names, then start again
DEFAULT_FEATURES and dbo.sys_feature_definition have diverged, followed by a list The build and the catalogue table are from different versions: a control the build reads has no row, or its default, type or scope differs Run the catalogue migration that ships with this build. Do not edit the table by hand
A schema guard error naming a table or policy A migration in step 2 did not complete Re-run it — each is safe to re-run

If your process manager restarts the application automatically, it will retry a failed start again and again — every few seconds — while you repair the catalogue. Stop the application first (for PM2: pm2 stop complifly-api), repair, then start it. A start that failed the check changes nothing in your data; it only repeats the same refusal.

This check is deliberate. A server that offered controls its code does not agree with would let an administrator set a value that is silently read as something else.

The check is one-directional for extra rows. If the catalogue table holds a control that this build does not know — which happens after you roll back to an older build, or when a newer release's migration has already run — the server starts. It logs a warning naming the extra controls, records a FEATURE_CATALOGUE_DRIFT event, and hides those controls from Operational Controls so nobody can set a switch the build ignores. You do not need to delete the rows to roll back.

If a server must be started while the catalogue is being repaired (Complifly support or your platform administrator only — this is a recovery step, not a setting): set the environment variable FEATURE_CATALOGUE_CHECK=warn, restart, repair the catalogue, then remove the variable and restart again. While it is set the server starts despite a mismatch, and Operational Controls may show a control whose default or scope disagrees with the product, so a value set during that time can be read as something else. The default is strict; any other value is treated as strict. Every start with the override set is recorded in the platform event log.

What the reconciliation does

Before it, every setting is stored per GSTIN. A company with ten registrations and a module switched on for all of them holds ten identical rows — and nothing says which are deliberate exceptions.

For each company and each control it finds the value most of the company's registrations have. Where that differs from the platform default it becomes the company default. Each per-GSTIN row is then either removed, because it now merely repeats what the registration inherits, or kept, because it is a genuine exception.

Decision in the report Meaning
PROMOTE_COMPANY_DEFAULT Most registrations share a value that is not the platform default — it becomes the company default
REMOVE_GSTIN_ROW The row repeated what the registration now inherits — removed
RETAIN_GSTIN_ROW A genuine exception — kept as an override
CREATE_GSTIN_ROW A registration that held the platform default in a company whose default is now different — an override is created so its value does not change
RETAIN_PINNED_ROW Redundant, but the registration has an ERP connection and the row keeps Excel Upload or Invoice Generation where it is — kept
MOVE_TO_COMPANY A company-wide control (TDS, Complifly Agent) that was stored on a GSTIN — moved to the company
SKIP_NO_COMPANY A registration with no company — left untouched

How correctness is proved

For every registration and every control, the value in force before must equal the value in force after. That is checked three times:

Proof When If it fails
In the database, inside the reconciliation's own transaction Before anything is committed The transaction rolls back. Nothing changed
Through the application's own resolver Immediately after commit The script restores the backup automatically
Row counts against storage metadata After commit Reported as a failure — restore manually (below)

"The script ran without error" is not the evidence. The three PASS lines are.

Rollback sequence

Situation Do this
A proof failed during step 7 Nothing — the reconciliation rolled itself back or restored itself. Keep the output and investigate before retrying
The reconciled settings are wrong node scripts/opcontrols_phase3.js restore --batch <batch id>. The build keeps running; values resolve from per-GSTIN rows again
The build must be rolled back after step 7 First run restore as above. Then roll the build back
The build must be rolled back before step 7 Roll the build back. The new tables are ignored by the old build
The new tables must be removed Roll the build back first — it will not start without them. Then run the _rollback.sql file beside each migration, in reverse order

restore puts back only rows nobody has changed since the reconciliation. A setting an administrator changed afterwards is reported as a conflict, not overwritten — decide each one by hand.

Running it again

The reconciliation can be run more than once. On an already-reconciled database, plan proposes only what has changed since. Run it again after a bulk import or a seed script that wrote per-GSTIN settings directly.

Validation

Check Method Pass condition
Security policies exist on the new tables Query sys.security_policies sys_company_rls, sys_company_feature_config_rls, sys_feature_config_history_rls, sys_feature_migration_rls present and enabled
The build agrees with the catalogue Start-up log catalogue in sync with DEFAULT_FEATURES
The preview was clean Step 6 output Proof PASSED
The reconciliation was correct Step 7 output Three PASS lines
Rollback works In a non-production environment: migrate, then restore Both settings tables identical to the backup
Nothing changed for users Effective View spot-checks Values as before
A company default reaches its registrations Change one harmless control at company level; open Effective View for a registration without an override The new value, source Company, on the next request

Troubleshooting

Symptom Cause Action
migrate stops with "Feature configuration changed after this batch was prepared" A setting was changed between the snapshot and the reconciliation Nothing was changed. Run migrate again with a new batch id
migrate stops with "This batch id has already been prepared" The batch id was used before, including by a run that stopped early Use a new batch id
migrate prints "The application resolver DISAGREES with the SQL snapshot — not migrating" The application and the snapshot disagree about a current value, usually a cached value up to a minute old Nothing was changed. Wait a minute and run migrate again with a new batch id. If it repeats, stop and contact support with the table it printed
After reconciliation a control shows "changed by MIGRATION:<batch>" The reconciliation's own audit trail Expected
restore lists conflicts Those settings were changed after the reconciliation Review each; they were deliberately left as they are
TDS or the Complifly Agent is off for a company after step 4 but before step 7 It should not be — the build reads the old per-GSTIN setting for these until the reconciliation moves it Check Effective View; if it persists, do not proceed — contact support with the output of plan
Operational Controls shows "Control catalogue load failed" The browser is talking to a server still running the previous build Confirm every application server was restarted on the new build