On this page
stado-database
A product asks Stado for its database's placement and credential coordinate
instead of hard-coding a provider. connect uses SeaORM for PostgreSQL,
MySQL and a fleet SQLite file; credentials gives products using other
engines the URL and certificate authority for their own driver. The
declaration and consumer grant use the same commands for every engine.
[dependencies]
stado-database = { git = "https://github.com/wisent-ai/stado", package = "stado-database" }
sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio-rustls", "macros"] }
let database = stado_database::FleetDatabase::for_product("handtohuman", "HANDTOHUMAN_FLEET_HOME")?;
let connection: sea_orm::DatabaseConnection = stado_database::connect(&database).await?;
Tables are SeaORM entities in the product; the schema changes through a
sea-orm-migration migrator the product runs at startup.
Synchronous code
A command-line tool, or a service whose row operations are plain functions,
uses stado_database::sync::Client instead of writing a client of its own:
use stado_database::{params, sync::{Client, OptionalExtension}};
let client = Client::connect(&stado_database::FleetDatabase::for_product("grant-cli", "GRANT_FLEET_HOME")?)?;
client.execute("UPDATE sources SET last_synced_at = $1 WHERE id = $2", params![now, id])?;
let name: Option<String> = client
.query_row("SELECT name FROM sources WHERE id = $1", [id], |row| row.get("name"))
.optional()?;
let transaction = client.transaction()?;
transaction.execute("DELETE FROM watches WHERE id = $1", [id])?;
transaction.commit()?; // dropped without commit, it rolls back
The client runs the SeaORM connection on a runtime of its own and waits for
each statement; on a multi-threaded Tokio worker it leaves the worker for the
wait. Error::is_unique_violation says an insert hit a unique constraint, and
Row::json gives a whole row as a JSON object.
A product whose tables are SeaORM entities but whose callers are synchronous
hands its entity work to the same client with run, and runs its migrator
the same way; it keeps no runtime or connection thread of its own:
let client = Client::connect(&FleetDatabase::for_product("jeden", "JEDEN_STADO_HOME")?)?;
client.run(|db| async move { Migrator::up(&db, None).await })??;
let row = client.run(move |db| async move { value::Entity::find_by_id(key).one(&db).await })??;
run answers the closure's own result inside sync::Result; its only error
of its own is a task that stopped before answering.
What connect does
stado database resolve <product> --consumer <product> --jsonnames the Skarbiec item holding the database (<product>-database).stado service directory connect skarbiec --consumer <product> --jsonnames the Skarbiec route on this host.stado credentials get <item> --field pooler_urland--field ca_certificate, run as consumer<product>-database-clientwith the bearer in~/.stado/<product>-database-client-skarbiec-token, give the database URL and the certificate authority it is verified against. Asqlite://URL names a file, which has no server to verify, so no certificate is read. For Postgres it also reads--field session_url.- The pool connects with
sslmode=verify-fullagainst that certificate (mysql://URLs withVerifyIdentity). Postgres is opened onsession_url, notpooler_url.
Why Postgres opens on session_url
A Supabase database's pooler_url is its pooler in transaction mode: each
transaction may run on another server connection. SeaORM sends every
statement through sqlx as a named prepared statement (sqlx_s_1, sqlx_s_2,
…), and on a transaction-mode pooler the next client finds that name already
taken: prepared statement "sqlx_s_1" already exists. session_url is the
same pooler in session mode (Supabase's session port), where one client
connection keeps one server connection, so a statement's name stays its own.
A fleet Postgres database and an external one are reached directly, so their
session_url is the server's own URL. stado database create writes it for
every provider; stado database adopt writes it into an existing Supabase
item, and create again writes it into an existing fleet or external one.
pooler_url stays the transaction-mode route for a service that delivers it
as DATABASE_URL.
connect opens the three engines SeaORM speaks: postgres:// or
postgresql://, mysql://, and sqlite://<file> for a fleet SQLite
database. A SQLite file is opened as it is, never created, so a consumer on a
host that does not hold it is refused at step connect with the file named.
A database of any other engine (MongoDB,
Redis, SQL Server, ...) is refused by connect at step read pooler_url
with its scheme named; the product calls stado_database::credentials, which
does steps 1 to 3 and answers item, pooler_url and ca_certificate, and
opens the server with that engine's own driver, verified against the
certificate authority.
On a machine without Stado or Skarbiec, set <PRODUCT>_DATABASE_URL (the
product name upper case, dashes as underscores) and
<PRODUCT>_DATABASE_CA_FILE, the PEM bundle the server is verified against;
a sqlite:// URL needs no certificate file.
connect then uses them and runs none of the steps above. A server URL without its
certificate file, or a file that cannot be read, is refused with the variable
named.
Creating a database
stado database create places PostgreSQL or SQLite on a fleet host, can
provision PostgreSQL through Supabase, or adopts an existing server of any
engine through --provider external. The default is fleet PostgreSQL, so
the operator need not choose a vendor:
stado database create <product> --consumer <product>
| flag | default | what it chooses |
|---|---|---|
--engine |
postgres (fleet, supabase); the URL's engine (external) |
postgres or sqlite (fleet), postgres (supabase); external: any engine, named by the connection URL, which --engine only confirms |
--provider |
fleet |
fleet: Stado runs the database itself on a fleet host, no vendor and no bill; supabase: a hosted Postgres project; external: a server of any engine the user already runs anywhere (Postgres, MySQL, MongoDB, Redis, SQL Server, ...; RDS, Cloud SQL, Neon, PlanetScale, Atlas, Azure, their own server) |
--ca-certificate |
none | external only: PEM bundle of the server's certificate authority; refused without it |
--host |
the vault owner | fleet host the database is placed on |
--port |
none; required for a fleet Postgres | port a fleet Postgres listens on; Stado assumes none |
--anchor |
the one organization and region every project the Supabase token sees shares | supabase only: declared database whose organization and region the project joins; a token that sees projects in several is refused with each named |
--accept-monthly-usd |
none | supabase only: the monthly compute cost accepted; refused without it |
The Supabase commands (create --provider supabase, adopt, destroy) call
the management API with field value of the vault item that plays role
supabase-management. A fleet where no item plays it is refused with the
two commands that give it: stado credentials item retag --host <owner> <ITEM> --tags stado:role:supabase-management and stado credentials grant add --host <owner> --role supabase-management --field value --token-file <bearer file> <consumer>.
create --provider supabase reuses a project of the same name, so it reads
the token's projects first; an answer to GET /projects that is not a project
list is refused (whether <name> exists is unknown; nothing was created)
rather than read as no projects, which would create a second, billed one.
adopt refuses the same answer, a key listing that is not a list, a key that
comes back without its value, and a project listed without its ref, so no
item is written with an empty key or from an empty project path.
When Supabase answers a project's pooler without its user or host, the item
gets no pooler_url or session_url and carries pooler_unreported with the
reason instead; the password create generated is kept, and stado database adopt reads the pooler again.
A fleet database is placed by stado database place <name> [--engine] --port <port> (the port only for Postgres), which create runs on the
chosen host through the host channel.
place initialises the data under ~/.stado/databases/<name>/ on that host
and writes the credential item <name>-database into the owner vault. For
Postgres it runs initdb with a generated password, issues a certificate
authority of the database's own and a server certificate it signs for the
host's routable address and loopback, accepts only TLS connections with a
password, stores pooler_url and session_url (both the server's URL),
ca_certificate, db_user and db_password
on the item, and installs the managed unit <name>-database that runs
postgres on that data. For SQLite it creates the file and stores host,
path and pooler_url (sqlite://<file>); connect opens it for a
consumer on that host. A database already placed
is reported as reused and never initialised twice. Every provider declares
the database for its consumers the same way.
An existing server of any engine is brought in with --provider external.
The connection URL is read from standard input, so it never appears in the
process table or shell history:
stado database create <product> --consumer <product> --provider external \
--ca-certificate <server-ca.pem> < <file holding the connection URL>
The URL's scheme names the engine: postgres:// or postgresql:// is
Postgres, mysql:// MySQL, mongodb:// or mongodb+srv:// MongoDB,
redis:// or rediss:// Redis, mssql:// SQL Server, and so on for any
engine. --engine, when given, must agree with it. The item <name>-database
then holds pooler_url (that URL; a Postgres one also as session_url),
ca_certificate (the bundle), host,
engine and provider: external, the same fields a consumer reads from any
other database. Running it again re-points the item at the URL given. The
stado-database crate opens it like any other: connect opens Postgres and
MySQL (a mysql:// URL as a MySQL pool verified with VerifyIdentity, and
the synchronous Client binds parameters in the connection's own dialect),
and credentials hands any other engine's URL and certificate authority to
that engine's own driver.
Refusals name what failed: --anchor or --accept-monthly-usd with
--provider fleet (a fleet database has no vendor bill), an --engine other
than postgres with --provider supabase (supabase runs Postgres only),
--host or --port with --provider supabase, --host, --port,
--anchor or --accept-monthly-usd with --provider external (the server is
already placed), --provider external without --ca-certificate, a
certificate file that holds no PEM certificate, empty standard input, a URL
with no host, a URL whose engine differs from --engine, --ca-certificate
with another provider, a sqlite engine with --provider external, any engine
other than postgres or sqlite with --provider fleet (named with
--provider external as the way in), an unknown provider, an engine name that
is not a lowercase URL scheme,
a fleet Postgres without --port (refused before anything is initialised), a host
without initdb, postgres or openssl on its PATH (the PATH is printed),
a host with no routable address, an initialised data directory whose unit is
missing (the exact stado service deploy line that installs it is printed),
and each failed initdb or openssl step with its own error output.
Fleet PostgreSQL certificate policy
A new PostgreSQL placement requires the placement host to declare database.postgres_tls, the certificate validity and key size its authority and server certificate are issued with: Fleet PostgreSQL certificate policy.
Stado Desktop
Databases → Create… runs the same stado database create. It offers the
provider (fleet, supabase or external) and an optional engine; left
empty, the engine is postgres on fleet and supabase and the connection URL's
own engine on external. Fleet adds an optional host (empty: the vault owner);
supabase adds the accepted monthly cost, and with it empty the button reads
Read cost and the CLI's sentence naming what one more project adds to the
bill appears above the form; external adds the connection URL, which goes to
the CLI's standard input and never into its arguments, and the path of the
server's CA certificate. Declare… takes any engine name. A row's menu
holds Remove…, which withdraws the declaration, and Destroy…, the
inverse of Create…: Destroy runs stado database destroy, and Destroy
and delete its Supabase project adds --delete-project. Every refusal is
the CLI's own sentence under The database plane refused.
Provisioning a product once
stado database create <product> --consumer <product>
stado credentials token mint <product>-database-client --host <vault owner> \
--capabilities 'read:<product>-database#pooler_url,read:<product>-database#ca_certificate,read:<product>-database#session_url' \
--audience <product>-database-client --token-file-name <product>-database-client-skarbiec-token
stado credentials token sync <product>-database-client --from-host <vault owner> --host <this host> \
--source-token-file <owner home>/.stado/<product>-database-client-skarbiec-token \
--token-file ~/.stado/<product>-database-client-skarbiec-token --shared-vault
stado service directory consumer add skarbiec <product> --target <this host>
A client minted before session_url existed may read only pooler_url and
ca_certificate, and connect is refused at read credential field with
Skarbiec's 403. stado database client <product> widens that client's grant
on the vault owner to every field the crate reads for the database's engine
(pooler_url; ca_certificate for a server; session_url for Postgres). It
keeps the bearer, so the copies other hosts hold keep working, and running it
again changes nothing. A client the vault owner holds no bearer for is refused
with Skarbiec's answer. Stado Desktop's database row menu runs it as Grant
library client reads.
Refusals
Every failure is the fleet database could not be reached at step <step>: <detail>, where the step is one of locate Stado, resolve database,
resolve Skarbiec route, read credential field, read pooler_url,
read session_url or connect, and the detail carries Stado's own answer.
A Postgres item without session_url reads step read credential field: stado credentials get <product>-database --field session_url ... exited ...; a Postgres item carries session_url, the route that keeps one server session per connection: stado database adopt writes it for a Supabase database,stado database create for a fleet or external one. For example an
undeclared database reads step resolve database: stado database resolve <product> ... exited 2: Error: unknown database "<product>"; declared: ...,
a route the resolver has not bound yet reads step resolve Skarbiec route: ... did not answer at http://127.0.0.1:<port>, a SQLite database opened off
its host reads step connect: opening <product> at <file> through <product>-database#pooler_url failed (a fleet SQLite file opens only on the host that holds it): ..., and connect on a MongoDB
database reads step read pooler_url: <product>-database#pooler_url is a mongodb URL; connect opens postgres, mysql and sqlite through SeaORM, so open this engine with its own driver from stado_database::credentials.
Every database command
Every stado database command, what it does and how it refuses: Every database command.
Source: this website