The storage infrastructure is shared by SQL-backed node services. Functional plugins request domain repositories; provider adapters own connections, SQL, ORM mappings, indexes and module schema initialization. H2 is the only currently implemented SQL provider. An in-memory test provider verifies SPI dispatch; it is not evidence of PostgreSQL/MySQL compatibility.
storage-api:StorageProvider,StorageSession,RepositoryAdapterand typed repository lookup. No JDBC or ORM dependency.storage-jdbc: infrastructure SPI available to JDBC adapters only.storage-h2: URL construction, shutdown ownership, module schema metadata and a bounded pool (eight connections per location, ten-second borrow timeout).fido-database: the H2/ORMLite mail implementation and database plugin.MailRepositoryand editable robot contracts are infido-tools.directory-storage-h2: directory SQL behindDirectoryRepository.fileecho-storage-h2: area/access/creation-policy SQL behindFileEchoStorage.account-storage-h2: account/passkey/profile and reader-state SQL behindAccountRepository,AccountProfilesandUserMailRepository.
The database plugin publishes MailStorage. RPC checks the MailRepository
capability instead of casting to the concrete MailDatabaseFacade. Domain
services do not obtain a connection or execute named arbitrary SQL queries.
A mail repository can provide additional typed repositories through
RepositoryAccess.repository(Class<T>).
A backend location has one process-local owner. Multiple clients share its repository instances and pool through reference-counted leases. Closing a setup or directory client does not close another client's repository. The last lease closes repositories in reverse order and then closes the backend. The node stops workers and RPC transports before releasing storage. Borrowed extension registries close only their own repositories, never their parent's backend.
A shared source does not mean a single JDBC connection or a transaction spanning unrelated operations. Directory import, UI requests and mail work use independent connections. A repository owns each transaction and rollback boundary. Sharing a DataSource with ORMLite does not implicitly join a JDBC transaction to an ORM one.
Defaults preserve the existing files: mail under database/jftn10, directory at
NodeDirectoryPath, accounts at RpcAuthPath (or the existing Tmp fallback), and
reader state under Tmp/ui-state. Password hashes, passkey IDs/counters, address
grants, retained directory editions, drafts and submission reservations are not
rewritten or reset. SQL files are not automatically merged.
The optional conf.d/05-storage.conf contains provider selection. All declarations
for the repeated key must be in one fragment:
# StorageEndpoint=name|provider|provider-specific-location
StorageEndpoint=mail|h2|./database/jftn10
StorageEndpoint=directory|h2|./database/nodelist/catalog
# Specify accounts/reader only after checking the paths of the existing installation.Names used by the application are mail, directory, accounts and reader.
Without a matching declaration, existing per-component path settings still apply.
A provider interprets and normalizes its location; business plugins do not parse
JDBC URLs. Duplicate/malformed declarations, unavailable providers and missing or
ambiguous repository adapters fail explicitly. No fallback to H2 occurs after
selecting another provider. These settings require restart and remain file-only.
The UI preserves the fragment, including during first-run setup.
Changing a location selects a different store; it does not copy data. Stop the node, back up all related state and use a separately reviewed migration procedure before changing locations or combining physical database files. Do not point all endpoints at an empty file and assume old accounts/mail were transferred.
- Put a typed operation/query/result in the domain repository contract. Keep pagination, ordering, case sensitivity, null handling and errors explicit.
- Implement it in backend adapters. Use a new small capability interface when an optional feature should not expand the mandatory contract for every plugin.
- Register
StorageProviderandRepositoryAdapterimplementations underMETA-INF/services. A provider must supply every enabled required capability. - Keep backend SQL/resources in the adapter JAR. Scripts may invoke the domain API; Groovy is not responsible for credentials, connection lifecycle or arbitrary SQL.
- Run shared behavioral tests, rollback/restart tests and authorization tests against each actual supported backend. A leftover adapter whose optional plugin contract is absent does not break unrelated repositories. Missing required implementations fail before the dependent service starts; never silently emulate a different backend's behavior.
RepositoryAdapter.schemaVersion() is persisted per contract in storage_modules.
Version 1 adopts the current schema as baseline without historical conversion.
A different recorded version is rejected before opening the repository. Future
schema changes need an explicit migration design; neither a new adapter nor a
new plugin version silently upgrades/downgrades tables. H2 DDL and file publication
are not advertised as one crash-atomic transaction.
FileEcho obtains FileEchoStorage through the mail storage's RepositoryAccess.
The functional plugin contains immutable area/access/creation-policy values and
domain validation; it contains no SQL, ORM mappings or concrete mail-facade casts.
Robot settings use the separate EditableRobotRules capability.
fileecho-storage-h2 registers the H2 implementation through repository SPI. It
borrows the DB provider's existing source and does not open or close a separate
database. Area creation and its initial access grant are one transaction; a
changed creation policy rejects the operation. Existing areas never acquire
additional grants through automatic creation. Configured initial areas remain
insert-only, preserving UI edits and disabled records on restart.
The original fileecho_area, fileecho_access and fileecho_creation tables,
including their keys, are adopted in place at schema version 1. No export/import
or reset is required. The old extensionDao/extensionTransaction API is removed.
Only H2 has an implementation today; another SQL provider must supply an adapter
for this contract. Missing capability/adapter errors stop initialization rather
than silently changing the configured source.
Full and Headless include the adapter JAR in plugins/. Legacy and Transit
exclude it and retain FileEchoSource=files, without opening a SQL provider.
Do not add the adapter to FileEcho's main runtime dependencies: that would bring
SQL infrastructure into SQL-free distributions.
Standalone ORM constructors remain available to isolated tests and offline tools. Their scoped extension bridge uses that ORM store's location. Normal node startup uses provider-owned connections and persistent schema metadata.
Legacy/Transit do not open a SQL provider or substitute a dummy database. Neutral contracts can be present in these bundles; H2/ORM/SQL adapters remain excluded.
Load the development environment, then run relevant checks:
.\gradlew.bat :storage-api:test :storage-h2:test :fido-config:test
.\gradlew.bat :fileecho-storage-h2:test :fileecho:test
.\gradlew.bat :fido-database:test :nodelist:test :rpc:test :rpc-node:test :web-ui:test
.\gradlew.bat :jftn-bundle:test --tests '*ProfileDistributionTest' --tests '*ServicePluginDistributionTest'
.\gradlew.bat spotlessCheck verifyJavaFormattingFormatting tasks can be run separately from frontend-producing tasks. Tests use isolated state; none of these commands updates an installed station's private data. UI/VRT suites are not required for this backend-only change.
