todos table with a sqlx-cli migration and serve create, read, update, and delete queries from a small axum server. Every connection uses TLS with full certificate verification.
Prerequisites
- Rust 1.94 or later. This guide was tested with Rust 1.98.1,
sqlx0.9.0,sqlx-cli0.9.0, andaxum0.8.9. - A ClickHouse Cloud account
psql, to create the database. You can also run theCREATE DATABASEstatement in the SQL console.
Create a ClickHouse Managed Postgres service
In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.Get your connection details
Click Connect in the left sidebar of your service. Keep Directly selected and turn on Use SSL. The connection URL on the url tab now ends insslmode=verify-full&sslrootcert=<service-name>-ca-certificate.pem, and the port is 5432.
Click Download CA certificate. You can also download it later from Settings → CA Certificate. The certificate is unique to your instance, so the driver can use it to verify that it’s talking to your server.
This guide connects directly to Postgres on port 5432, for both the app and sqlx-cli. An axum server is a long-lived process, and PgPool keeps its own pool of connections, so it doesn’t need a second pooler in front of Postgres. Migrations should always use the direct connection. If you run many app instances, see Use PgBouncer.
Set up the project
Create a project and addsqlx with the Postgres driver, the Tokio runtime, and the rustls TLS backend, plus axum and a few helper crates:
sqlx-cli with the same TLS backend:
Configure the connection
Move the CA certificate you downloaded into the project directory and rename it toca-certificate.pem.
Create a database for the app. Replace <PASSWORD> and the host with the values from the Connect modal:
.env file in the project root, or update DATABASE_URL in your existing one, so that it points at the guide_rust database. Both the app and sqlx-cli read DATABASE_URL from it:
.env
sslmode and sslrootcert from the URL. With verify-full, it checks that the server certificate is signed by your instance’s CA and that it matches the hostname. The sslrootcert path is resolved relative to the directory you run commands from. Keep .env out of version control, and if your password contains characters such as @, /, or #, percent-encode them.
Create the table with a migration
Create a migration file:migrations directory and add the table definition:
migrations/<timestamp>_create_todos.sql
sqlx migrate run records applied migrations in the _sqlx_migrations table and only runs new ones on later calls. Commit the migrations directory with your code. To apply migrations when the app starts instead, call sqlx::migrate!().run(&pool).await? in main, as long as the pool uses the direct connection.
Build the API
Replacesrc/main.rs with a small axum server that runs one query per route. In an existing app, create the pool the same way as in main, and add the Todo struct, the handlers, and the routes to your own router:
src/main.rs
.bind as parameters ($1), never formatted into the SQL string. Each distinct SQL string is prepared once per connection and kept in a statement cache of 100 statements, which sqlx closes when it evicts them.
Run and verify
Start the server:pg_stat_ssl, and confirms that the session is encrypted. In a second terminal, create two todos, mark the first one done, and delete the second:
-w '\n' option only adds a line break after each response:
http://localhost:3117/todos in your browser to list the remaining todos:
guide_rust and then public, and click the todos table. The _sqlx_migrations table next to it holds the migration history. The todos table contains the remaining row:
Use PgBouncer
To connect the app through the bundled PgBouncer, select via PgBouncer in the Connect modal and use port6432 in the app’s DATABASE_URL. The same CA certificate works. For example, keep .env on the direct connection for sqlx-cli and set the variable when you start the app:
pg_stat_ssl reports PgBouncer’s own connection to Postgres, so the check prints none. Your app’s connection to PgBouncer is still verified: a wrong CA certificate fails with UnknownIssuer in the same way.
PgBouncer runs in transaction pooling mode, so each transaction can run on a different Postgres connection. With sqlx:
- Connecting requires
.extra_float_digits(None), as inmain.rs. By default, sqlx sendsextra_float_digitsas a startup parameter, and PgBouncer rejects the connection withunsupported startup parameter: extra_float_digits. For the same reason,PgConnectOptions::optionsfails withunsupported startup parameter in options. - Prepared statements work with the default settings. sqlx frees statements that it evicts from its cache with a protocol-level
Closemessage, which PgBouncer handles, also inside transactions. Don’t setstatement_cache_capacity(0): it isn’t needed, and with the cache turned off, sqlx still prepares named statements but never closes them. - Don’t use
.persistent(false)on queries that run outside a transaction. sqlx then prepares an unnamed statement and executes it in a second round trip, which PgBouncer can send to a different Postgres connection. The query can fail withunnamed prepared statement does not exist, or silently run another client’s statement and return wrong results. - Session settings made with
SET, including in a pool’safter_connecthook, don’t stay on your connection, and can leak to other clients. UseSET LOCALinside a transaction instead. - Migrations must use the direct connection.
sqlx-cliand thequery!macros can’t connect through PgBouncer, because they always sendextra_float_digits.sqlx::migrate!takes a session-level advisory lock (pg_advisory_lock), and through PgBouncer the unlock can reach a different Postgres connection. The lock then stays held on a pooled connection, and later migrations, including over the direct connection, hang until PgBouncer closes that connection.
Next steps
- Connection: connection strings, PgBouncer, and TLS
- Settings: Postgres and PgBouncer parameters, such as
max_connections - Read replicas: send read-only queries to a replica with a second
PgPool - Security: IP access lists and private networking