# Architecture Decision Records ## ADR-1: The source is TypeScript, linked into an application while both change Status: Accepted Context: manage-ui and sql-query-webui share their server helpers and web components. The applications run their servers with Node's type stripping and build their web code with Vite. They depend on the published package (ADR-2), and a change of the toolkit is tried in them before it is released. Decision: The source is `.ts` and `.tsx` files with three entry points: `./server`, `./web` and `./vite`. While the toolkit and an application change together, `npm link --no-save` replaces the installed package with the checkout, leaving `package.json` and the lock file on the published version; Node runs the server helpers from the checkout's real path, where type stripping applies, and Vite compiles the web code. The server helpers import only Node's modules at run time, and types of Fastify; the Vite helpers import only types of Vite, taking `loadEnv`'s answer from the application. The web code's React, MUI and Emotion come from the application, which dedupes them with `TOOLKIT_RESOLVE`. The peer dependencies are optional, as an application uses only some of the entry points, and are development dependencies for the toolkit's own checks. Consequences: The dependency of an application never names a path; `npm ci` replaces a link with the published version. Node refuses type stripping in `node_modules`, so the published package is compiled (ADR-2). ## ADR-2: The published package is built in dist/ and published to Gitea by a tag Status: Accepted Context: An installed package lives in `node_modules`, where Node does not strip types, so the published package has to be JavaScript, while the linked checkout stays TypeScript source. The repository is on Gitea, which has an npm registry per owner and Actions mostly compatible with GitHub's. Decision: `build.mjs` compiles the toolkit with `tsconfig.build.json`, which rewrites the `.ts` import extensions to `.js`, into `dist/` with declarations, and writes `dist/package.json` with exports of the compiled files and declarations; the package published is `dist/`, while the source `package.json` stays private with exports of the source. The name is `@koszewscy/sk-react-toolkit`, scoped to the owner, so an application takes only this scope from Gitea; it depends on a published version and imports the package by this name, which is also the name `npm link` gives the checkout. The package is built and published only by the `Release` workflow, which follows linux-cloud-router's: it runs for each tag and on demand, with the secrets `CI_URL`, `CI_OWNER` and `CI_TOKEN`, and skips a version already published. `actions/setup-node` gives npm the registry of the package's scope and the token, so the package carries no registry; `release.mjs` refuses a tag `x.y.z` that is not the version of `package.json`. Consequences: A release raises the version in both CHANGELOG.md and `package.json`, and a re-run, or a tag `x.y.z-n`, publishes nothing. The declarations of `renderApp` reference the declaration of the `virtual:font` module, which `build.mjs` copies into `dist/`. ## ADR-3: Credentials of any kind are kept in config.json, with secrets encrypted by a key of the environment Status: Accepted Context: sql-query-webui kept PostgreSQL connection profiles with encrypted passwords, and manage-ui needs a PostgreSQL connection and an Azure service principal without taking them from its environment. Other systems will need other kinds of credentials. Decision: A credential store is a list in a key of the application's `config.json`, all of one kind; each entry has a name and the fields of the store's kind, and no kind of its own, as the key of `config.json`, whose schema the application defines, already says what the entries are. An application with several kinds of credentials has a store, a key, for each. The application gives the kind as data: each field with its key, label, help, type (text, port, GUID or a choice), whether it is required and whether it is secret. The toolkit checks the entries, serves read, validate and write routes, and renders the dialog from the kind, so a new kind needs no new code; the dialog edits the stores of one `config.json` in tabs, and its Save writes them one after the other, each with the version the previous write answers. Secret fields are encrypted with AES-256-GCM, a random 12-byte IV each, with the key the SHA-256 of `ENCRYPTION_KEY`, else of the content of `ENCRYPTION_KEY_FILE`. A secret is never answered: read gives null and whether it is kept, and a write sends null to keep it, `''` to drop it, or a new secret. The applications decide how a credential is used: `credentialValues` answers it with its secrets decrypted. The types the server and the dialog share are in `shared/credentials.ts`, exported by both entry points. Consequences: Without the key no secret can be saved or used, and a changed key makes the saved secrets unusable until they are entered again. SHA-256 does not slow down guessing a weak key, so the key has to be long and random. Anyone who reaches the routes can replace credentials, as the applications have no authentication. ## ADR-4: The key of a credential store is a path, so related stores share one object Status: Accepted Context: sql-query-webui keeps connection profiles of PostgreSQL and of SQL Server. As stores of top-level keys, the keys have to carry the kind in their names, and a key such as `connections` does not say which kind it holds once there are several. Decision: The key of a store is a key of `config.json` or a path of keys separated by dots, such as `connections.pg` and `connections.mssql`. The store reads the list at the path, reports its problems by `..`, and a write sets the list at the path, creating the objects of the path that are missing, or are not objects, and keeping their other keys, so stores of one object can be written one after the other. The kind stays the store's, not a field of an entry (ADR-3). Consequences: A key of `config.json` holding a dot cannot be the key of a store. A single key reads and writes as before, so applications of one-key stores need no change. The application checks the object of a path, such as the keys it allows in it, itself. ## ADR-5: Terraform modules, instances and their variable files are tables shared through the toolkit Status: Accepted Context: Cloud Platform deploys many instances of many Terraform modules, and each module's configurator, a separate application, edits the variables of its instances. Both have to read and write the same modules, instances and variable files, with the same versions, so that a write from an outdated read is refused whichever application made it. Decision: The toolkit holds the tables and their functions and procedures, `server/sql/tfvars_store.sql`, and `createTfvarsStore`, which both applications use with their own `pg` pool. An instance names the schema of its state, unique among instances. A variable file is a row of its instance, its object as `jsonb` with the one it replaced. The version of an instance's files is the name and the SHA-256 of each file's text, `tfvarsText` of its object, in name order; a write locks the table, compares the version and writes in one transaction. A change the tables refuse is an `HttpError` with the status the routes answer. `pg` is an optional peer dependency, as the applications that do not use the store do not need it. Consequences: Every application of the store works on the same rows and refuses the same conflicts, and changes of the tables reach them with a release of the toolkit. The objects' keys are in `jsonb`'s order. The store is tested against a real PostgreSQL server, which the release workflow does not have, so `npm test` runs on the developer's machine. ## ADR-6: A fourth entry point, /shared, exports the shared types and constants Status: Accepted Context: An application keeps the types of its API in one file that its server and its web application both import. When those types name types of the toolkit, such as `TfModule`, the file must import them from an entry point both sides can resolve: `/server` brings Fastify and `pg` into the web application's type check, `/web` brings React and MUI into the server's. Decision: `shared/index.ts` is the entry point `/shared`, built and exported like the other three. It exports the types and constants of `shared/`, the credential types and the types and name patterns of the tfvars store, and no code that needs a peer dependency. `/server` and `/web` keep exporting them too. Consequences: An application's shared API types import from `/shared` without the code of either side. A type added to `shared/` is exported from `/shared` as well as from the side that uses it. ## ADR-7: Every write of an instance's variable files is a revision with its schema and producer Status: Accepted Context: A configurator changes the layout of the variable files it writes from one version to the next, and Cloud Platform has to tell whether an instance's files are of the layout its module's configurator writes before it runs Terraform with them. The files written earlier have to stay available, to restore or compare them. Decision: `tf_variable_revisions` holds every write of an instance's files: all files after the write, the schema version of their layout as the writer gives it, null when it does not know it, the producer, the writing application with its version, and the time. `write` and `remove` take the schema and the producer and record the revision in the transaction of the change, under the lock of the files; a write of no file records none. The current files stay in `tf_variable_files`, so reading them and their version is unchanged. Revisions are kept until removed by hand with SQL. Files without revisions are recorded as revision 1 of an unknown producer and schema when the store is first used. Consequences: The schema of an instance's files is the schema of its newest revision; files imported without a schema, or written before revisions, have none until a configurator writes them. Revisions grow with every write; the README describes the SQL that removes them. ## ADR-8: A Vite plugin sets the color scheme before the first paint Status: Accepted Context: `appTheme` puts the class of the color scheme on `` from React, after the scripts of the application have loaded. Until then a page in the dark scheme is white, which shows on every load, and on every opening of a page that embeds an application in an iframe. Decision: The `colorScheme()` plugin adds an inline script and style to `index.html`, so the class is set and the dark background painted before the application loads. The script reads MUI's storage key `mui-mode` and follows `prefers-color-scheme` for the system mode, as MUI does, and the style uses MUI's default dark background, which `appTheme` keeps. The plugin is part of the Vite helpers, which every application already uses, so the applications need no code of their own in `index.html`. Consequences: The storage key and the dark background are repeated in the plugin; a theme that changes either has to change the plugin too. An application of the toolkit adds `colorScheme()` to the plugins of its `vite.config.ts`. ## ADR-9: The checks run in a container built from the files git knows, with a scratch PostgreSQL Status: Accepted Context: The checks are faster on a remote Docker engine than on the developer's machine. The store's tests need a PostgreSQL server whose role creates databases, and a remote engine sees neither the checkout nor its `node_modules`. Decision: `test-container.mjs` sends a tar archive of the files git tracks and the untracked files it does not ignore as the build context of `test.Dockerfile`, whose build runs the checks that need no server, and runs the tests in a container next to a PostgreSQL container on a network of their own, both removed afterwards. The image's `package` stage holds the packed toolkit, which the applications install to check against the checkout. Consequences: The tests need no PostgreSQL server of the developer's. A file git ignores is not part of the checks. The checks run on Linux, whatever the developer's system.