Platform
Hosting & deploysPush to a branch, get a live linkDatabaseReal Postgres, one per appSign-inEmail and Google, users includedFile storageUploads, photos and documentsAI gatewayEvery model, with a hard spend cap
Resources
DocsGuides written for people who are newFor AI agentsClaude Code, Cursor, MCP, JSON CLICompareCamplax beside the stack it replacesPricingFrom $25/month plus prepaid creditsSupportA person answers, not a queue botPoliciesTerms, privacy and how we handle data Get started
Open in ChatGPT Open in Claude Last updated 2026-11-07

Postgres database

Postgres 18 per environment, with pooled and direct connection strings — migrations, a schema editor, extensions like pgvector, index advice, CSV import, a database per preview, point-in-time restore and export.

Every Camplax project gets its own Postgres 18 database — the place your app keeps its data. It is real Postgres, not a proprietary layer with SQL painted on, so any Postgres client, ORM or migration tool you already know still works: pg, postgres.js, Prisma, Drizzle.

Production runs on a Postgres cluster of its own, always on, with a PgBouncer connection pooler in front and continuous backups for point-in-time restore. Each preview gets a separate database with its own login (see a database per preview). No other project can reach either.

Two connection strings

Your app receives two:

VariableWhat it is for
DATABASE_URLThe pooled connection — what the app uses at runtime, built for many short server connections
DIRECT_URLThe direct connection — for migrations and other long-lived clients that need a session of their own

Both are injected by the platform and live in the vault, so you never paste either into code. The practical rule: the app reads DATABASE_URL; migrations and other long-lived clients use DIRECT_URL. See Environment variables for how they are injected.

Both addresses only work inside Camplax — from your app, its builds and its release step. They do not resolve from your laptop or from CI, and camplax dev does not hand out database passwords. To look at or change a live database from outside, use the console's Database page (or camplax db studio, which opens it): the SQL runner, the data grid and the schema editor all work against the real database.

Prisma and other ORMs

  • Runtime: use DATABASE_URL as it is. You don't need ?pgbouncer=true: the pooler supports prepared statements, and the flag only slows Prisma down by turning its statement cache off.
  • Migrations: run prisma migrate deploy (or your tool's equivalent) as the release step — "build": { "release": "npx prisma migrate deploy" } in camplax.json — so it runs inside Camplax against DIRECT_URL, once per deploy, before traffic moves. Point the migration URL at DIRECT_URL: directUrl = env("DIRECT_URL") in schema.prisma, or process.env.DIRECT_URL in prisma.config.ts on Prisma 7. Prisma 7's own env("DIRECT_URL") helper fails the build, because builds don't get the database address.
  • Don't run migrations in the start command. The app's home folder is read-only, so npx fails there, and every restart would migrate again. The release step is the place.
  • prisma migrate dev creates a throwaway "shadow" database, which your app's database user is not allowed to do. Run it on your laptop against a local Postgres to write the migration files, commit them, and let the release step apply them.
  • Settings in the connection string (&options=-c search_path=…, a time zone) are kept for every query. Add them after the ?sslmode=verify-full both URLs already end in (databases made before November 2026 say ?sslmode=require until Camplax updates them; both are encrypted).
  • The connection is encrypted. Both URLs ask for verified TLS (sslmode=verify-full), and your app trusts the database's certificate through NODE_EXTRA_CA_CERTS, which Camplax sets. With pg, postgres (postgres.js) and Prisma on the pg adapter, the certificate and the address are both checked; Prisma's built-in engine encrypts. Use the URLs as they are: don't change sslmode or pass your own ssl option, and don't set NODE_EXTRA_CA_CERTS yourself, because Camplax's value replaces yours. The database turns away connections without TLS. A libpq client inside Camplax (psql in a release step, pg-native) needs the certificate as a file: add &sslrootcert=/etc/camplax/db-ca/ca.crt for that client only — not in code that also runs postgres, which would send it to the server.

What the pooled connection does not keep

DATABASE_URL goes through PgBouncer in transaction mode: it shares a few server connections between all of your app's requests, one transaction at a time. Anything tied to a single session ends with the transaction that made it:

FeatureOn DATABASE_URLDo this instead
LISTEN / NOTIFYThe listener receives nothing, with no errorListen on a client connected to DIRECT_URL. NOTIFY itself works on either
Advisory locks (pg_advisory_lock)Released when the transaction endspg_advisory_xact_lock inside a transaction, or a client on DIRECT_URL
Temporary tablesDropped when the transaction endsCreate and use them inside one transaction
SET statement_timeout (and other SETs)Can land on another request's connectionPut it in the connection string, or use SET LOCAL inside a transaction
SQL PREPARE / EXECUTEFails or collides between requestsYour driver's prepared statements (Prisma, Drizzle, pg and postgres.js all work)

How many queries run at once: up to 10 per environment on DATABASE_URL (4 on a preview). More wait their turn — a query that waits over 30 seconds fails with query_wait_timeout. DIRECT_URL has far fewer connections to give (about 25 at a time), so use it for migrations and listeners, not for traffic.

The console database page

camplax db studio opens it, or find Database in the console sidebar. Six things live there:

  • Schema — every table, column, type and relation, read live from the database.
  • Data — a grid over any table: browse with sorting and pagination, add rows, edit cells, delete rows, and import a CSV.
  • SQL — a runner for hand-written queries, behind a guard that caps reads and asks before writes. It keeps your query history and saved queries, and can suggest indexes for a slow one.
  • Extensions — Postgres extensions like pgvector and pg_trgm, one switch each.
  • Migrations — the apply history: each schema change recorded as a named snapshot with its SQL.
  • Branches — the database each preview gets when its branch is deployed, with compare-and-merge, recreate and delete.

There is also a copilot: ask a question in English and it writes the SQL against your real schema — review the statement, then run it. It uses the same included AI allowance as Lee, the console assistant, and log search, which everyone on the account's projects shares and which resets on the 1st of each month; once it is used up, the SQL tab still runs anything you write by hand. And on an empty table, Fill with sample rows (in the table's ⋯ menu, or on the empty table itself) adds 100 believable made-up rows for testing, after filling any empty table it points at with 25 rows each. A table that already has rows is never topped up. On production it asks first, because your live app shows those rows. Through the API (POST /v1/projects/:slug/database/tables/<table>/seed, add ?environment=<env-id> for a preview's database) one call adds up to 1,000 rows; for more, call it again or use INSERT … SELECT … FROM generate_series(…) in the SQL tab.

History and saved queries are yours. The SQL tab remembers the last 100 statements you ran on the project — the SQL, whether it worked, the row count and how long it took, never the rows it returned — and any query you save by name. Other people on the project see their own, not yours.

Migrations

Schema changes live in your repo, next to the code that needs them. Name the folder in camplax.json — "database": { "migrations": "sql/migrations" } — then camplax config diff shows which .sql files are pending and camplax config apply runs each one once, in name order, in its own transaction, recorded in _camplax_migrations. See camplax.json as code.

The app also runs its checked-in SQL at boot — applied on a fresh database, verified on a rollout — and a deploy only reports ready after a real read/write probe against the database has passed, so the schema and the app cannot drift apart silently.

During a rollout the previous release keeps serving the same database until the new one is ready, so a file in sql/migrations that drops or renames something would break it mid-deploy — such a file stops the deploy instead. Split the change: the additive part (new columns, new tables) goes in sql/migrations, and the part that removes things goes in sql/contract. Contract files run once, in name order and in one transaction, about two minutes after the new release is live and only when no older release is still serving; on a brand-new database they run right away. If one fails, the app keeps running on the current schema and the step is tried again on the next start.

Editing the schema without SQL files

The schema tab doubles as an editor. Change the schema there and Camplax computes the diff, shows you the exact SQL it will run, and warns you about anything destructive or table-locking. Applying runs the statements as one transaction — a failure halfway leaves the schema exactly as it was. Anything that loses data needs a second, explicit confirmation, and each apply lands in the history list as a named snapshot (the last 50 are kept). One thing the editor cannot guess: renaming a column and dropping-plus-adding one look identical afterwards, so it asks you to say which you meant.

camplax gen types          # writes database.types.ts from the live schema
camplax gen types --out db-types.ts

Extensions

Postgres extensions add types and functions to the database. Turn one on from the Extensions tab, or ask Lee. Each change is recorded on the Migrations tab like any other schema change.

ExtensionWhat it is for
vector (pgvector)Embeddings and similarity search — AI search, RAG, recommendations
postgisPlaces and maps: points, shapes, distances, "near me"
pg_trgmFuzzy text search, and LIKE '%word%' that can use an index
citextCase-insensitive text, e.g. emails
pgcrypto, uuid-osspHashing, encryption, more UUID generators
unaccent, fuzzystrmatchAccent-free and sound-alike matching
hstore, ltree, tablefuncKey–value columns, tree paths, pivot tables
btree_gin, btree_gistPlain columns inside GIN/GiST indexes — e.g. "no two bookings overlap"

That list is the whole allowlist. The tab shows only what your database image actually ships; anything else is marked as not available rather than offered. Extensions that could reach the network or the file system (dblink, postgres_fdw, untrusted languages) are never installable.

pgvector and PostGIS take a moment. Postgres only lets a superuser install them, and no role your app or the console holds is one — so the database operator installs them for you. The switch shows Installing until it has, usually about two minutes. The others are on immediately.

Once vector is on, it is ordinary SQL:

CREATE TABLE documents (
  id bigserial PRIMARY KEY,
  body text NOT NULL,
  embedding vector(1536)
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

-- the five closest documents to a query embedding
SELECT id, body FROM documents ORDER BY embedding <=> $1 LIMIT 5;

Turning an extension off is refused while any column still uses one of its types — drop or change that column first, so no data goes with it. The API is GET /v1/projects/:slug/database/extensions and POST …/extensions/<name> with { "enabled": true }.

You do not need an extension for search: Postgres has full-text search built in. Add a generated tsvector column and a GIN index, then match with @@:

ALTER TABLE posts ADD COLUMN search tsvector
  GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
CREATE INDEX posts_search_idx ON posts USING gin (search);

SELECT id, title FROM posts
 WHERE search @@ websearch_to_tsquery('english', $1)
 ORDER BY ts_rank(search, websearch_to_tsquery('english', $1)) DESC
 LIMIT 20;

websearch_to_tsquery understands what people type into a search box — quotes, or, -word. For typo-tolerant search on short fields like names, turn on pg_trgm and use similarity() or ILIKE with a trigram index instead.

Faster queries: index advice

On the SQL tab, Suggest indexes looks at the query in the box and tells you which index would help. The query is only planned — EXPLAIN without ANALYZE, inside a read-only transaction — so asking is safe even for an UPDATE or DELETE: nothing runs and nothing changes.

The advice comes from the plan itself: a table read end to end to find a few rows gets an index on the columns it was filtered by, equality columns first; a join key read the same way gets one too; a LIKE/ILIKE search gets a trigram index (turn on pg_trgm first). If a column already has an index Postgres chose not to use, it says so — usually the table is still small enough that reading it whole is cheaper. If the database has index_advisor installed, its cost-based suggestions are used instead.

Each suggestion shows the exact CREATE INDEX CONCURRENTLY statement, and Create index runs it as a recorded migration. It builds concurrently, so your app keeps writing to the table while it does. A build that takes longer than 30 seconds is stopped and the half-built index removed — add that one as a migration in your repo instead. The API is POST /v1/projects/:slug/database/indexes/advice with { "sql": "…" }, and POST …/indexes with { "table", "columns", "method" }.

Import a CSV

On the Data tab, pick a table and press Import CSV. Choose the file (comma, semicolon or tab separated; the first line must be the column names; up to 2 MB and 10,000 rows) and Camplax shows how the file's columns map onto the table's — by name, ignoring case and spaces — and lets you change any of them or skip one. Every row is checked against the column's real type before anything is written, and the rows that will not fit are listed by line number.

Empty cells take the column's default when it has one, NULL when it can be empty, and are an error when it can be neither. Rows go in batches, each batch in one transaction; if the database refuses one (a duplicate unique value, a foreign key that points nowhere), Camplax narrows it down to the exact lines and imports everything else. The report at the end names each line that did not go in, and why.

Imported rows are ordinary inserts made with the same permissions as the data grid — they cannot change the schema. The API is POST /v1/projects/:slug/database/tables/<table>/import with { "csv": "…", "dryRun": true } to preview and without dryRun to import.

Import from another database

camplax import --name <slug> --db <postgres-url> (or the MCP import_project tool, or POST /v1/imports with sourceDatabaseUrl) creates a new app and copies the public schema of another Postgres into its database — tables, rows, indexes, constraints, sequences, functions and triggers.

  • What it accepts: an ordinary connection string, postgresql://user:password@host:5432/database, from Neon, Railway, Render, RDS, DigitalOcean, your own server, or Supabase. For Supabase, copy the Session pooler string (port 5432) from Connect: the direct db.<ref>.supabase.co address only answers over IPv6 unless you bought Supabase's IPv4 add-on. Transaction poolers (port 6543, Neon's -pooler hosts) cannot copy a database and are refused.
  • What it refuses: an address that is, or looks up to, a private network, a local name or Camplax's own servers. The check runs when you start the import and again on the server that does the copy, which then connects only to the address it checked, always over TLS.
  • Limits: up to 2 GB of source database and one hour of copying. If the new app's database already has a table with the same name, nothing is copied.
  • Your password is kept encrypted only until the copy starts and is deleted when it ends. It is never written to a log.
  • What is not copied: other schemas (Supabase's auth and storage), sign-in users and files. Anything in public that depends on them — a policy calling auth.uid(), a key pointing at auth.users — cannot be created here; the import still finishes and says how many items it skipped.

The import shows which step it is on — setting up the app, copying tables, done — as progress on GET /v1/imports/<id>, and on project_status over MCP. A failed copy says why in plain words: the address was refused, the password did not work, the database is too big, or the tables were already there.

A database branch per preview

A preview never sees production data. Every preview runs against its own database branch — a separate database with its own login, made for its git branch the first time that branch is deployed: a push to a connected repo, a pull request, or camplax deploy --branch <name>. There is nothing to create by hand. A preview deploy waits for its database and never falls back to production.

Two things to know plainly:

  • A branch starts empty — unless you ask for production data. Branches are empty by default; if you want real data in a preview, use Recreate with production data on the Database page's branches tab. Only the project's Owner or a Member can, from a signed-in browser: it asks for the preview's name and your password, and is written to the audit log. Restoring production as a new copy follows the same rule. It is a raw copy of production — rows, tokens and personal data come along verbatim, nothing is sanitized — so treat a data-carrying preview like production for access purposes, and turn on Password-protect previews in Settings.
  • A branch is disposable. Recreate on the branches tab starts it again (empty, or with production's schema or data); deleting the git branch or closing the PR deletes it along with the rest of the preview. See Preview deployments.

Point-in-time restore

Every database keeps a continuous backup (a WAL archive plus a daily full backup). How far back it reaches is your plan's restore window: 7 days on Indie, 14 on Pro, 30 on Scale. A plan change moves every database's window within about 15 minutes.

You can restore production to any moment inside that window in two ways.

As a new copy — production is not touched:

  • On the Database page, open the branch picker, pick a date and time under Restore to a point in time, and choose Restore as a new copy. The copy holds production's real data, so it is the Owner's or a Member's and asks you to confirm it is you (your password, or a recent sign-in).
  • Or, from a signed-in browser, POST /v1/projects/:slug/database/pitr with { "at": "<RFC 3339 timestamp>", "password": "…" } — e.g. { "at": "2026-09-22T14:30:00Z", "password": "…" }.

The copy lands as a new, separate environment (branch name restore-<timestamp>) holding production's data as of that moment. Pick it in the branch picker to look at it; tear it down from the same list when you are done.

Production itself, in place — for when bad data reached production (a broken migration, a mistaken bulk DELETE) and you want it back the way it was:

  • Pick the moment the same way and choose Restore production. Only the project's Owner can, from a signed-in browser, after typing the project's name and confirming it is you (your password, or a sign-in in the last ten minutes for accounts without one, plus your authenticator code when two-factor is on).
  • Camplax builds a new database from the backups while your app keeps running, then stops the app for a minute or two, switches it to the restored database and starts it again. The Database page shows each step.
  • Everything written after the chosen moment disappears from production, but it is not deleted: the database it replaced is kept, stopped, for 3 days, and Undo restore on the Database page switches back to it. An undo loses anything written since the restore.
  • After those 3 days the replaced database and its backups are deleted. From then on, the restore window starts at the restore: you cannot go back to before it.
  • If the switch does not finish, the Database page says so and Try again continues where it stopped.

The limits, plainly:

  • How recent. Changes reach backup storage at least every 5 minutes, so the latest restore point is about 5 minutes ago, not "now". GET /v1/projects/:slug/database/pitr/window returns the exact earliest and latest points and the window in days, and the console's date picker uses them.
  • How far back. The first backup is taken when the database is created (or restored) and finishes a few minutes later. Until then there is nothing to restore from. After that you can go back your plan's window, or to the first backup if the database is younger.
  • If backups stop. If backups ever stop reaching storage, the latest restore point stays at the moment they stopped, and Camplax is alerted.
  • Large databases. Rebuilding a large database can take a while. A copy shows as provisioning until it can be queried; a restore of production shows "Rebuild the database from backups" until it is ready, and the app keeps serving the old data meanwhile.
  • Deleting. When an environment is deleted, its backups are deleted from storage too.

Export and restore

You can take your data with you:

  • Export — POST /v1/projects/:slug/settings/export/database starts a pg_dump of your database and answers 202 { exportId, downloadToken }. Poll GET …/export/database/<exportId> until status is succeeded, then download once with POST …/export/database/<exportId>/download and the body { "token": "<downloadToken>" } (it works for an hour, for the person who started the export). The file is deleted as it is handed over, and one nobody collects is deleted a day after it was asked for. It is every row you have, so it is the Owner's, from a signed-in browser, and asks you to confirm it is you (your password, or a sign-in in the last ten minutes, plus your two-factor code when it is on) — a project API key, CLI token or MCP client cannot export the database. The dump holds every schema your app created, not only public, and goes straight to storage, so its size is not limited by the API. Settings → Data → Database dump does all three steps for you.
  • Restore a file — POST /v1/projects/:slug/settings/restore/database?environment=<preview-id>&overwrite=1 accepts a .dump file up to 64 MB and loads it into that preview environment, replacing its data. Poll GET …/restore/database/<restoreId> for the result. Production is never a restore target: restore into a preview, check it there, and decide what to do next. Settings → Data → Restore does this from the console. For rewinding to a moment in time instead of a file, use point-in-time restore above.

Database webhooks

Tell your app when a row changes. On Database → Webhooks, pick a table, the changes you care about (inserted, updated, deleted) and a path on your app; each matching change is POSTed there, signed with Camplax's key:

"POST /hooks/orders": async (req, res) => {
  const raw = await readRawBody(req);
  const verified = await verifyCamplaxWebhook(req, raw, { purpose: "db-webhook" });
  if (!verified.ok) return sendFail(res, verified);
  const { id, table, op, row, old } = JSON.parse(raw.toString());
  sendOk(res, { ok: true });
},

row is the new row (null on delete), old the previous one (null on insert); a row wider than 16 KB arrives as { "truncated": true } — re-read it by key. Dedupe on id, which stays the same across retries. Delivery is at-least-once, retried for about eight hours (30s, 2m, 8m, 32m, then every 2h), then kept as dead with the error; ten dead deliveries in a row pause forwarding and email the owners — changes are still recorded and held, and Resume forwarding sends them. Switching a hook off yourself stops recording. Each project can queue 10,000 deliveries and 64 MB per rolling 24 hours; past that, changes wait in the outbox in your own database, and the hook says so.

The backlog is capped. At most 10,000 changes wait in your database's outbox (about 330 MB at worst). If more pile up — your app down for a long time, the daily quota used up — new changes are dropped rather than filling your disk, and the hook shows how many were dropped. Every delivery can be replayed from the console.

The write never waits. A trigger on the table adds one small row to camplax_hooks.outbox inside the same transaction — a rolled-back write sends nothing — and the Camplax runtime next to your app drains that outbox (straight away via NOTIFY, and every 30 seconds regardless), handing changes to Camplax, which delivers them. Nothing leaves your database until the write has committed, and an outbox row is only deleted once Camplax has recorded it, so a deploy or restart delays deliveries rather than losing them. Changes made while a hook is switched off are not sent later. Production only — previews do not fire database webhooks.

The API: GET/POST /v1/projects/<slug>/db-webhooks ({ name, table, events, forwardPath }), PATCH/DELETE /:id, POST /:id/reinstall (re-apply the trigger), GET /:id/deliveries, POST /deliveries/:id/replay — console session, CLI token or a manage key. Each hook shows whether its trigger is actually installed.

Why not pg_net (Supabase's approach)? It is not in the database images Camplax runs, it would put outbound HTTP inside your database, and its requests are fire-and-forget. The outbox commits with your write and survives restarts.

Rows stay with their owners

Access control lives in the app, not in a separate rules engine. In a Plax app — including the camplax create starter — every table is wrapped in defineResource with an ownerField, and each read, update and delete it serves is scoped to the signed-in user's id inside the query itself. The client cannot set that field; the server fills it from the session, and a row you cannot see answers 404. The starter's notes resource is the pattern to copy for new tables.

One honest limit

There is no SQL-over-HTTP endpoint for app code: the SQL runner and copilot are console surfaces for you, not an API your app calls. Your app talks to its database over the normal Postgres protocol through DATABASE_URL.

Under the hood: one Postgres database per environment on the app's cell, always on, behind the injected DATABASE_URL — with WAL continuously archived for the point-in-time window above.

Camplax

Hosting, database, sign-in and storage in one project. One login, one bill.

Platform
More
Legal
Company
© 2026 Camplax From $25 / month · credits stop at zero