Connect a database
Turn on an app-scoped Postgres DB, read and write it from code, and browse it as tables in the console. Template apps already have it on.
Data your app works with — order lists, member records — lives in an app-scoped database.
Apps built from a template already have it turned on, so you can create tables and start reading and writing right away.
What you get is standard Postgres. Keep using whatever you already use — postgres, pg, Prisma, Drizzle.
If you built from a template
The Next.js and Astro templates come with all of this in place.
| Already set up | What it means |
|---|---|
axhub.yaml declaration | database: engine: postgres is already there |
| Connection code | db() in lib/db.ts handles connecting |
| Creating tables | Write your CREATE TABLE inside ensureSchema() in the same file |
import { db, ensureSchema } from '@/lib/db';
await ensureSchema();
const rows = await db()`SELECT * FROM todos WHERE user_key = ${userKey}`;Values passed as ${...} are bound safely for you — never concatenate them into the string yourself.
To add a table or a column, extend the CREATE TABLE IF NOT EXISTS block inside ensureSchema(). It's plain SQL.
Developing on your machine
The template ships commands that bring up a local Postgres.
npm run db:up # start local Postgres
npm run db:reset # wipe the data and start freshDATABASE_URL in .env.local points at that local DB. On deploy it's replaced by the value AxHub injects, and your code stays the same.
If you're turning it on yourself
Without a template, add two lines to axhub.yaml and deploy.
database:
engine: postgresengine currently accepts only postgres. If you'd rather turn it on from the terminal than edit the file, axhub apps raw-db enable --app <app> --execute has the same effect.
On the next deploy an app-scoped DB is created and the connection details arrive as environment variables.
| Environment variable | Purpose |
|---|---|
DATABASE_URL | The address for everyday reads and writes. It goes through a relay that pools connections for you (a connection pooler) |
DIRECT_DATABASE_URL | For work that must connect directly, bypassing the pooler — like changing table structure (migrations) |
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query('SELECT * FROM orders WHERE done = $1', [false]);datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // queries
directUrl = env("DIRECT_DATABASE_URL") // migrations
}You may need prepare: false. The relay that DATABASE_URL goes through doesn't support prepared statements, so drivers like postgres need that option turned off. If it works locally but only fails after deploying, check this first.
postgres(process.env.DATABASE_URL, { prepare: false })production (the live service) and staging (for testing) get separate DBs. Deploying to each environment creates that environment's DB, and their data never mixes.
Browse it as tables in the console
To see how data actually accumulated, open the app console → Resources tab → Tables and browse rows as they are.
From the terminal, it looks like this.
axhub tables db-list --app demo # list tables
axhub tables db-rows orders --app demo # browse rows
axhub tables db-rows orders --app demo --environment stagingInside app code, the SDK reads the same thing via sdk.apps.rawDb.tables(appId) and sdk.apps.rawDb.tableRows(appId, 'orders') — SDK setup happens in Install the SDK.
All three surfaces are read-only, so you can't modify data there. And if the app owner has locked the app with a viewing policy, even admins may be blocked from viewing row contents in the console or terminal.
Good to know
- It doesn't attach to UI-only apps — a static hosting app has no always-running server, so there's nowhere to inject the connection details. Deploying with the declaration in place fails clearly.
- Removing the declaration doesn't delete the DB — data and injection stay. To really turn it off, use
axhub apps raw-db disable --app <app> --execute. Even then your tables and data remain and only access is cut — turn it back on and connection details arrive again with a fresh password. - You can't override
DATABASE_URLyourself — on an app with the DB enabled, the value AxHub injects always wins. An environment variable you save under the same name is ignored. (On an app without the DB enabled, you're free to set an external DB address.) - It isn't injected retroactively into already-running apps — the environment variables arrive starting with the first deploy after you turn it on.
- The SDK has no row-write API — writes go through the app runtime's
DATABASE_URL.
You've succeeded when
After the deploy finishes, the app console → Resources tab → Tables shows your app's tables and rows. Whatever your app code read and wrote through DATABASE_URL appears there as is.
Next, set up a place for images and attachments in Connect storage.