Skip to content

Deployment — Backup & Rollback

What holds the NKO ERP's state, how to back it up so it can be restored consistently, and how to roll a release back — including the cases where a rollback cannot be done without restoring data.

Nothing in the repository automates any of this: there are no backup jobs, scripts or scheduled tasks. Everything below is procedure, derived from where the application actually keeps its data.

What holds state

Store What is in it How to back it up
NKO_DB Everything except the warehouse: employees and logins, permissions, HR, payroll, procurement, finance, recruitment, training, notifications SQL Server backup — together with NKO_DB_Warehouse
NKO_DB_Warehouse Stock, assets, maintenance, destruction, waybills SQL Server backup — together with NKO_DB
Hangfire database Queued and failed background jobs — the warehouse Excel exports Optional. Hangfire recreates its schema (PrepareSchemaIfNecessary = true); losing it loses only in-flight exports.
AWS S3 — one folder per feature (StorageConfigs:Systems) Every uploaded document: attachments, contracts, signatures, RFQ quotes, GRN and PO files Enable bucket versioning. The application calls DeleteFile, so a deleted object is gone unless versioning keeps it.
C:\upload\nko\files\userImages\ Employee profile photos — saved to local disk, not to S3. The database stores only the file name. File backup. Lose this folder and every photo is gone, with the database still pointing at the missing files.
C:\upload\nko\templates\ e-sign.pdf, contract.xlsx File backup — they cannot be regenerated by the application
Other C:\upload\… folders Generated Excel exports Not needed — regenerated on demand
wwwroot\Rotativa\ wkhtmltopdf binaries Keep a copy — they are not in the repository (R5-3)
Server configuration appsettings.json or environment variables — connection strings, AWS keys Keep a secure copy; it is not in the repository

Back up the two databases together

The core and warehouse databases have no foreign keys between them, but they refer to each other anyway:

  • warehouse rows store core ids — UserId, CreatedBy, ProjectId, GRN numbers — as plain integers;
  • BGRN.TransfareToWarehouse writes both databases in one operation — stock and assets into NKO_DB_Warehouse, then the IsTransfareToWarehouse flag into NKO_DB.

That transfer is not atomic: it calls ware_db.SaveChangesAsync() and then db.SaveChangesAsync() as two separate commits, and there is no transaction anywhere in the solution (R7-18). If the two backups are taken at different moments, a restore can produce GRNs marked as transferred with no stock behind them, or stock that points at a GRN still awaiting transfer — which the next transfer would then duplicate.

So:

  • back up both databases in the same window, ideally with the site offline (app_offline.htm) so nothing is written between the two;
  • or run both in the full recovery model with log backups, and restore both to the same point in time.

Before every release

  • [ ] Full backup of NKO_DB and NKO_DB_Warehouse, taken together.
  • [ ] Copy of C:\upload\nko\files\userImages\ and C:\upload\nko\templates\.
  • [ ] Record the last applied migration of each context:

    SELECT TOP 1 MigrationId FROM NKO_DB.dbo.__EFMigrationsHistory           ORDER BY MigrationId DESC;
    SELECT TOP 1 MigrationId FROM NKO_DB_Warehouse.dbo.__EFMigrationsHistory ORDER BY MigrationId DESC;
    
  • [ ] Keep the previous publish package, so the old build can be redeployed without rebuilding.

  • [ ] Read the new migration scripts for DROP TABLE / DROP COLUMN — see below.

Rolling back

flowchart TD
    A{"Did the release apply migrations?"}
    A -- no --> B["Redeploy the previous package"]
    A -- yes --> C{"Does any of them drop a table or column?"}
    C -- no --> D["Revert the schema, then redeploy the previous package"]
    C -- yes --> E["Restore BOTH databases from the pre-release backup,<br/>then redeploy the previous package"]

Code only

Redeploy the previous package with Web Deploy, keeping extra files on the server (see Build & Publish). The schema is unchanged, so nothing else is needed.

With non-destructive migrations

Revert each context to the migration recorded before the release. The generated script can be reviewed before it runs:

dotnet ef migrations script <NewMigration> <PreviousMigration> --context NKO_DBEntities1 `
    --project ../../NKO.Lib/NKO.DataAccess --startup-project . -o rollback-core.sql

This runs each migration's Down() method in reverse order. Then redeploy the previous package.

With destructive migrations — restore instead

A migration that drops a table or column has a Down() that re-creates the schema — but the data is gone. Running it "succeeds" and leaves empty tables and null columns. For these the only real rollback is to restore both databases from the pre-release backup and redeploy the previous package. Anything entered between the release and the restore is lost, so decide quickly.

Which migrations destroy data

Nine of the 23 existing migrations drop tables or columns — all in the core context; none of the five warehouse migrations do:

Migration Tables / columns dropped
20250420061112_removeUneededEntitites 10
20250420064809_removeUneededEntitites2 1
20250507120311_removeUserlink 2
20250619063304_jobFixedAdded 1
20250619064002_jobFixedAdded2 2
20250718162830_removeTravel 12
20250718174446_removeTracking 5
20260729094813_ProjectFinanceSODOnlyOne 3
20260730200327_finSODRemoved 4

These are already applied, so they matter when rolling a database back past them — for example to run an older build against a copy of production. Future migrations should be checked the same way before every release.

After a restore

  • [ ] Restore both application databases from the same backup set.
  • [ ] Clear the Hangfire database, or expect its queued jobs to reference rows that no longer exist.
  • [ ] Redeploy the build that matches the restored schema — the last applied MigrationId must match the build's migrations.
  • [ ] Run the smoke test.
  • [ ] Check that open requests still have a valid NextUserId, or run Refresh Next users with All at /Admin/TriggerNextUser rather than waiting for 03:00 (and see R8-11).