Use this guide to configure a local Microsoft SQL Server + TypeORM database, apply migrations and seeds, browse data, and run database checks.
See also:
For host-side development, create the local environment file if it does not already exist:
cp .env.sqlserver.example .env.sqlserverFor the default devcontainer profile, create its environment file if it does not already exist, then rebuild the devcontainer:
cp .devcontainer/.env.example .devcontainer/.envStart the local SQL Server Developer container with:
npm run db:upStop it with:
npm run db:downThe default Compose file is docker-compose.sqlserver.yml.
For host-side development, it publishes SQL Server on 127.0.0.1:1433
(loopback only) and persists data in a named Docker volume. The separate
devcontainer Compose profiles publish their SQL Server port on all host
interfaces; restrict access through the host's network controls when using
those profiles.
If your machine already has a local SQL Server using 1433, override
SQLSERVER_HOST_PORT in .env.sqlserver or .devcontainer/.env. Match
DB_PORT to that published port for host-side clients; clients inside the
devcontainer still connect to db:1433.
For explicit remote access to the host-side database, use an SSH tunnel from the client workstation to a trusted development host:
ssh -N -L 127.0.0.1:11433:127.0.0.1:1433 user@development-hostConnect the client to 127.0.0.1:11433 while the tunnel is open. Replace the
remote 1433 with the published SQLSERVER_HOST_PORT when it differs. Use an
authorized SSH account, verify the host key, and restrict SSH access to
trusted networks or a VPN. Keep the SQL Server port closed to public ingress.
The local SQL Server workflow uses encrypt=true together with
trustServerCertificate=true by default. That is intentional for local
development because the SQL Server container presents a self-signed
certificate unless you add your own trusted certificate chain.
db:up and db:down pass .env.sqlserver to Docker Compose. The admin CLI
loads .env, .env.development, .env.local, then .env.development.local
from the working directory. Later files override earlier files, but variables
already in the process environment take precedence over all four files.
Run the commands from the repository root.
The Next.js development runtime uses its standard environment-file loading;
neither it nor the admin CLI automatically loads .env.sqlserver. Put host
connection overrides in .env.development.local or the process environment.
When changing the container password or published port in .env.sqlserver,
keep the client settings in sync. The SQL integration and specification
performance npm commands explicitly load .env.sqlserver through dotenv.
The requirement-list performance script uses the admin CLI's four-file loader.
The default and elevated devcontainer Compose stacks inject variables from
.devcontainer/.env into the development container. Those process variables
take precedence over workspace environment files, including local overrides.
Use command-scoped variables when temporarily targeting another database.
The SQL Server admin scaffold uses the master database for readiness checks
and reset/setup bootstrap steps, so db:setup can create kravhantering
even when that database does not exist yet.
Local/dev SQL Server connection strings are normally derived in code from:
DB_HOST=...
DB_PORT=...
DB_NAME=...
DB_USER=...
DB_PASSWORD=...
DB_READONLY_USER=...
DB_READONLY_PASSWORD=...
DB_ENCRYPT=...
DB_TRUST_SERVER_CERTIFICATE=...The committed development defaults use kravhantering_app for the application,
kravhantering_job for migration and required seed, and sa only for
principal/database bootstrap. db:setup creates and maps those principals
idempotently. Password rotation is outside db:setup's scope.
For the read-only login, avoid passwords that contain the login name
(readonly) because SQL Server password policy can reject them even when they
otherwise look complex.
DATABASE_URL and DATABASE_READONLY_URL are the canonical explicit
connection string contract when you need to override the derived local/dev
settings. The Next.js runtime and admin CLI both use these names. Migration
connections replace the credentials in DATABASE_URL with
DB_MIGRATION_USER and DB_MIGRATION_PASSWORD.
The canonical runtime contract is:
DATABASE_URL=... # runtime identity
DATABASE_READONLY_URL=...
DB_MIGRATION_USER=... # migration identity name
DB_MIGRATION_PASSWORD=... # migration identity secretApplication runtime and schema migration use separate SQL Server credentials.
The release-versioned manifest in
typeorm/runtime-permission-manifest.mjs
lists exact object/operation grants, including protected column-scoped updates;
new tables receive nothing implicitly through kravhantering_runtime.
db:migrate applies migrations and then reconciles direct grants and verifies
DB_RUNTIME_USER membership. If that user belongs to db_datareader or
db_datawriter, reconciliation removes those broad memberships only after the
custom contract verifies. It does not alter other user roles or direct user
permissions. Verification fails when those permissions give the runtime user
effective schema-migration or protected-audit mutation access. Reconciliation,
broad-role removal, and final effective verification are atomic, and a
role-only runtime login cannot run TypeORM migrations.
Use npm run db:permission-status for secret-free JSON evidence or
npm run db:permission-reconcile for an explicit repair. Both report the
manifest version/digest, role presence, missing/unexpected grants, managed-user
presence and membership, broad-role memberships, and unexpected parent roles.
A compatible report has empty legacyRoles and
prohibitedEffectivePermissions arrays for every managed runtime user.
The Next.js runtime builds one shared TypeORM DataSource per process. The
runtime DataSource keeps SQL Server behavior explicit:
synchronizeis disabled.- TypeORM high-level where clauses throw on
nullorundefinedvalues. - SQL Server date/time values use UTC.
- SQL Server transactions use
XACT_ABORTthroughabortTransactionOnError=true. - TypeORM transactions default to
READ COMMITTEDthrough the DataSource-levelisolationLevel. DAL paths that need stronger ordering passSERIALIZABLEexplicitly at the transaction call site. - Connection and request timeouts are explicit.
- Connection pool sizing and idle/acquire timeouts are explicit.
The runtime does not set SQL Server options.connectionIsolationLevel for
out-of-transaction reads. SQL Server and the tedious driver already default
new connections to READ COMMITTED, and TypeORM documents that connection
isolation settings may not be reliably preserved across pooled connection reuse.
Treat connectionIsolationLevel as a new-connection default, not as a
checkout reset; a reused session can retain a prior transaction's isolation
level.
Runtime pool defaults are conservative for a single app process:
| Variable | Default | Purpose |
|---|---|---|
DB_POOL_MAX |
10 |
Maximum SQL Server connections in the TypeORM runtime pool |
DB_POOL_MIN |
1 |
Minimum SQL Server connections kept in the TypeORM runtime pool |
DB_POOL_IDLE_TIMEOUT_MS |
30000 |
Time before an idle runtime pool connection is eligible for eviction |
DB_POOL_ACQUIRE_TIMEOUT_MS |
15000 |
Time a runtime request may wait for a pool connection |
| Command | Purpose |
|---|---|
npm run db:up |
Start the local SQL Server Developer container |
npm run db:down |
Stop the local SQL Server Developer container |
npm run db:wait |
Poll the configured SQL Server endpoint until it responds |
npm run db:health |
Run a simple SELECT 1 health probe |
npm run db:browse |
Print a read-only VS Code SQLTools connection block |
npm run db:setup |
Wait, reset, bootstrap principals, migrate and reconcile permissions, seed required + demo profiles, and configure the read-only login |
npm run db:migration-status |
Report expected, observed, pending, and unknown TypeORM migrations as JSON |
npm run db:migrate |
Run TypeORM migrations and reconcile runtime permissions |
npm run db:seed:required |
Apply only required system and lookup seed data |
npm run db:seed:demo |
Reset non-required rows, then apply optional demo, smoke-test, guide, and integration seed data |
npm run db:reset |
Drop and recreate the database |
npm run test:sql-integration |
Reset a dedicated test database and run focused SQL Server invariant tests |
Seed execution requires a DataSource that creates a transaction-owning QueryRunner, or a QueryRunner/EntityManager already bound to an active transaction. Query-only executors without an active transaction are rejected before seeding so lookup locks remain held through the associated insert and lifecycle transitions.
db:setup drops and recreates the configured database. Use it only for
disposable development data; use db:migrate to preserve existing data.
For an empty production-like database, first run
node scripts/db-sqlserver-admin.mjs bootstrap with bootstrap administrator
credentials to create the database and principals if they do not exist.
Then run npm run db:migrate plus npm run db:seed:required.
Add npm run db:seed:demo only when you need the
local development, integration-test, guide, or smoke-test fixtures. The demo
profile is destructive: it clears non-required data before reseeding the
current fixtures.
Run database concurrency, constraint, transaction, rollback, and pagination invariants with:
npm run db:up
npm run test:sql-integrationThe npm command loads .env.sqlserver; existing process variables take
precedence. The suite replaces the resolved database name with
<database>_sql_integration_tests. It resets and
migrates only that dedicated database. Set
SQLSERVER_INTEGRATION_TESTS_URL to provide an explicit test database URL, or
SQLSERVER_INTEGRATION_TESTS_DB_NAME to override only the derived database
name. Both overrides select a database that the suite will reset; use only
a disposable test database.
To run one contract, append its test file, for example:
npm run test:sql-integration -- \
tests/sql-integration/rfi-assessments.sqlserver.test.tsThe ordinary npm test command excludes tests/sql-integration/. The
Integration Tests workflow runs the SQL suite as a separate required job
against its own test database.
The requirement list SQL path has a required SQL Server performance check for
listRequirements and production-style cursor continuation. It seeds roughly
10,000 PERF-* requirements with two to four versions each and compares
first-page and deep-page queries within that fixture.
Use the regular check when you want to verify that the current branch still fits the committed baseline:
npm run db:setup
npm run perf:requirements-listUse the update command only when the measured baseline is intentionally changing, for example after a deliberate query rewrite, a measured index change, a SQL Server image/runtime change, or a fixture-size change:
npm run db:setup
npm run perf:requirements-list:update
npm run perf:requirements-listThe update command rewrites
tests/performance/requirements-list-baseline.json. Review the diff and
commit it with the code or schema change that justifies the new numbers. Do
not use it to silence a one-off noisy failure; first rerun the check on an
idle, comparable SQL Server environment.
The baseline has profiles because SQL Server can choose different execution
plans in GitHub Actions and in local containers, especially on Apple Silicon.
CI runs with the strict ci profile from the top-level thresholds object.
Normal local runs use the developer profile from thresholdProfiles, which
allows the higher logical-read plan seen in local Mac containers while keeping
the CI gate tighter. Override the profile explicitly when needed:
PERF_REQUIREMENTS_BASELINE_PROFILE=ci npm run perf:requirements-list
PERF_REQUIREMENTS_BASELINE_PROFILE=developer npm run perf:requirements-listperf:requirements-list:update updates the active profile. On a normal local
machine that means the developer profile; in CI, or when
PERF_REQUIREMENTS_BASELINE_PROFILE=ci is set, it updates the top-level CI
thresholds. Commit profile changes only when that environment was intentionally
remeasured.
Run baseline updates against an isolated local or CI-like SQL Server Developer
container, not a shared or production database. The script creates or refreshes
the PERF-* fixture rows in the target database. Use a disposable database for
the db:setup examples above: that command resets all existing data.
The script captures actual execution plans and measures warm-cache samples.
Results and .sqlplan files are written under
test-results/requirements-list-performance/.
The baseline file contains threshold counters:
sampleCountandwarmupCount: how many measured and warm-up runs were used per scenario.maxMedianDurationMs: maximum allowed median elapsed time across measured warm-cache samples.maxP95DurationMs: maximum allowed 95th-percentile elapsed time. This catches occasional slow samples better than the median.maxLogicalReads: maximum allowed SQL Server logical page reads reported bySTATISTICS IOfor the list query. This is usually the most stable regression signal because it tracks work done by SQL Server rather than host CPU noise.allowSpills: whether execution-plan spill warnings are accepted. Keep thisfalseunless a spill has been reviewed and deliberately accepted. The console table shows this asactual/allowed, soyes/nomeans SQL Server produced a spill warning and the committed baseline does not accept it. Whenupdate-baselineis used, scenarios with measured spills are recorded withallowSpills: true; review the saved.sqlplanfiles before committing that change. In practical terms, a spill means SQL Server needed more working memory for a query step than it was granted, so it usedtempdbscratch space instead. The query result is still correct, but the plan may be slower or more sensitive under load. Treat a spill as a performance warning: fix or investigate it when it comes with high duration, high logical reads,tempdbpressure, or a new plan change; accept it only when the measured timings and reads are still comfortably inside the baseline budget.maxMissingIndexImpact: maximum accepted SQL Server missing-index impact in the captured plan. A high value is not proof that the suggested index is correct, but it is a prompt to inspect the plan before raising thresholds.
Only refactor the query, add indexes, or introduce projections after the captured SQL Server execution plans and baseline results show a real bottleneck. If the update lowers thresholds after an improvement, keep the stricter baseline so future regressions are caught.
Run the mixed requirements specification campaign against a migrated, seeded
local SQL Server database. This command loads .env.sqlserver and creates and
cleans up fixtures in the configured database; it does not derive a separate
test database. Use a disposable target:
npm run perf:specification-itemsThe blocking CI job runs the same command for pull requests and pushes to
main. It creates isolated 70/30 and 20/80
library/specification-local fixtures at 200 and 500 items. For every supported
sort and direction it traverses the unchanged result twice and requires exact
stable-reference order with no missing or duplicate rows.
The same run records one Requirement ID traversal for both mixes at 1,000 items. Those 1,000-item results are diagnostic artifacts only: they have no duration or logical-read regression threshold and do not change the supported 500-item sizing target.
The committed regression thresholds in
tests/performance/specification-item-pagination-baseline.json are separate
from product-decision response budgets. The campaign records warm complete
traversal duration and captures representative actual plans, logical reads,
spills, key/RID lookups, and missing-index evidence under
test-results/specification-item-pagination-performance/. CI retains those
artifacts on failure.
Keep the existing membership indexes while warm traversals and logical reads remain inside this baseline and plans show no spill-driven or material missing-index regression. A key lookup alone does not justify another index. Replace a simple membership index with one measured covering index only when fresh 200/500 evidence exceeds the baseline and the captured plan identifies that lookup as the cause. Do not add one index per sort.
Create a new file in typeorm/migrations/ named NNNN_short_description.mjs
(zero-padded, monotonically increasing). The admin CLI auto-discovers .mjs
migration files in that directory. The numeric filename prefix
keeps discovery deterministic and reviewable, while TypeORM uses the
timestamp suffix in the migration name as the executable order. There is no
manual import list to update. Do not edit a released migration; add a new
migration and update the corresponding entities and
canonical schema reference.
Existing dev or production databases that are already at an earlier migration
will show pending migrations in npm run db:migration-status and pick up new
files on the next npm run db:migrate. Clean databases run the full set in
order via npm run db:setup. The status command accepts empty migration
history as a valid migration-job state, but app runtime readiness requires the
database to have reached the build metadata field
expectedDatabaseSchemaVersion.
Use VS Code SQLTools with a read-only login:
-
Install or enable:
mtxr.sqltoolsmtxr.sqltools-driver-mssql
-
Configure a least-privilege SQL Server login for browsing.
-
Set
DATABASE_READONLY_URLif you need an explicit browse connection override. Otherwise the tool derives the read-only connection from theDB_*values andDB_READONLY_PASSWORD. -
Run:
npm run db:browse
-
Copy the printed JSON into the SQLTools connection UI or workspace settings.
The scaffold intentionally avoids printing a real password. By default it emits
${env:DB_READONLY_PASSWORD} so the UI connection can remain read-only without
committing secrets into the repo.
Keep current dev/test seed data semantics stable unless a change is absolutely necessary. That means:
- keep stable IDs and business identifiers where feasible
- preserve scenario coverage and edge-case fixtures
- preserve ordering assumptions that tests or guides rely on
- document every unavoidable change explicitly
Seed inserts in typeorm/seed-required.mjs must be idempotent: guard with
IF NOT EXISTS (or composite-PK equivalent) and wrap identity-bearing tables
in SET IDENTITY_INSERT [table] ON/OFF so the required seed can be re-run
safely. The demo profile in typeorm/seed.mjs is reseeded from a clean
non-required data set by npm run db:seed:demo.
Put new system or lookup rows that the app needs to boot in the required profile. Put examples, screenshots, privacy exercises, Playwright fixtures, dogfood Krav and other disposable data in the demo profile.
Use only a disposable demo/test database and the migration identity. The explicitly confirmed demo clear removes non-required data, including seeded RFI suggestions that have entered review or been handled. Required lookup data remains.
For a manual CLI check, select the disposable database through the documented
DB_* configuration, then:
- Run
npm run db:seed:demoand confirm that the demo seed completes. - Run
node scripts/db-sqlserver-admin.mjs demo:clearand confirm rejection requiring--confirm-clear-non-required-data. - Run the same command with
--confirm-clear-non-required-data; confirm the reported non-required table count and a successful exit. - Run
npm run db:seed:demoagain and confirm that the current demo fixtures can be recreated.