K

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 upWhat it means
axhub.yaml declarationdatabase: engine: postgres is already there
Connection codedb() in lib/db.ts handles connecting
Creating tablesWrite your CREATE TABLE inside ensureSchema() in the same file
Reading and writing
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 fresh

DATABASE_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.

axhub.yaml
database:
  engine: postgres

engine 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 variablePurpose
DATABASE_URLThe address for everyday reads and writes. It goes through a relay that pools connections for you (a connection pooler)
DIRECT_DATABASE_URLFor work that must connect directly, bypassing the pooler — like changing table structure (migrations)
pg driver
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]);
Prisma (schema.prisma)
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 staging

Inside 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_URL yourself — 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.