Files
Termix/readme/DATABASE-LAYER-REFACTOR-PLAN.md
T
ZacharyZcR 1e7c9ea53c draft: database layer refactor (#1054)
* feat(sshid) - sshid.io equivalent for termix (#919)

* feat(ssh-id): database schema, migrations and field encryption

Adds ssh_identities, ssh_identity_keys and ssh_identity_ca tables (public keys
stored plaintext for the unauthenticated resolver; CA private key registered
for per-user field encryption), with UNIQUE(user_id), an index on
ssh_identity_keys(identity_id), and idempotent CREATE TABLE migrations.

* feat(ssh-id): backend API — resolver, key management, CA and certificates

Mounts /sshid (nginx route added). Public text/plain authorized_keys resolver
(+ exact /:algo filter, HTML viewer) and CA public-key endpoint; no-store +
noindex headers on every resolver response including early 404s. Authenticated
management: claim/rename/delete handle, add/import/generate/enable/delete keys,
and a per-user CA (create/rotate/delete) with pure-Node OpenSSH certificate
issuance. Audit logging on all mutations; UNIQUE races map to a precise 409.
Unit tests for key parsing and certificate signing (ssh-keygen-validated).

* feat(ssh-id): frontend panel, API client and i18n

SSH ID panel wired into the app rail and AppShell: claim handle, resolver URL +
curl one-liner, key list, generate, paste/import, CA enable/rotate/remove with
server trust command, and per-key certificate issuance. API client re-exported
through main-axios.ts; all strings i18n'd.

* style(ssh-id): align panel and resolver page with Termix theme

- Rebuild the SSH ID sidebar panel with the theme's square components
  (SectionCard / SettingRow / FakeSwitch) instead of rounded ad-hoc cards;
  use accent-brand and destructive tokens rather than raw red/green.
- Fix panel scrolling: move overflow to a block scroll container so the
  cards keep their natural height instead of being clipped.
- Restyle the public resolver HTML page (/sshid/u/:handle) to the Termix
  dark theme: square corners, #18181b/#303032 palette, #f59145 accent,
  uppercase section labels.
- Tidy copy: 'Save To Credentials' label, drop the redundant generate intro,
  and correct the generate tooltip (the key is stored when saving to vault).

* feat: rename to Termix ID, improve UI, backend inconsistencies, and general bug fixes

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* ci(deps): bump actions/checkout from 6 to 7 in the github-actions group (#922)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump the dev-patch-updates group with 11 updates (#923)

Bumps the dev-patch-updates group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@codemirror/search](https://github.com/codemirror/search) | `6.7.0` | `6.7.1` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.43.0` | `6.43.1` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.8` | `4.1.9` |
| [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `4.1.8` | `4.1.9` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.0.7` | `17.0.8` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.2` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |


Updates `@codemirror/search` from 6.7.0 to 6.7.1
- [Changelog](https://github.com/codemirror/search/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/search/commits)

Updates `@codemirror/view` from 6.43.0 to 6.43.1
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)

Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

Updates `@vitest/coverage-v8` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/coverage-v8)

Updates `@vitest/ui` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/ui)

Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3)

Updates `lint-staged` from 17.0.7 to 17.0.8
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `sharp` from 0.35.1 to 0.35.2
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.2)

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: "@codemirror/search"
  dependency-version: 6.7.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitest/ui"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: lint-staged
  dependency-version: 17.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: sharp
  dependency-version: 0.35.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump nanoid in the prod-patch-updates group (#925)

Bumps the prod-patch-updates group with 1 update: [nanoid](https://github.com/ai/nanoid).


Updates `nanoid` from 5.1.11 to 5.1.15
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/5.1.11...5.1.15)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 5.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the major-updates group with 5 updates (#926)

Bumps the major-updates group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.2.0` | `5.0.0` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.4` | `10.0.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `26.0.0` |
| [concurrently](https://github.com/open-cli-tools/concurrently) | `9.2.1` | `10.0.3` |
| [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.5.0` |


Updates `js-yaml` from 4.2.0 to 5.0.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...5.0.0)

Updates `@eslint/js` from 9.39.4 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@types/node` from 25.9.2 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `concurrently` from 9.2.1 to 10.0.3
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.2.1...v10.0.3)

Updates `eslint` from 9.39.4 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.5.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: concurrently
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(ssh): add HashiCorp Vault SSH signer authentication

* fix: small fixes to vault feature to align with Termix codebase

* chore: add view docs links for vault/termix id

* fix: file upload fails with 400 and missing schema migrations on upgrade (#929)

Two bugs introduced in v2.4.1:

1. uploadFileStream uses fileManagerApi.post() which triggers axios's
   transformRequest to JSON-serialize the FormData because the instance
   default Content-Type is application/json. Change to postForm() which
   sets Content-Type: multipart/form-data so the browser XHR sends the
   correct multipart body with boundary.

2. Two schema items added to schema.ts were not included in migrateSchema()
   in db/index.ts, causing 500 errors on existing installations upgrading
   from v2.4.0:
   - user_preferences.status_color_scheme (no such column)
   - dashboard_service_links table (no such table)

Fixes #928

Co-authored-by: sash <sash@fominykh.io>

* fix: support PuTTY PPK ssh keys (#930)

* fix: chunk large file manager uploads (#932)

* fix: route dashboard hosts by protocol (#934)

* fix: resolve tunnel endpoints reliably (#935)

* Fix Electron OIDC browser auth failures (#936)

* Allow RDP connections without stored credentials (#937)

* Sync role credential shares for OIDC users (#938)

* Fix terminal link dialog layering (#940)

* Confirm large files before opening editor (#942)

* Confirm closing active host connections (#943)

* Preserve file path case in file manager UI (#941)

* fix: preserve unicode guacamole tokens (#933)

* Persist VNC authentication settings (#944)

* Fix Guacamole websocket base path (#946)

* Promote file manager terminals to tabs (#939)

* Guard Guacamole disconnect during startup (#945)

* chore: increment ver

* feat: bitwarden ssh agent integration

* feat: serial connections support

* fix: various small bug fixes

* feat: open all sessions in a folder and terminal custom theme color support

* feat: cross host file manager clipboard and several small bug fixes

* feat: tailscale/wireguard support and added a new status state for when backend is checking status

* feat: grafana like server stats history, new alert system, ntfy/webhook support

* feat: new grid and widget based homepage function

* feat: new donate button in dashboard

* fix: alert ui incorrectly using termix css and fixed issue with alert system not loading

* chore: start database layer refactor

* docs: plan database layer refactor

* docs: audit database layer refactor phase zero

* chore: add database runtime adapter skeleton

* chore: add settings repository skeleton

* chore: add user session repository skeleton

* chore: add host credential repository skeleton

* chore: add field encryption boundary

* chore: migrate settings route slice

* chore: migrate user settings routes

* chore: migrate host metrics settings routes

* chore: migrate acme settings route

* chore: migrate terminal settings route

* chore: migrate tailscale settings read

* chore: migrate guacamole settings reads

* chore: migrate session timeout settings reads

* chore: migrate auth route settings reads

* chore: migrate host metrics settings reads

* chore: migrate startup settings reads

* chore: migrate user settings cleanup

* chore: migrate password reset settings

* chore: migrate oidc legacy settings read

* chore: migrate user route settings slice

* chore: migrate oidc state settings

* chore: migrate user login settings reads

* chore: migrate user crypto settings

* chore: consolidate startup settings defaults

* chore: consolidate database settings import export

* chore: migrate core session auth paths

* chore: migrate remaining session auth paths

* chore: migrate admin user routes

* chore: migrate user route admin checks

* chore: migrate user lifecycle routes

* chore: migrate auth user lookups

* chore: migrate oidc user routes

* chore: migrate api key repository paths

* docs: add database gray rollout guide

* chore: migrate trusted device paths

* chore: migrate user session route user lookups

* chore: add database repository rollout guard

* chore: expose repository rollout status

* chore: warn on repository rollout misconfiguration

* chore: migrate remaining user lookup helpers

* chore: migrate ssh user lookups

* chore: migrate user settings admin lookups

* chore: migrate acme ssl user lookups

* chore: migrate audit log admin checks

* chore: migrate oidc account user updates

* chore: migrate password reset user updates

* chore: migrate user deletion core records

* chore: migrate snippet audit user lookups

* chore: migrate ldap user sync paths

* chore: migrate totp user updates

* chore: migrate rbac user checks

* chore: migrate rbac role paths

* chore: migrate permission role lookups

* chore: migrate rbac access list reads

* chore: migrate shared rbac reads

* chore: migrate rbac access writes

* chore: migrate permission host access

* chore: migrate role host access lookup

* chore: migrate snippet access lookup

* chore: migrate shared credential access lookups

* chore: migrate host access cleanup writes

* chore: migrate host list access checks

* chore: migrate host access cleanup routes

* chore: migrate shared credential role lookups

* chore: migrate user role cleanup

* chore: migrate admin role sync

* chore: migrate ldap role sync

* chore: migrate user role assignment

* chore: migrate sso provider access

* chore: migrate audit log access

* chore: migrate user preference access

* chore: migrate open tab access

* chore: migrate dismissed alert access

* chore: migrate homepage layout access

* chore: migrate network topology access

* chore: migrate dashboard service link access

* chore: migrate command history access

* chore: migrate recent activity cleanup

* chore: migrate ssh credential usage access

* chore: migrate transfer recent access

* chore: migrate file manager bookmark access

* chore: migrate c2s tunnel preset access

* chore: migrate homepage item access

* chore: migrate session recording access

* chore: migrate tmux session tag access

* chore: migrate opkssh token access

* chore: migrate vault token access

* chore: migrate vault profile access

* chore: migrate host metrics preference access

* chore: migrate host health access

* chore: migrate host metrics history access

* chore: migrate alert persistence access

* chore: route alert host lookup through repository

* chore: migrate user data export reads

* chore: route host metrics stats sync through repository

* chore: migrate host folder persistence

* chore: migrate host resolution reads

* chore: route jump host resolution reads

* chore: route docker console jump host reads

* chore: route docker ssh resolution reads

* chore: route proxmox discovery resolution reads

* chore: route file manager activity host reads

* chore: route host metrics resolution reads

* chore: route ssh auth credential reads

* chore: route tunnel endpoint credential reads

* chore: route credential deployment resolution reads

* chore: route command history host flag reads

* chore: route snippet execution resolution reads

* chore: route terminal host resolution reads

* chore: route vault oidc host resolution reads

* chore: route wake on lan host reads

* chore: route internal host list reads

* chore: route host key verification persistence

* chore: route credential read paths

* chore: route credential host usage reads

* chore: route credential folder rename

* chore: route host owner access checks

* chore: route shared credential source reads

* chore: route user host credential cleanup

* chore: route credential delete reads

* chore: route credential update reads

* chore: route host credential reads

* chore: route host read paths

* chore: route host projection reads

* chore: route host list reads

* chore: route snippet read paths

* chore: route snippet folder writes

* chore: route snippet crud paths

* chore: route snippet bulk import

* chore: route rbac ownership reads

* chore: route user count reads

* chore: route cleanup snippets folders

* chore: route shared credential persistence

* chore: route dashboard activity

* chore: route guacamole host reads

* chore: route host bulk lookups

* chore: remove unlock-only simple db ops

* chore: route host autostart persistence

* chore: route ldap provisioning through users

* chore: route credential encrypted writes

* chore: route host encrypted writes

* chore: route bulk host encrypted writes

* chore: route termix id credentials

* chore: route termix id ca persistence

* chore: route termix identity persistence

* chore: route credential system migration

* chore: isolate user encryption migration storage

* chore: remove legacy simple db ops

* chore: isolate legacy sqlite migration copy

* chore: route database settings import export

* chore: route database host credential export

* chore: route database host credential import

* chore: route database file-manager import export

* chore: route database alert usage import export

* chore: route database user checks

* chore: isolate auth lazy migration storage

* chore: route explicit database saves

* chore: initialize database save boundary

* chore: route migration snapshot saves

* chore: isolate sqlite import constraints

* chore: route import sqlite boundary

* chore: route user encryption migration store

* chore: centralize current repository runtime

* chore: route more current repositories

* chore: route activity repository runtimes

* chore: route token repository runtimes

* chore: route health repository runtimes

* chore: route identity repository runtimes

* chore: route rbac repository runtime

* chore: centralize current sqlite runtime access

* chore: route user deletion key cleanup

* chore: route user deletion vault cleanup

* chore: route user deletion homepage cleanup

* chore: route user deletion health cleanup

* chore: route user deletion alert cleanup

* chore: route user deletion identity cleanup

* chore: add database layer preupgrade backup

* Fix database repository type errors

* fix: complete post-merge compile fixes for database refactor

Restore missing DatabaseSaveTrigger/getDb imports, session log format
fallback, OIDC provider resolution, guacamole recording insert, and
passwordFallbackOnly typing after merging current dev.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: DivByZero <mr.oplus@yahoo.fr>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: devdanetra <46488477+devdanetra@users.noreply.github.com>
Co-authored-by: Aleksandr Fominykh <neoformalex@users.noreply.github.com>
Co-authored-by: sash <sash@fominykh.io>
2026-07-15 23:28:10 -05:00

1188 lines
42 KiB
Markdown

# Database Layer Refactor Plan
Status: Draft
Branch: `feature/database-layer-refactor`
Target: Replace the current in-memory encrypted SQLite snapshot model with a persistent, secure, multi-database architecture.
Phase 0 audit: [`DATABASE-LAYER-PHASE-0-AUDIT.md`](./DATABASE-LAYER-PHASE-0-AUDIT.md)
Gray rollout guide: [`DATABASE-LAYER-GRAY-ROLLOUT.md`](./DATABASE-LAYER-GRAY-ROLLOUT.md)
Phase 1 status: database runtime config and SQLite adapter skeleton have started under
`src/backend/database/runtime/`. This code is not wired into the existing production
database initialization yet.
Repository status: the first repository skeletons, `SettingsRepository`,
`UserRepository`, `SessionRepository`, `HostRepository`, and
`CredentialRepository`, have started under `src/backend/database/repositories/`.
`RoleRepository` has also started for the RBAC role/user-role slice, and these
repositories are covered by SQLite-backed tests. `RbacAccessRepository` has
started for RBAC host/snippet access-list read models.
Field encryption status: repository-level field encryption boundary tests have
started. New field encryption requires a stable record id and must not invent a
temporary encryption context.
Settings migration status: a first low-risk production route slice now uses
`SettingsRepository` through the current SQLite runtime context. Legacy in-memory
runtime writes still force a snapshot save through `DatabaseSaveTrigger`.
The user settings route has also moved its direct `settings` table reads and
writes behind `SettingsRepository`.
Host metrics settings routes now use the same repository boundary for global
monitoring defaults and history retention settings.
ACME SSL settings route also uses the repository boundary for its persisted
configuration key.
Terminal route session settings and command history global flag now read/write
through `SettingsRepository`.
Tailscale device route now reads its API key through `SettingsRepository`.
Guacamole route and WebSocket server now read `guac_url` through the current
settings repository boundary.
Authentication token expiry and terminal session timeout reads now use the
current settings repository boundary.
Open tabs, TOTP, and LDAP auth routes now read their settings through the
current settings repository boundary.
Host metrics polling now reads global interval and retention settings through
the current settings repository boundary.
Backend startup now reads persisted log level and Guacamole enablement through
the current settings repository boundary.
User deletion cleanup now removes per-user settings through
`SettingsRepository.deleteLike`.
Password reset route now stores reset codes and temporary reset tokens through
`SettingsRepository`.
OIDC utility legacy config fallback now reads `oidc_config` through
`SettingsRepository`.
User route registration/password flags and OIDC config administration now use
the current settings repository boundary.
OIDC authorize/callback temporary state and auto-provision reads now use the
current settings repository boundary.
User login settings reads now use the current settings repository boundary, so
`routes/users.ts` no longer reads or writes `settings` directly.
User encryption metadata in `utils/user-crypto.ts` now reads and writes KEK/DEK
settings through the current settings repository boundary.
Database import/export user unlock and admin checks now use the current user
repository boundary.
Auth login lazy user-field encryption migration now calls the `DataCrypto`
current-runtime migration boundary instead of opening SQLite in
`auth-manager.ts`.
Current user-field encryption migration now resolves SQLite through
`createCurrentUserEncryptionMigrationStore` and the shared current runtime
helper, keeping `DataCrypto` on the migration-store interface.
User, admin, TOTP, OIDC account, and credential migration explicit saves now
use `DatabaseSaveTrigger` instead of importing the SQLite snapshot save function
from routes.
Current SQLite snapshot save trigger now initializes after database startup
regardless of file-encryption mode, and backend shutdown uses that save
boundary.
Direct SQLite snapshot save-function imports are now isolated inside
`database/db/index.ts`; current user-field migration saves through
`DatabaseSaveTrigger`.
Database import SQLite foreign-key toggling is isolated behind the
`withSqliteForeignKeysDisabled` runtime boundary and restores constraints in a
`finally` path; the current variant resolves SQLite through the shared current
runtime helper.
Database import now uses the current SQLite foreign-key boundary without
importing `getDb()` in the route handler.
Settings, user, host, API key, session, trusted device, audit log, user
preference, open tab, dismissed alert, homepage layout/item, and network
topology, dashboard service link, session recording, command history, recent
activity, transfer recent, and file-manager bookmark current repository
factories plus C2S tunnel preset, tmux session tag, OPKSSH token, Vault
token/profile, SSH credential usage, role, SSO provider, host folder, alert,
host health, host metrics preference/history, credential, host resolution,
snippet, RBAC access, shared credential, Termix ID identity/CA, and user data
export current repository factories now share `current-repository-runtime` for
SQLite context and write-save hooks.
Database startup and schema migration defaults in `database/db/index.ts` now use
local raw settings helpers instead of scattered settings SQL.
Database import/export settings handling in `database/database.ts` now uses
local helper boundaries for export filtering and admin import upserts.
Those import/export settings helpers now use the current settings repository
boundary.
Session repository now has a current-runtime factory and write-save hook, and
core `auth-manager.ts` session create/read/update/revoke/list paths have started
using it.
Remaining `auth-manager.ts` session cleanup/middleware/logout paths and
`user-session-routes.ts` single-session lookup now use the current session
repository boundary.
User repository now has a current-runtime factory and write-save hook, and
`user-admin-routes.ts` list/admin promotion/admin removal/admin-create user
paths now use it for `users` table reads and writes.
Low-risk `routes/users.ts` current-user lookup and admin gate checks now use the
current user repository boundary.
User registration, self-delete, password change hash updates, and admin
delete-user lookup paths in `routes/users.ts` now use the current user
repository boundary while preserving the first-user admin transaction inside
`UserRepository`.
Traditional login username lookup and `auth-manager.ts` admin user checks now
use the current user repository boundary.
Credential list, folder list, detail reads, update lookup/readback, host route
credential resolution reads, folder rename writes, apply usage writes, shared
credential source row reads, credential delete lookup/delete, and
credential-host list reads now use current credential and host resolution
repository boundaries.
Database export host and credential read/decryption paths now use current host
and credential repository boundaries.
Database import host and credential duplicate checks plus encrypted creates now
use current host and credential repository boundaries.
Database import/export file-manager recent/pinned/shortcut target reads and
writes now use the current file-manager bookmark repository boundary.
Credential create/update encrypted writes now use the current credential
repository boundary, including system-key copies for shared credentials.
Credential delete host cleanup and apply-to-host writes now use the current host
repository boundary.
Credential system-key copy backfill migration now uses current credential and
shared credential repository boundaries.
Legacy user field-encryption migration SQL is now centralized behind
`RawSqliteUserEncryptionMigrationStore`.
Legacy unencrypted SQLite copy/verification during encrypted-file migration is
now centralized behind `LegacySqliteDatabaseCopyStore`.
Termix ID credential lookup, generated credential persistence, and generated
credential cleanup now use the current credential repository boundary.
Termix ID identity handle CRUD/resolution, public key publish/list/update/delete,
linked credential lookup, and certificate target key lookup now use the current
Termix ID repository boundary.
Termix ID CA public lookup, encrypted private-key create/rotate/delete, and
certificate signing reads now use the current Termix ID CA repository boundary.
Host route update readback, single-host fetch, password-field fetch, and host
export reads now use the current host resolution repository boundary.
Host route create/update encrypted writes now use the current host repository
boundary.
Host route update-state and delete-audit host reads now use the current host
resolution repository boundary.
Host route delete final host-row writes and audit actor username lookups now use
the current host and user repository boundaries.
Host route own/shared list assembly reads now use the current host resolution
repository boundary while preserving route-level own-host decryption.
Host bulk-update state reads, non-sensitive bulk flag/config writes, and JSON
plus SSH-config import encrypted create/update writes now use the current host
repository boundary, while host bulk import overwrite lookup plus credential
fallback reads use current host resolution and credential repository boundaries.
Host autostart enable, disable, status, and endpoint-host resolution paths now
use the current host repository boundary.
Guacamole host token host and protocol credential reads now use the current
host resolution repository boundary while preserving request-user decryption
behavior for credential fallback.
Snippet folder list/create/metadata/rename/delete, owned lookup, visible-list
owned reads, reorder, create/update/delete, export reads, and bulk import now
use the current snippet repository boundary.
User cleanup and password-reset data-discard host, credential, snippet, snippet
folder, and host folder deletes now use current repository boundaries.
GitHub and standard OIDC callback user lookup/create/rollback/profile/admin
sync writes now use the current user repository boundary, removing direct
Drizzle `users` table access from `routes/users.ts`, `user-admin-routes.ts`,
and `auth-manager.ts`.
User setup/count/db-health checks, password-login TOTP guard reads, and
last-admin delete guard reads now use the current user repository boundary.
API key create/list/delete and API key authentication last-used updates now use
the current API key repository boundary.
Trusted device check/add/remove and TOTP trusted-device cleanup now use the
current trusted device repository boundary.
User session routes now use the current user repository boundary for user/admin
lookups and admin session username enrichment.
Vault admin checks, Termix ID audit actor username lookup, permission manager
admin checks, and user data export user lookup now use the current user
repository boundary.
SSH credential OIDC username expansion and tmux monitor audit actor username
lookup now use the current user repository boundary.
User settings route admin checks and audit actor username lookups now use the
current user repository boundary, removing direct DB/schema imports from that
route module.
ACME SSL route admin checks and audit actor username lookups now use the current
user repository boundary, removing direct DB/schema imports from that route
module.
Audit log route admin checks now use the current user repository boundary,
removing direct `users` access from that route module.
OIDC account link/unlink route user lookups and OIDC field updates now use the
current user repository boundary, removing direct `users` access from that route
module.
Password reset route user lookups, password hash updates, TOTP reset fields, and
per-user encrypted-data cleanup now use current repository boundaries.
User deletion helper now removes sessions, API keys, trusted devices, dashboard
links, homepage layout/items, host health checks/history, host metrics
preferences, C2S presets, Vault tokens/profiles, roles, alert data, audit logs,
Termix ID identity/CA, tmux session tags, encrypted data, UI state, and related
per-user rows through current repository boundaries before deleting the final
user record.
Snippet create/update/delete audit actor username lookups now use the current
user repository boundary; the remaining snippet `users` usage is the shared
snippet owner join.
LDAP login first-user provisioning, admin-group user creation, existing-user
lookup, encryption rollback delete, admin sync, and display-name sync now use
the current user repository boundary.
TOTP setup/enable/disable/backup-code/login verification user updates and
session revocation now use the current user/session repository boundaries.
RBAC host sharing, role assignment, and snippet sharing target-user existence
checks now use the current user repository boundary while retaining existing
RBAC/snippet owner username joins.
RBAC role list/create/update/delete, user-role assignment/removal/listing, and
shared host/snippet role-id lookups now use the current role repository
boundary while retaining existing RBAC/share credential joins in the route.
Permission manager role permission aggregation, role-id lookups for shared host
access, and admin role checks now use the current role repository boundary.
RBAC host/snippet access-list read models now use the current RBAC access
repository boundary, and snippet route shared-access role-id lookups now use the
current role repository boundary.
RBAC shared host/shared snippet read models and the main snippet shared-snippet
read model now use the current RBAC access repository boundary.
RBAC route host/snippet owner checks and direct host-access credential existence
reads now use current host, snippet, and credential repository boundaries.
RBAC host/snippet access grant, revoke, and direct host-access credential
override writes now use the current RBAC access repository boundary while
retaining shared credential material creation in the existing manager.
Shared credential material create/update/delete, pending re-encryption, and
user cleanup persistence now use the current shared credential repository
boundary while keeping encryption/decryption logic in the existing manager.
Permission manager host-access expiration cleanup, shared host-access lookup,
and last-access timestamp updates now use the current RBAC access repository
boundary.
RBAC role assignment now reads role-shared host credential sources through the
current RBAC access repository boundary while keeping shared credential material
creation in the existing manager.
Snippet single-item shared-access checks now use the current RBAC access
repository boundary.
Shared credential manager host-access lookups for role-member credential
creation, shared credential reads, and pending re-encryption owner discovery now
use the current RBAC access repository boundary.
Host route host-access cleanup writes for credential removal and host deletion
now use the current RBAC access repository boundary.
Host route shared-host list access checks now use the current role and RBAC
access repository boundaries while host row loading stays local to the host
route.
Credential deletion, folder deletion, and user deletion host-access cleanup
writes now use the current RBAC access repository boundary.
Shared credential manager role-member lookups now use the current role
repository boundary.
User deletion role-assignment cleanup now uses the current role repository
boundary.
User admin route role sync and admin-created default role assignment now use
the current role repository boundary.
LDAP login default role assignment and admin-role sync now use the current role
repository boundary.
Local, GitHub OIDC, and standard OIDC user default role assignment plus OIDC
admin-group role sync now use the current role repository boundary.
SSO provider listing, management, OIDC config loading, and LDAP provider
validation now use the current SSO provider repository boundary.
Audit log writes, filtered reads, action lists, and user cleanup now use the
current audit log repository boundary.
User preferences read/write and user cleanup now use the current user
preference repository boundary.
Open tab restore/upsert/sync/update/delete and user cleanup now use the current
open tab repository boundary.
Dismissed alert read/dismiss/undismiss/export and user cleanup now use the
current dismissed alert repository boundary.
Homepage layout read/write now uses the current homepage layout repository
boundary.
Homepage item list/create/update/delete now uses the current homepage item
repository boundary.
Network topology read/write and user cleanup now use the current network
topology repository boundary.
Dashboard service link list/create/update/delete now uses the current dashboard
service link repository boundary.
Session recording create/list/read/content/delete/prune and host/folder/user
cleanup now uses the current session recording repository boundary.
Command history save/list/delete and host/user cleanup now use the current
command history repository boundary.
Dashboard recent activity list/log/trim/reset and host/user cleanup now use the
current recent activity repository boundary, with host ownership/share checks
using current host resolution, role, and RBAC access repository boundaries.
SSH credential usage writes and host/user cleanup now use the current SSH
credential usage repository boundary.
Transfer recent list/upsert/prune/export and host/folder/user cleanup now use
the current transfer recent repository boundary.
File manager recent/pinned/shortcut list/create/delete/export and
host/folder/user/password-reset cleanup now use the current file manager
bookmark repository boundary.
C2S tunnel preset list/create/update/delete now uses the current C2S tunnel
preset repository boundary.
Tmux session tag list/rename/delete/replace now uses the current tmux session
tag repository boundary.
OPKSSH token upsert/read/touch/delete and user cleanup now use the current
OPKSSH token repository boundary.
Database import/export dismissed alert reads and writes now use the current
dismissed alert repository boundary.
Database export SSH credential usage reads now use the current SSH credential
usage repository boundary.
Vault token upsert/read/touch/delete now uses the current Vault token repository
boundary.
Vault profile list/create/update/delete and profile lookup now use the current
Vault profile repository boundary.
Host metrics layout preference read/upsert and statsConfig widget sync now use the current host metrics
preference repository boundary.
Host health check config and history read/write now use the current host health
repository boundary.
Host metrics history record/prune/query now uses the current host metrics
history repository boundary.
Alert notification channels, rules, linked channels, firings, and alert engine
persistence reads/writes now use the current alert repository boundary.
User data export host and credential read models now use the current user data
export repository boundary.
SSH folder list, metadata upsert, rename, and folder host deletion writes now
use the current host folder repository boundary.
Host, jump-host, Docker SSH, Proxmox discovery, Docker console jump-host,
file-manager activity, host metrics, terminal SSH auth, and tunnel endpoint
credential plus credential deployment and command history host-flag resolution
plus snippet execution, terminal OPKSSH/activity, Vault OIDC profile, and
Wake-on-LAN host, internal host list, host-key verification metadata,
credential, and shared override read/write models now use the current host
resolution repository boundary.
Host metrics, host metrics viewer, tmux monitor, Docker, tunnel, and Proxmox
unlock gates now use `DataCrypto` directly instead of `SimpleDBOps`.
Legacy `SimpleDBOps` compatibility helper has been removed after migrated route
and utility paths stopped importing it.
Permission manager host owner checks now use the current host resolution
repository boundary.
Gray rollout status: the branch is expanding the repository boundary slice while
keeping every migrated domain behind `DATABASE_LAYER_REPOSITORY_ROLLOUT`, which
supports `all`, `off`, and a comma-separated allowlist for controlled gray
targets.
## 1. Background
Termix currently uses an encrypted SQLite snapshot model:
```text
db.sqlite.encrypted
-> decrypt on startup
-> open as in-memory SQLite
-> runtime reads/writes memory database
-> serialize whole database
-> encrypt and write snapshot back to disk
```
This works well for small single-instance deployments, but it creates several long-term problems:
- Recent writes can be lost if the process crashes before the snapshot save runs.
- Direct database writes can forget to call `DatabaseSaveTrigger`, causing data to exist only in memory.
- Saving requires serializing and encrypting the whole database, so cost grows with database size.
- The architecture cannot safely support multiple backend instances.
- It blocks clean PostgreSQL/MySQL support because business code depends directly on SQLite and Drizzle SQLite tables.
The refactor should keep Termix secure while moving persistence to real database writes.
## 2. Goals
- Support persistent database backends:
- SQLite
- PostgreSQL
- MySQL/MariaDB
- Keep sensitive data encrypted at the field level.
- Remove dependence on full in-memory database snapshots.
- Introduce a database adapter boundary.
- Move direct database access behind repositories/services.
- Provide a safe upgrade path from the existing encrypted SQLite snapshot.
- Preserve existing user data, encryption keys, credentials, hosts, settings, audit records, and feature data.
- Keep runtime-only state in memory:
- SSH clients
- WebSocket sessions
- tunnels
- metrics polling sessions
- transfer runtime state
- Make migrations deterministic, idempotent, and rollback-safe.
## 3. Non-Goals
- Do not rewrite the whole backend at once.
- Do not force existing users to configure PostgreSQL or MySQL during normal upgrade.
- Do not remove SQLite support.
- Do not store SSH live sessions, WebSocket handles, or tunnel process handles in the database.
- Do not encrypt every field blindly. Searchable and sortable metadata should stay queryable unless it is sensitive.
- Do not change user-facing behavior unless required by the storage model.
## 4. Current Architecture
### 4.1 Database Runtime
- Database initialization lives in `src/backend/database/db/index.ts`.
- `actualDbPath` is `:memory:`.
- Encrypted database files are decrypted into memory on startup.
- Runtime database access uses Drizzle over `better-sqlite3`.
- On save, the whole memory database is serialized and encrypted.
### 4.2 Save Behavior
- Historical `SimpleDBOps.insert/update/delete` writes used to trigger
`DatabaseSaveTrigger`; migrated write paths now use repository save hooks.
- Some routes call `getDb().insert/update/delete` or raw SQLite directly.
- Direct writes must manually call `DatabaseSaveTrigger.triggerSave()`.
- This creates a persistence gap when direct writes forget to trigger saves.
### 4.3 Runtime State
The following state is intentionally memory-only today and should remain memory-only:
- Terminal sessions in `terminal-session-manager.ts`
- File manager sessions in `file-manager.ts`
- Metrics sessions and caches in `host-metrics-sessions.ts` and `host-metrics-state.ts`
- Tunnel runtime maps in `tunnel.ts`
- Pending OPKSSH/Vault/OIDC authentication sessions
### 4.4 Schema State
- Schema is declared with `sqliteTable` in `src/backend/database/db/schema.ts`.
- Additional schema setup is done manually through `CREATE TABLE IF NOT EXISTS`.
- Schema migration uses many `addColumnIfNotExists` calls.
- This is tightly coupled to SQLite.
## 5. Target Architecture
```text
Backend Feature Code
-> Service Layer
-> Repository Layer
-> Database Adapter
-> SQLite
-> PostgreSQL
-> MySQL/MariaDB
-> Field Encryption Boundary
-> Persistent Database
```
### 5.1 Database Adapter
Introduce a database runtime module responsible for:
- Reading database configuration.
- Creating the correct database client.
- Running migrations.
- Exposing a typed query context.
- Handling database-specific transaction behavior.
- Hiding dialect-specific differences from business logic.
Proposed configuration:
```env
DB_TYPE=sqlite
DATABASE_URL=file:/app/data/termix.sqlite
# or
DB_TYPE=postgres
DATABASE_URL=postgres://termix:password@postgres:5432/termix
# or
DB_TYPE=mysql
DATABASE_URL=mysql://termix:password@mysql:3306/termix
SYSTEM_KEY=...
```
SQLite should remain the default.
### 5.2 Repository Layer
Business code should stop calling `getDb()` directly.
Examples:
```ts
hostRepository.getById(userId, hostId);
hostRepository.create(userId, input);
credentialRepository.resolveForHost(userId, hostId);
sessionRepository.create(userId, sessionData);
auditRepository.write(event);
settingsRepository.get(key);
```
Repositories should own:
- Query construction
- Data mapping
- Field encryption/decryption
- Stable record-id enforcement before encrypting field values
- Permission-aware reads where appropriate
- Transaction participation
- Database-specific compatibility
### 5.3 Service Layer
Services should own business workflows:
- Host creation/update/delete
- Credential resolution
- User/session lifecycle
- RBAC operations
- Import/export
- Migration orchestration
Services can call multiple repositories inside one transaction.
### 5.4 Runtime State Boundary
Runtime state should remain in process memory and should not be migrated into the database:
- SSH connection objects
- PTY streams
- SFTP handles
- WebSocket instances
- Timers
- Child processes
- Pending MFA/auth prompts
The database should store only durable metadata:
- session recording metadata
- audit events
- user open tabs, if still needed
- tunnel definitions, not live tunnel handles
- metrics history, not active polling state
## 6. Security Model
### 6.1 Keep Field-Level Encryption
Sensitive fields must be encrypted before entering any persistent database:
- SSH passwords
- SSH private keys
- SSH key passphrases
- credential secrets
- TOTP secrets and backup codes
- OAuth/OIDC client secrets
- Vault tokens
- OPKSSH tokens/cert cache secrets
- API tokens or token material
- RDP/VNC/Telnet passwords
### 6.2 Keep Queryable Metadata Plain
Some data should remain queryable:
- numeric IDs
- user IDs
- host names
- folders
- tags
- enable flags
- timestamps
- connection type
- port numbers
- status/config flags
For fields like host IP/domain, decide explicitly:
- Plaintext improves search, sorting, and connection setup.
- Encrypted improves confidentiality.
- If encrypted, consider additional blind indexes for search.
### 6.3 Envelope Encryption
Recommended model:
```text
SYSTEM_KEY
-> wraps application/database keys
-> wraps per-user data keys
-> per-user data keys encrypt user-owned secrets
```
Each encrypted field should include enough metadata for future rotation:
```json
{
"v": 2,
"alg": "aes-256-gcm",
"kid": "user-key-id",
"iv": "...",
"tag": "...",
"ct": "..."
}
```
### 6.4 Key Rotation
The new design should support:
- Detecting encryption version.
- Reading old ciphertext.
- Writing new ciphertext.
- Lazy migration on write.
- Optional explicit re-encryption job.
### 6.5 Database-Level Encryption
Field-level encryption is required for all database types.
Optional database/file encryption:
- SQLite can optionally use encrypted file storage later.
- PostgreSQL/MySQL should rely on field encryption plus deployment-level disk encryption/TLS.
- Do not require full database encryption for correctness.
## 7. Database Support Strategy
### 7.1 SQLite
SQLite remains the default for simple self-hosted installs.
Requirements:
- Direct persistent SQLite file, not in-memory snapshot.
- WAL mode if safe for the deployment model.
- Proper shutdown handling.
- No full database serialize on every save.
### 7.2 PostgreSQL
PostgreSQL should be the preferred production multi-user backend.
Requirements:
- Connection pool.
- Transaction support.
- Native boolean/timestamp handling.
- JSONB where useful.
- Migration support.
- TLS support through connection string options.
### 7.3 MySQL/MariaDB
MySQL/MariaDB support should be implemented after SQLite and PostgreSQL are stable.
Requirements:
- Connection pool.
- Compatible migrations.
- JSON/text behavior reviewed.
- Timestamp/default behavior reviewed.
- `RETURNING` differences handled at repository/adapter level.
### 7.4 Dialect Differences to Handle
- Boolean storage
- Auto-increment IDs
- Timestamp defaults
- JSON columns
- `RETURNING`
- Upsert syntax
- Case sensitivity and collations
- Foreign key enforcement
- Transaction isolation
- Raw SQL fragments
## 8. Migration Strategy
### 8.1 Default Upgrade Path
Default user upgrade should not require external database setup.
Recommended default:
```text
old encrypted SQLite snapshot
-> decrypt with existing logic
-> normalize old schema
-> migrate to new persistent SQLite
-> keep old snapshot as backup
```
PostgreSQL/MySQL migration should be explicit:
```bash
termix migrate-db --from sqlite --to postgres
termix migrate-db --from sqlite --to mysql
```
or later through an admin UI.
### 8.2 Backup Rules
Before migration:
- Verify source database can be decrypted.
- Verify target database can be connected.
- Verify target schema can be migrated.
- Create a timestamped backup of the old encrypted database.
- Do not delete the old encrypted snapshot automatically.
Suggested backup naming:
```text
db.sqlite.encrypted.pre-db-refactor-YYYYMMDD-HHmmss
```
### 8.3 Migration Marker
New databases should store migration state:
```text
schema_migrations
id
version
name
applied_at
checksum
system_migrations
key
value
applied_at
```
The old snapshot migration should have a one-shot marker:
```text
legacy_snapshot_migrated = true
legacy_snapshot_source_hash = ...
legacy_snapshot_migrated_at = ...
```
### 8.4 Idempotency
Migration must be safe to rerun after interruption.
Rules:
- Never destroy the source before full success.
- Use unique keys or deterministic IDs where possible.
- Commit in table-level or batch-level transactions.
- Write migration markers only after successful commit.
- On startup, detect partial migration and either resume or fail safely.
### 8.5 Dry Run
Add a dry-run mode:
```bash
termix migrate-db --dry-run
```
Dry run should report:
- Source database detected
- Source schema version
- Target database type
- Target reachability
- Tables to migrate
- Row counts
- Unsupported old schema issues
- Estimated warnings
### 8.6 Old Version Compatibility
Users may upgrade from older Termix versions.
Migration must:
- Run old schema normalization first.
- Add missing columns in the legacy reader if needed.
- Handle absent optional tables.
- Handle older ciphertext formats.
- Handle old settings keys.
### 8.7 Failure Behavior
If migration fails:
- New app should not start with an empty database.
- Old encrypted snapshot should remain untouched.
- Database-layer gray startup creates a one-time pre-upgrade backup before
opening the existing SQLite snapshot, and fails closed if that backup cannot
be written unless `DATABASE_LAYER_SKIP_PREUPGRADE_BACKUP=1` is explicitly set.
- Error logs should identify the failed stage.
- User should get recovery instructions.
- Retrying should be safe.
## 9. Implementation Phases
### Phase 0: Audit and Design Lock
Estimated time: 2-3 days
Tasks:
- Inventory all database tables.
- Inventory all `getDb()`, `getSqlite()`, raw SQL, and `DatabaseSaveTrigger` usage.
- Classify tables by domain:
- auth/users
- hosts/credentials
- RBAC/sharing
- terminal/session logs
- file manager
- snippets
- metrics/alerts
- homepage/preferences
- tunnels/proxmox/guacamole/vault/opkssh
- Mark sensitive fields.
- Decide SQLite/PostgreSQL/MySQL type mapping.
- Decide migration framework.
Deliverables:
- Database access inventory.
- Sensitive field map.
- Final adapter/repository interface proposal.
### Phase 1: Adapter Foundation
Estimated time: 3-5 days
Tasks:
- Introduce database config parser.
- Introduce adapter interface.
- Add SQLite persistent adapter.
- Add migration table.
- Add transaction helper.
- Keep old code path running.
- Add tests for config and adapter initialization.
Deliverables:
- `DB_TYPE=sqlite` persistent adapter works in isolation.
- No business routes migrated yet.
### Phase 2: Repository Skeleton
Estimated time: 4-6 days
Tasks:
- Create repository interfaces.
- Implement initial SQLite-backed repositories.
- Add encryption boundary helpers.
- Add repository tests with SQLite.
- Prevent new direct `getDb()` usage in new code.
Initial repositories:
- settings
- users
- sessions
- hosts
- credentials
- audit
Deliverables:
- Core repositories compile and pass tests.
- Existing routes can still run through compatibility shims.
### Phase 3: Core Vertical Slice
Estimated time: 1-2 weeks
Tasks:
- Migrate user/session auth flows.
- Migrate settings routes.
- Migrate hosts CRUD.
- Migrate credentials CRUD and credential resolution.
- Migrate host resolver and permission-critical reads.
- Add transaction coverage for multi-table operations.
- Verify field encryption behavior.
Acceptance criteria:
- User login/logout/session validation works.
- Host create/update/delete works.
- Credential create/update/delete/resolve works.
- Existing encrypted user data remains readable.
- No snapshot save trigger needed for migrated core paths.
### Phase 4: Feature Data Migration
Estimated time: 1-2 weeks
Tasks:
- RBAC and sharing
- folders/tags
- snippets and snippet access
- file manager bookmarks/recent/pinned/shortcuts
- dashboard/homepage/preferences/open tabs
- alerts and notification channels
- metrics preferences/history/health checks
- tunnels and C2S presets
- Proxmox config
- Guacamole config
- OPKSSH/Vault records
- audit logs
- API keys
Acceptance criteria:
- Direct feature routes use repositories or domain services.
- Growth tables have retention strategy where appropriate.
- Runtime-only data remains memory-only.
### Phase 5: Multi-Database Support
Estimated time: 1-2 weeks
Tasks:
- Add PostgreSQL adapter.
- Add MySQL/MariaDB adapter.
- Add dialect-specific schema/migrations.
- Add CI matrix for SQLite/PostgreSQL/MySQL if feasible.
- Replace SQLite-only raw SQL in migrated paths.
- Add Docker compose examples.
Acceptance criteria:
- Fresh install works on SQLite.
- Fresh install works on PostgreSQL.
- Fresh install works on MySQL/MariaDB.
- Core CRUD tests pass across supported databases.
### Phase 6: Legacy Snapshot Migration
Estimated time: 1 week
Tasks:
- Implement legacy encrypted snapshot reader.
- Normalize legacy schema in memory.
- Migrate legacy snapshot to new SQLite.
- Keep backup of old encrypted snapshot.
- Add dry-run.
- Add migration logs.
- Add recovery documentation.
Acceptance criteria:
- Existing encrypted SQLite snapshot migrates to new persistent SQLite.
- Migration can be retried safely.
- Failed migration leaves old data untouched.
### Phase 7: External Database Migration Tool
Estimated time: 3-5 days
Tasks:
- Add CLI or admin-only migration command.
- Support SQLite to PostgreSQL.
- Support SQLite to MySQL/MariaDB.
- Add dry-run and validation.
- Add clear rollback instructions.
Acceptance criteria:
- Existing SQLite install can be migrated to PostgreSQL/MySQL explicitly.
- Migration is not automatic unless user opts in.
### Phase 8: Cleanup and Enforcement
Estimated time: 3-5 days
Tasks:
- Remove in-memory snapshot runtime path.
- Remove `DatabaseSaveTrigger` from normal persistence.
- Keep only legacy import compatibility.
- Add lint/test guard against direct database access outside approved modules.
- Update docs.
Acceptance criteria:
- Business modules no longer import `getDb()` directly except approved infrastructure/repository files.
- `DatabaseSaveTrigger` is no longer required for normal data durability.
- Old snapshot code is isolated to legacy migration/import.
## 10. Testing Plan
### 10.1 Unit Tests
- Encryption/decryption helpers
- Repository mapping
- Adapter config parsing
- Migration idempotency
- Legacy ciphertext compatibility
- Dialect-specific value conversion
### 10.2 Integration Tests
Run core flows against:
- SQLite
- PostgreSQL
- MySQL/MariaDB
Core flows:
- user create/login/logout/session validation
- host CRUD
- credential CRUD and host resolution
- RBAC host sharing
- snippet CRUD
- settings update
- audit write/read
### 10.3 Migration Tests
Fixtures:
- latest legacy encrypted snapshot
- older legacy snapshot with missing columns
- snapshot with no optional feature tables
- snapshot with encrypted credentials
- snapshot with large metrics/audit tables
Checks:
- row counts match
- sensitive fields decrypt after migration
- target app starts
- rerun does not duplicate data
- failed migration preserves source
### 10.4 Manual QA
- Fresh SQLite install
- Fresh PostgreSQL install
- Fresh MySQL install
- Upgrade from existing encrypted SQLite install
- Switch to external DB through explicit migration
- Docker deployment
- Electron/local deployment if supported
## 11. Rollout Plan
### Version N
- Add new architecture behind SQLite default.
- Auto-migrate legacy encrypted snapshot to new persistent SQLite.
- PostgreSQL/MySQL marked experimental or manual.
- Keep legacy snapshot backup.
### Version N+1
- Stabilize PostgreSQL/MySQL.
- Add admin UI or CLI migration tooling.
- Improve docs and diagnostics.
### Version N+2
- Remove old snapshot runtime path.
- Keep legacy import compatibility only.
## 12. User Upgrade Behavior
Default upgrade should be boring:
1. User updates Termix.
2. App detects old encrypted snapshot.
3. App creates backup.
4. App migrates to new persistent SQLite.
5. App starts normally.
6. Old snapshot remains available for recovery.
External DB migration should be opt-in:
1. User configures target database.
2. User runs dry-run.
3. User confirms migration.
4. App migrates data.
5. App writes new DB config marker.
6. App starts on external DB.
## 13. Operational Considerations
### 13.1 Backups
- SQLite: backup file copy with app stopped, or online backup API.
- PostgreSQL/MySQL: recommend native backup tooling.
- Sensitive fields remain encrypted in backups.
### 13.2 Observability
Add logs for:
- adapter selected
- migration start/end
- migration table/version
- row counts per table
- legacy snapshot backup path
- encryption version warnings
- failed migration stage
### 13.3 Performance
Expected improvements:
- No full database serialization on every save.
- Large audit/metrics tables no longer increase snapshot save cost.
- External DBs can handle larger multi-user installs.
New costs:
- Network latency for external DBs.
- Connection pool tuning.
- More complex migrations.
### 13.4 Deployment
Docker should support:
- default SQLite volume
- PostgreSQL compose example
- MySQL/MariaDB compose example
- clear `DATABASE_URL` examples
## 14. Risk Register
| Risk | Severity | Mitigation |
| ---------------------------------------------- | -------: | ----------------------------------------------------------- |
| User data cannot decrypt after migration | Critical | Preserve key model, add fixtures, dry-run decryption checks |
| Migration partially writes target DB | High | Transactions, migration markers, idempotent batches |
| Old snapshot overwritten or deleted | Critical | Never delete source automatically, backup before migration |
| Direct DB access remains scattered | High | Repository enforcement and lint guard |
| Dialect differences break features | High | Adapter tests and DB matrix |
| Sensitive fields accidentally stored plaintext | Critical | Central encryption boundary and sensitive field map |
| Large migrations block startup too long | Medium | Progress logs, batching, optional preflight |
| External DB configuration confuses users | Medium | Keep SQLite default, make external DB opt-in |
| Runtime state accidentally persisted | Medium | Explicit runtime/persistent boundary |
## 15. Initial Work Breakdown
Recommended first PRs:
1. Add database access inventory and sensitive field map. (Started in `DATABASE-LAYER-PHASE-0-AUDIT.md`)
2. Add adapter interfaces and SQLite persistent adapter skeleton.
3. Add repository skeleton for settings/users/sessions/hosts/credentials.
4. Add field encryption boundary tests. (Started)
5. Migrate settings as the first low-risk vertical slice. (Started)
6. Migrate users/sessions.
7. Migrate hosts/credentials.
8. Add legacy snapshot migration dry-run.
## 16. Open Decisions
- Should host IP/domain be encrypted, plaintext, or plaintext with optional privacy mode?
- Should metrics history stay in the main database or support separate retention storage?
- Should session log content remain file-based with DB metadata only?
- Should PostgreSQL be considered stable before MySQL/MariaDB?
- Should external DB migration be CLI-only first, or include admin UI from the start?
- Should old encrypted snapshot import remain forever as an import feature?
## 17. Estimated Total Effort
Conservative estimate for full implementation:
- Minimum: 3 weeks with reduced scope and SQLite/PostgreSQL focus.
- Realistic: 4-6 weeks for SQLite/PostgreSQL/MySQL, migration, tests, docs.
- Safer production-grade rollout: 6-8 weeks including extensive legacy fixtures and external DB QA.
Recommended planning estimate: 5 weeks.
## 18. Recommended Starting Point
Do not start by replacing every `getDb()` call.
Start with a vertical slice:
```text
settings -> users/sessions -> hosts -> credentials
```
This validates:
- adapter shape
- repository shape
- transaction shape
- field encryption
- migration pattern
- test strategy
After that slice works, migrate the rest domain by domain.