Databases

Built-in engines and your own database servers, with users, grants, snippets and a sign-in-free admin tool.

In preview databases version 0.2.0 Part of Databases

Actions

46 actions, callable from the panel, the command palette and the API as POST /api/v1/a/<id>. Internal actions used between modules are not listed.

ActionWhat it doesRiskPreview
databases.engine.install Install a database engine critical No dry run
databases.engine.list List installed engines (read) low No dry run
databases.db.create Create a database medium
databases.db.list List databases (read) low No dry run
databases.db.get Get a database (read) low No dry run
databases.db.stats Refresh database sizes low No dry run
databases.db.delete Delete a database high
databases.db.export Export a database low No dry run
databases.db.import Import a dump medium No dry run
databases.user.create Create a database user medium
databases.user.list List database users (read) low No dry run
databases.user.delete Delete a database user medium
databases.user.password.set Reset a user's password medium No dry run
databases.user.password.reveal Reveal a vaulted password (read) high No dry run
databases.grant.set Set a user's access to a database medium
databases.remote.set Allow remote connections high
databases.valkey.create Create a Valkey instance medium
databases.valkey.list List Valkey instances (read) low No dry run
databases.valkey.update Change a Valkey instance medium
databases.valkey.reset Reset a Valkey password medium No dry run
databases.valkey.stats Valkey usage (read) low No dry run
databases.valkey.delete Delete a Valkey instance high
databases.server.create Register an external database server medium No dry run
databases.server.list List external database servers (read) low No dry run
databases.server.get Get an external database server (read) low No dry run
databases.server.update Change an external database server medium No dry run
databases.server.test Test an external database server low No dry run
databases.server.delete Remove an external database server high No dry run
databases.db.snippets Connection snippets (read) low No dry run
databases.admin.open Open the database admin tool medium No dry run
databases.reconcile Re-apply all databases medium No dry run
databases.config.reset_all Reset the whole service to defaults high
databases.config.snapshot.list Snapshots before resets (read) low No dry run
databases.config.snapshot.restore Undo a reset (restore a snapshot) high
databases.config.history.list Earlier versions of a file (read) low No dry run
databases.config.history.diff Compare an earlier version (read) low No dry run
databases.config.history.restore Restore an earlier version of a file high
databases.unmanaged.scan Scan now for objects made outside the panel low No dry run
databases.unmanaged.list Objects made outside the panel (read) low No dry run
databases.unmanaged.ignore Ignore or un-ignore an unmanaged object low
databases.unmanaged.adopt Adopt an object made outside the panel medium
databases.unmanaged.remove Remove an object made outside the panel high
databases.config.status Hand-edited database server files (read) low No dry run
databases.config.adopt Keep a hand edit of a database server file medium
databases.config.reapply Render a database server file again high
databases.config.unmanage Hand a database server file off to the administrator high

Permissions and limits

Permissions

  • databases.db.view View databases, users and instances
  • databases.db.manage Create and delete databases
  • databases.user.manage Manage database users and passwords
  • databases.grant.manage Change database grants
  • databases.remote.manage Allow remote connections
  • databases.export.run Export (dump) databases
  • databases.import.run Import (restore) databases
  • databases.valkey.manage Manage Valkey instances
  • databases.engine.manage Install database engines on servers
  • databases.reconcile Re-apply every database to its server
  • databases.server.view View external database servers
  • databases.server.manage Register and change external database servers
  • databases.admin.open Open the database admin tool (single sign-on)

Plan limits

  • databases.count Databases
  • databases.users Database users
  • databases.size_mb Total database size
  • databases.connections Connections per database user
  • databases.remote_hosts Remote host rules
  • databases.valkey_instances Valkey instances
  • databases.valkey_mb Valkey memory (all instances)
  • databases.servers_sql External MySQL/MariaDB/PostgreSQL servers (0 = not allowed)
  • databases.servers_clickhouse External ClickHouse servers (0 = not allowed)
  • databases.servers_redis External Redis/Valkey servers (0 = not allowed)
  • databases.servers_mongodb External MongoDB servers (0 = not allowed)

Engineering notes

Generated from modules/databases/docs.md at build d90e9e2. These are the notes the engineers keep next to the code: precise, technical, and honest about what is not done yet.

Customer database servers are installed on the node by the driver (internal/agent/databases*.go); the panel's own Postgres is unrelated. Listeners are loopback-only until some user has remote hosts.

Model

  • Names are <account>_<name> (account - becomes _); database <= 48, user <= 32 characters.
  • Privileges (readonly / readwrite / admin) replace each other on databases.grant.set. MariaDB: SELECT / DML / ALL on `acct\_db`.* (underscore escaped; no GRANT OPTION). PostgreSQL: per database three NOLOGIN group roles <db>_own (owner; admin users are members and log in with SET ROLE, so what they create is owned by the group), <db>_rw, <db>_ro, plus default privileges for new tables.
  • Passwords are generated (24 chars), returned once by create/reset, never stored, never in list/get. With store_in_vault the vault keeps it and databases.user.password.reveal returns it (audited). Supplied passwords: 12-128 chars, letters+digits, no quotes/backslash/space/backtick. Top-level password keys are redacted by the host audit.
  • Remote access (databases.remote.set): IPv4/IPv6 addresses or IPv4 CIDR; %//0 and ranges wider than /16 admin-only. MariaDB: extra 'user'@'host' entries (CIDR becomes addr/netmask) + REQUIRE SSL; PostgreSQL: hostssl/host lines in rc-hba.d/<user>.conf (included from pg_hba.conf, PG 16+). The engine listens on 0.0.0.0 only while some user has remote hosts (restart on change). TLS uses a self-signed cert generated by the agent (ssl module cert later). The firewall rule belongs to the security module (not built): the plan warns about it.
  • Limits: databases.count/users/valkey_instances/valkey_mb/remote_hosts Reserve/Release; databases.connections becomes MAX_USER_CONNECTIONS / role CONNECTION LIMIT (read from the plan by probing Reserve, see follow-ups); databases.size_mb is enforced by databases.db.stats (reserves growth; when refused the database's grants are forced read-only, restored when it fits again; status quota_exceeded).
  • Dump/restore: databases.db.export writes .sql/.sql.gz into the account's home, databases.db.import reads it. All file access runs as the account via runuser (no symlink escape by a root agent). Import runs as an ephemeral user with rights on that database only (cannot CREATE USER / touch other DBs; verified).
  • Valkey: rc-valkey@<account>_<name> from a template unit, User=<account>, Slice=rc-acct-<account>.slice, loopback port from a sequence + unix socket in ~/valkey/<name>/, user default off, ACL user with a sha256 hash (no clear password on disk), dangerous commands denied, maxmemory from the request (counted against databases.valkey_mb), MemoryMax = 2x+64M. An rc-agent ACL user (token only in /etc/rc-valkey) feeds stats.
  • Desired state first, then apply (idempotent); databases.reconcile re-applies everything keeping passwords; accounts.deleted subscription removes everything of the account (durable, idempotent).

Verified in the lab (2026-10-09, Ubuntu 24.04: MariaDB 10.11, PostgreSQL 16, Valkey 7.2)

  • Create db+user on both engines; connect from a second container over TLS with the generated password; wrong password refused; before remote.set the port is closed; with require_tls a non-TLS client is refused (both engines).
  • databases.count limit 2: third create -> limit_exceeded; valkey instance limit likewise; connection limit 3: fourth concurrent session -> max_user_connections error.
  • readonly user cannot INSERT; readwrite can, cannot CREATE; password reset invalidates the old one; vault reveal.
  • Export -> drop table -> import restores rows on both engines (gz and plain), ephemeral roles gone; a dump with CREATE USER is refused; ../ paths refused.
  • Size quota flips a PostgreSQL database read-only (an empty PG database is ~7 MB).
  • Reconcile recreated a database and user dropped behind the panel's back; remote off closes the listener again; Valkey runs as the account in its slice, CONFIG is denied; account deletion removed DBs/users/roles via the event.

External (bring-your-own) servers — w6-04

  • databases.server.create registers a MySQL/MariaDB, PostgreSQL, MongoDB, Redis/Valkey or ClickHouse server: host, port, tls (off | on = encrypted, certificate not checked | verify = checked, optionally against a pasted CA) and the admin login. It is TESTED ON SAVE through the account's node (databases.server.test agent op: version, encryption, latency); a failing test saves nothing. The admin password goes to the vault (server/<id>) and is never returned, logged, audited or put in an event; it reaches the client only in its environment (MYSQL_PWD, PGPASSWORD, an HTTP header, RESP AUTH), never argv or SQL (unit-tested).
  • Plans: limits databases.servers_sql / _clickhouse / _redis / _mongodb (count; 0 = that kind is not allowed).
  • Every existing action takes server_id (db.create, user.create, lists); the engine is the server's kind. Names keep the <account>_ prefix. Users on an external MySQL/MariaDB get host % unless remote.set restricts them. Per kind: MySQL/MariaDB (flavour auto-detected: MySQL 8 copies hashes as IDENTIFIED WITH plugin AS 0x..) and PostgreSQL (same <db>_own/_rw/_ro model; a newer server brings the matching PGDG pg_dump) support everything incl. export/import/size; ClickHouse (HTTP interface: databases, SQL users, GRANT on db.*, sizes) has no export/import; Redis/Valkey: a "database" is the key namespace <name>:*, users are ACL users, readonly = %R~; MongoDB: register + connectivity test only (management is a follow-up). The agent refuses loopback / link-local / metadata hosts and names resolving to them (use the built-in engine for this node).
  • Deleting a server is refused while databases/users are registered on it; deleting an account forgets its servers (the servers themselves are never touched).
  • databases.db.snippets: PHP, Node, Python, Go, Ruby, Java and .NET for every family, with a PASSWORD placeholder.

Built-in engines on demand (databases.engine.install)

mysql (Oracle 8.4 LTS apt repo; refused while MariaDB owns :3306), postgresql + version 14-18 (PGDG), clickhouse (vendor repo; loopback; the passwordless default user is removed, rc_admin credentials in /etc/rc-clickhouse/admin.json 0600), qdrant (pinned release + SHA-256, loopback, API key in /etc/rc-qdrant/env), ferretdb (v1.24 pinned, over the node's PostgreSQL: a FerretDB "database" is a PostgreSQL database + user, clients use the PostgreSQL login with authMechanism=PLAIN). Repos and download URLs are fixed in databases_engines.go; keys are fetched over https.

Admin tool (Adminer) single sign-on

databases.admin.open (database or external server) -> agent op databases.admin.session: installs Adminer (Ubuntu/Debian package, Apache-2.0/GPL-2) + php-cli on demand, runs it with php -S on 127.0.0.1:17977 as system user rc-adminer (hardened unit), writes the login into a 0600 handoff file on tmpfs and returns a 60 s, single-use, HMAC-signed token (/?rc_sso=<token>). The page consumes the handoff, keeps the credentials in its server-side session and signs Adminer in through its own login POST with an EMPTY password field: the browser never sees a password. Built-in databases get an ephemeral rc_sso_* login limited to that database, swept by the schedule databases.admin.sweep (5 min). The panel has no reverse proxy yet: the action returns {node_id, port, path} for it (see handover).

Follow-ups closed

import takes clean (drops everything first: MariaDB/MySQL drop+create keeping charset, PostgreSQL recreates public); size enforcement runs from the schedule databases.stats.run (15 min, one unreachable server does not stop the rest).

Next

UI (w6-08); panel reverse proxy for the admin tool; MongoDB management on external servers; Qdrant collections/keys per account; ClickHouse/Redis dump; pin repo key fingerprints; grace period for deletes (AC-18), clone/rename, maintenance ops, slow-query log; firewall rule via security.