Database
Create and manage Postgres databases with ccp db.
Clusterbase provides managed Postgres databases, each running on its own isolated machine with a SQL-over-HTTP proxy for secure access.
Create a Database
ccp db create --name my-app-db
# ✓ Created database my-app-db
#
# ID db-c1cf9a9be667
# Host db-c1cf9a9be667.clusterbase.dev
# Token a9db7b185e40...
#
# ✓ Token saved to .ccp/config.json
# ✓ Injected DATABASE_URL + DATABASE_TOKEN into function fn-...
# ✓ Wrote 2 vars to .env (DATABASE_URL, DATABASE_TOKEN)If you run this from a project directory with a .ccp/config.json, the database credentials (DATABASE_URL and DATABASE_TOKEN) are automatically injected as environment variables into your linked function and written to your local .env file, so ccp dev and your deployed function see the same values. A redeploy merges env rather than replacing it, so these injected values are preserved server-side even when they're absent from your local .env.
DATABASE_URL is written as a bare host (db-<id>.clusterbase.dev) with no scheme. Prepend https:// in your handler code, which matches the pattern shown in the Querying from Handlers examples below.
List Databases
ccp db list
# • my-app-db ready db-c1cf9a9be667
# db-c1cf9a9be667.clusterbase.devShow Database Details
ccp db info db-c1cf9a9be667
# Database my-app-db
#
# ID db-c1cf9a9be667
# Status ready
# Host db-c1cf9a9be667.clusterbase.dev
# Database default
# User clusterbaseDelete a Database
ccp db destroy db-c1cf9a9be667
# Delete database db-c1cf9a9be667? This will destroy all data. (y/N)Use -y to skip the confirmation prompt.
Destroying a database also removes DATABASE_URL and DATABASE_TOKEN from your local .env file, mirroring the sync that ccp db create performs. This keeps the next ccp deploy from re-uploading the stale credentials and silently re-pointing the function at the destroyed database. The local cleanup runs only when the current project is linked to the database being destroyed (or has no database linked); destroying a different database by ID from a project linked to another one leaves that project's .env untouched.
ccp db destroy is the only supported way to delete a database. Each managed database runs on its own dedicated VM, and deleting that VM directly through the VM API (DELETE /api/v1/vms/{vm_id}) is refused with 409 vm_backs_managed_database, naming the database and pointing back here. The check fails closed: if the platform cannot determine whether a VM backs a database, it refuses the delete rather than risk it. Only the database surface tears down backups, routes, and env vars in the right order.
Interactive SQL Shell
Open an interactive SQL session with ccp db connect:
ccp db connectThis opens a terminal UI with a SQL prompt. Type queries ending with ; to execute them:
╭──────────────────────────────────────────────────╮
│ >_ ccp db connect (db-c1cf9a9be667) │
│ │
│ database: default │
│ host: db-c1cf9a9be667.clusterbase.dev │
╰──────────────────────────────────────────────────╯
› SELECT * FROM users;
id │ name │ email
───┼───────┼──────────────
1 │ Alice │ alice@co.com
(1 row)
43ms
› Enter SQL (end with ;)Meta-commands
| Command | Description |
|---|---|
\dt | List all tables |
\d <table> | Describe a table (columns, types) |
\q | Quit |
Navigation
- Up/Down arrows — cycle through query history
- Ctrl+D or Ctrl+C — exit (clears input if non-empty)
One-Shot SQL
Execute a single SQL statement without opening the interactive shell:
ccp db exec "SELECT count(*) FROM users;"
#
# count
# ─────
# 42
#
# (1 row)
#
# 38msWorks for any SQL — SELECT, INSERT, UPDATE, DELETE:
ccp db exec "INSERT INTO users (name, email) VALUES ('Bob', 'bob@co.com');"
#
# 1 row affected · 45msStatements with RETURNING clauses display the returned rows:
ccp db exec "DELETE FROM users WHERE id = 1 RETURNING *;"Migrations
Run SQL migration files against your database with ccp db migrate. Migrations are tracked in a _ccp_migrations table so they only run once.
Setup
Create a migrations/ directory in your project with numbered .sql files:
migrations/
001_create_users.sql
002_add_posts.sql
003_add_indexes.sqlEach file can contain multiple SQL statements. PL/pgSQL functions with $$ dollar-quoting are fully supported.
Run Migrations
ccp db migrate
# › Running 3 pending migrations...
#
# ✓ 001_create_users.sql 48ms
# ✓ 002_add_posts.sql 37ms
# ✓ 003_add_indexes.sql 42ms
#
# ✓ 3 migrations appliedRunning again is safe — only pending migrations are applied:
ccp db migrate
# ✓ All 3 migrations already appliedCheck Status
See which migrations have been applied without running anything:
ccp db migrate --status
# › Migration status:
#
# ✓ 001_create_users.sql
# ✓ 002_add_posts.sql
# ○ 003_add_indexes.sql
#
# 1 pending migrationOptions
| Flag | Description |
|---|---|
--dir <PATH> | Migrations directory (default: ./migrations) |
--db-id <ID> | Database ID (reads from .ccp/config.json if omitted) |
--token <TOKEN> | Database token (reads from .ccp/config.json if omitted) |
--status | Show migration status without applying |
If you provide --db-id, you must also provide --token (and vice versa).
How It Works
Each migration runs in a database transaction. The migration SQL and the tracking insert are atomic — if the migration fails, nothing is committed. Migrations are applied in filename order, so use a numeric prefix (001_, 002_, etc.) to control the sequence.
Backups
Databases are backed up automatically once per day and backups are retained for 7 days. You can also create manual backups on demand.
Create a Backup
ccp db backup create
# ✓ Backup created
# ID bkp-8f3e2a1c
# Created 2026-04-12 14:22 UTCUse --db-id <ID> to target a database other than the one in your .ccp/config.json.
List Backups
ccp db backup list
# • bkp-8f3e2a1c manual 2026-04-12 14:22 UTC
# • bkp-7d1f9e4b scheduled 2026-04-11 00:00 UTC
# • bkp-6c9a2d7e scheduled 2026-04-10 00:00 UTCls is an alias for list.
Restore from a Backup
ccp db backup restore bkp-8f3e2a1c
# Restore database db-c1cf9a9be667 from bkp-8f3e2a1c? This will overwrite current data. (y/N)Use -y / --yes to skip the confirmation prompt.
Delete a Backup
ccp db backup delete bkp-8f3e2a1crm is an alias for delete.
Querying from Handlers
Access your database from serverless functions using the SQL-over-HTTP proxy:
const res = await fetch(`https://${process.env.DATABASE_ID}.clusterbase.dev/query`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.DATABASE_TOKEN}`,
},
body: JSON.stringify({
sql: "SELECT * FROM users WHERE id = $1",
params: [42],
}),
});
const { rows } = await res.json();Endpoints
| Endpoint | Method | Description |
|---|---|---|
/query | POST | Execute a SELECT query, returns { rows, fields } |
/execute | POST | Execute INSERT/UPDATE/DELETE, returns { rows_affected } |
/transaction | POST | Execute multiple statements atomically |
Request Format
{
"sql": "SELECT * FROM users WHERE id = $1",
"params": [42]
}Parameters use $1, $2, etc. for positional binding.
Transaction Format
{
"statements": [
{ "sql": "INSERT INTO users (name) VALUES ($1)", "params": ["Alice"] },
{ "sql": "INSERT INTO logs (action) VALUES ($1)", "params": ["user_created"] }
]
}Client-Mode Authentication
By default, the database accepts only the master token (DATABASE_TOKEN) and runs every query as the privileged clusterbase role. That's fine for server-side handlers, but it means you can't safely expose the db-proxy host directly to browsers or scope queries per user without manual WHERE user_id = $N filters.
Client-mode opens two additional auth paths on the same database:
- Anon — requests with no
Authorizationheader are allowed and run as the read-onlyanonPostgres role. - JWT — requests with a
Bearer <JWT>header are validated against your OIDC issuer; the JWT'ssubis exposed inside Postgres viaauth.uid(), and the query runs as theauthenticatedrole.
The master token continues to work alongside both. Row-Level Security policies then scope rows per user via auth.uid().
Client-mode is off at create time and can be toggled on or off at any point without redeploying or recreating the database.
Enable Client-Mode
ccp db client-access enable
# ✓ client-access enabled on db-c1cf9a9be667This restarts the db-proxy in place (~3 seconds). In-flight queries fail and should be retried; the master path resumes immediately after restart. Run with no arguments to target the DB linked in .ccp/config.json, or pass ccp db client-access enable <db-id> to target another DB.
Disable Client-Mode
ccp db client-access disable
# ✓ client-access disabled on db-c1cf9a9be667Closes the anon and JWT paths. The master token continues to work.
Both commands are idempotent — calling enable on an already-enabled DB is a no-op success and does not restart the proxy.
Anon Queries
Once enabled, requests with no Authorization header run as the anon role:
const res = await fetch(`https://${process.env.DATABASE_URL}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sql: "SELECT * FROM public_posts", params: [] }),
});By default anon has no permissions — grant only what you want exposed publicly:
GRANT SELECT ON public_posts TO anon;JWT Queries
Forward a user's Cluster OIDC access token as the bearer (see Sign in with Cluster for how to obtain one):
const res = await fetch(`https://${process.env.DATABASE_URL}/query`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userJWT}`,
},
body: JSON.stringify({ sql: "SELECT * FROM todos", params: [] }),
});The query runs as the authenticated role with auth.uid() populated from the JWT's sub claim. The JWT is validated against the OIDC issuer's JWKS — invalid, expired, or wrong-issuer tokens return 401.
Row-Level Security with auth.uid()
The typical pattern: enable RLS on user-owned tables and scope rows by the authenticated user's sub.
CREATE TABLE todos (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL
);
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
CREATE POLICY todos_owner ON todos
FOR ALL TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
GRANT SELECT, INSERT, UPDATE, DELETE ON todos TO authenticated;With this policy, SELECT * FROM todos returns only the calling user's rows even with no WHERE clause, and inserts or updates referencing other users' rows are rejected.
Roles
| Role | Used when | Defaults |
|---|---|---|
clusterbase | Master token (DATABASE_TOKEN) | Owner of all objects; bypasses RLS |
authenticated | Valid JWT | Subject to RLS; grant tables explicitly |
anon | No Authorization header | Subject to RLS; grant tables explicitly |
Requirements
- The DB must be running. Paused DBs return 409; wake the DB with any query first, then retry the toggle.
- The toggled state persists across pause/resume and across db-proxy restarts — you don't need to re-enable after a wake.
Aliases
| Command | Alias |
|---|---|
ccp db list | ccp db ls |
ccp db destroy | ccp db rm |