Actions
54 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.
| Action | What it does | Risk | Preview |
|---|---|---|---|
databases.engine.install | Install a database engine | critical | |
databases.engine.tune | Size a database engine for this server | high | |
databases.valkey.tune | Size the panel's Valkey | high | |
databases.engine.list | List installed engines (read) | low | No dry run |
databases.engine.offered | Engines this account's server runs (read) | low | No dry run |
databases.slowlog.list | List slow queries (read) | low | No dry run |
databases.query.run | Run a statement | medium | |
databases.maint.run | Database maintenance | medium | |
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 | |
databases.db.snippets | Connection snippets (read) | low | No dry run |
databases.admin.open | Open the database tool | medium | |
databases.admin.session.list | Open database tool sessions (read) | low | No dry run |
databases.admin.close | Close a database tool session | low | |
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.viewView databases, users and instancesdatabases.db.manageCreate and delete databasesdatabases.user.manageManage database users and passwordsdatabases.grant.manageChange database grantsdatabases.remote.manageAllow remote connectionsdatabases.export.runExport (dump) databasesdatabases.import.runImport (restore) databasesdatabases.query.runRun statements in the query consoledatabases.valkey.manageManage Valkey instancesdatabases.engine.manageInstall database engines on serversdatabases.reconcileRe-apply every database to its serverdatabases.server.viewView external database serversdatabases.server.manageRegister and change external database serversdatabases.admin.openOpen the database tool
Plan limits
databases.countDatabasesdatabases.usersDatabase usersdatabases.size_mbTotal database sizedatabases.connectionsConnections per database userdatabases.remote_hostsRemote host rulesdatabases.valkey_instancesValkey instancesdatabases.valkey_mbValkey memory (all instances)databases.servers_sqlExternal MySQL/MariaDB/PostgreSQL servers (0 = not allowed)databases.servers_clickhouseExternal ClickHouse servers (0 = not allowed)databases.servers_redisExternal Redis/Valkey servers (0 = not allowed)databases.servers_mongodbExternal MongoDB servers (0 = not allowed)
Engineering notes
Generated from modules/databases/docs.md at build e0cdf300. 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 ondatabases.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 withSET 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_vaultthe vault keeps it anddatabases.user.password.revealreturns it (audited). Supplied passwords: 12-128 chars, letters+digits, no quotes/backslash/space/backtick. Top-levelpasswordkeys are redacted by the host audit. - Remote access (
databases.remote.set): IPv4/IPv6 addresses or IPv4 CIDR;%//0and ranges wider than /16 admin-only. MariaDB: extra'user'@'host'entries (CIDR becomes addr/netmask) +REQUIRE SSL; PostgreSQL:hostssl/hostlines inrc-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 thesecuritymodule (not built): the plan warns about it. - Limits:
databases.count/users/valkey_instances/valkey_mb/remote_hostsReserve/Release;databases.connectionsbecomesMAX_USER_CONNECTIONS/ role CONNECTION LIMIT (read from the plan by probing Reserve, see follow-ups);databases.size_mbis enforced bydatabases.db.stats(reserves growth; when refused the database's grants are forced read-only, restored when it fits again; statusquota_exceeded). - Dump/restore:
databases.db.exportwrites.sql/.sql.gzinto the account's home,databases.db.importreads it. All file access runs as the account viarunuser(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,maxmemoryfrom the request (counted againstdatabases.valkey_mb),MemoryMax = 2x+64M. Anrc-agentACL user (token only in /etc/rc-valkey) feeds stats. - Desired state first, then apply (idempotent);
databases.reconcilere-applies everything keeping passwords;accounts.deletedsubscription 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_tlsa non-TLS client is refused (both engines). databases.countlimit 2: third create ->limit_exceeded; valkey instance limit likewise; connection limit 3: fourth concurrent session ->max_user_connectionserror.- 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 USERis 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,
CONFIGis denied; account deletion removed DBs/users/roles via the event.
External (bring-your-own) servers — w6-04
databases.server.createregisters 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.testagent 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, RESPAUTH), 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%unlessremote.setrestricts them. Per kind: MySQL/MariaDB (flavour auto-detected: MySQL 8 copies hashes asIDENTIFIED WITH plugin AS 0x..) and PostgreSQL (same<db>_own/_rw/_romodel; a newer server brings the matching PGDGpg_dump) support everything incl. export/import/size; ClickHouse (HTTP interface: databases, SQL users, GRANT ondb.*, 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 aPASSWORDplaceholder.
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.
Database tool (Adminer) - w11-61, proven and ON by default in w11-85
databases.admin.open (one database + one of its users) -> a single-use ticket -> the editor gateway's isolated origin -> Adminer 6.1.1
(pinned in the version catalogue, sha256-checked) running as the ACCOUNT's own user in its slice with no network, behind a
root-owned socket. PHP is the node's own php-cli (the web server install brings it; the first open adds the PostgreSQL driver
through apt once). The login is ephemeral, has the chosen user's privilege only and is never shown, stored or audited. Sessions are
listed on the database's "Database tool" tab with Close; a session ends with the panel session, after 30 minutes without use, at
the end of its life (admin_tool_session_hours, 4) or when its link was never opened. Built-in MariaDB, MySQL and PostgreSQL
databases only; opens in its own tab (not framed), not made for a phone. Setting admin_tool_enabled (default on) switches it off
for the whole server and closes every open session at the next sweep. Proof, limits and the bugs the real sign-in found:
docs/security/database-tool.md; the lab walk is lab/tests/database-tool.sh.
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.