Postgres databases

velixir runs managed PostgreSQL inside the same K3s clusters that run your apps. Provisioning takes about a minute. Write-ahead logs ship to object storage continuously for point-in-time recovery.

Provision a database

From Databases in the dashboard, click New database. You choose:

  • A name for the instance - how it shows in the dashboard. This is not the database name.
  • A plan (CPU + memory + disk).
  • The PostgreSQL version (17, 16, or 15).
  • Optionally a database name and username - both default to app. They create the initial database and its owner role, and are fixed at creation time (Postgres can't rename the bootstrap database afterwards).
  • High availability on / off - HA runs a primary plus a synchronous standby replica on separate nodes, with automatic failover.
  • External access on / off - reach the instance from outside the cluster (psql, migrations, BI tools). In-cluster app connections work either way.

We use CloudNativePG as the operator. It handles primary election, streaming replication, and backups.

Connecting from your app

The cleanest pattern: bind the database to your app. From the database's detail page, under Bound apps, pick the app and click Bind. velixir injects the connection string into the app on its next deploy.

When you bind you choose the env var name (it defaults to DATABASE_URL) and the format - an ADO.NET keyword string for .NET / Npgsql / EF Core / Dapper, or a postgres:// URL for other stacks. velixir injects exactly that one variable, with the live connection string, on the app's next deploy. To attach a second database, bind it under a different name (e.g. ANALYTICS_DATABASE_URL). If you set an app env var with the same name yourself, your value wins. From the CLI: velixir db bind <db-id> --app <app-id>, adding --format adonet for the keyword form.

// .NET, with the database bound in the ADO.NET format
// (Npgsql does not accept a postgres:// URL)
var connString = Environment.GetEnvironmentVariable("DATABASE_URL");
builder.Services.AddDbContext<MyDbContext>(opt =>
    opt.UseNpgsql(connString));

Connecting from Node.js with pg: the URL ends in sslmode=require, which recent versions of node-postgres treat as "verify the certificate" against the public CAs. Your database's certificate comes from its own CA, so give pg that CA and it verifies properly (see Verifying the server certificate below).

Connecting from outside the cluster

For psql / DBeaver / Rider sessions: open the database's detail page and Reveal the external connection string (external access must be enabled on the instance), or run velixir db connect <id> --external. The connection passes straight through the edge to Postgres, encrypted end to end, so it works from anywhere. The username and password are the same as the in-cluster ones - only the host and port differ.

Verifying the server certificate

Every connection is encrypted. sslmode=require (what the connection strings use) encrypts without checking who's on the other end. To check that too, pin your database's CA certificate and use sslmode=verify-full.

Each database has its own CA. Download it with Download CA certificate on the database's page, with velixir db ca <id> --out yourdb-ca.crt, or from the API at GET /api/v1/databases/{id}/ca-certificate. The server certificate it signs names the in-cluster host and, when external access is on, the external host.


# psql

psql "postgres://app:[email protected]:31000/yourdb?sslmode=verify-full&sslrootcert=yourdb-ca.crt"

// Node.js (pg): strip sslmode from the URL and pass the CA
const fs = require('fs');
const url = new URL(process.env.DATABASE_URL);
url.searchParams.delete('sslmode');
const pool = new Pool({
  connectionString: url.toString(),
  ssl: { ca: process.env.DATABASE_CA_CERT ?? fs.readFileSync('yourdb-ca.crt', 'utf8') },
});

// .NET (Npgsql)
"Host=...;Database=...;Username=...;Password=...;SSL Mode=VerifyFull;Root Certificate=yourdb-ca.crt"

To give an app the CA without committing a file, store the certificate's text in an environment variable (multi-line values are kept intact) and read it from there, as in the Node example. A restored database is a new cluster with a new CA, so download the certificate again after a restore.

psql tip: use the URL form of the connection string (psql 'postgres://...'), not the ADO.NET keyword form - psql doesn't parse the latter. Make sure the database name is on the end of the URL (.../yourdbname); otherwise psql connects to the default postgres maintenance database instead of yours, and you'll see only the built-in schemas.

Credentials & rotation

Each instance has one role - the username and password you set at creation (default app) - used for both in-cluster and external connections. You can rotate the password anytime from the database's detail page: the old password stops working immediately, and any bound apps pick up the new credential on their next deploy (the dashboard tells you how many to redeploy). Use it for leak response or routine hygiene.

Databases, schemas & tables (note for MySQL users)

PostgreSQL has three levels where MySQL has two: database → schema → table. You connect into one database (the database name you set, default app) and your tables live in a schema inside it. Every database ships with the same built-in schemas - public, information_schema, pg_catalog, pg_toast - and unless you say otherwise your tables go in public. So a table is addressed as public.widgets, not mydb.widgets.

Translating from MySQL:

  • A MySQL database (mydb.widgets) maps to a PostgreSQL schema (public.widgets). To namespace tables the way you'd use separate MySQL databases, create schemas inside one database: CREATE SCHEMA billing; then billing.invoices. Cross-schema joins work normally.
  • A PostgreSQL database is one level up and fully isolated - you can't join across databases in a single connection (that needs postgres_fdw). Reach for one database, many schemas where you'd have reached for many MySQL databases.
  • There's no USE otherdb; - switching databases means a new connection (\c in psql opens one).

Quick checks once connected: SELECT current_database(); (which database you're in), \dn (schemas), \dt (tables on the search path).

Backups & recovery

Backups are handled by CloudNativePG's continuous WAL archiving to object storage, combined with periodic base backups. That's what gives you point-in-time recovery within your plan's retention window (7 days on Starter, 14 on Standard, 28 on Pro/Scale). Need longer? Take your own pg_dump exports - the window covers operational recovery, not archival. The Backups section of the database's page shows the backups taken for your instance.

Restore is self-serve from the Backups section of the database's page. Pick a backup, optionally enter a point-in-time within your retention window, and velixir provisions a brand-new database recovered to that point - your current instance is never touched, so you can verify the restore before switching apps over. Because recovery replays the continuously-archived WAL, you can target any moment in the window, not just a backup boundary. The recovered database keeps the same database name, username, and password, but it's a new instance with its own host and port: to switch over, bind it to your apps (under the same env var name) and redeploy them.

Plans

Database plans set per-instance resources (and per-replica resources in HA mode). You can change a plan with about a minute of downtime - we re-create the StatefulSet pod with the new resource limits.