Postgres / Prisma Backend
Run the commerce layer on any PostgreSQL database via the Prisma adapter.
The commerce layer ships with a server-side Prisma/PostgreSQL adapter that is a drop-in
replacement for the default Firebase backend: same CommerceServices
interface, same lifecycle events, same read shapes. It lets you run the entire store —
catalog, carts, quote requests, orders, discounts — on any Postgres database you control
(local, Supabase, Neon, RDS, a container).
- Adapter:
src/lib/commerce/server/prisma.ts→export const prismaCommerce - Schema:
prisma/schema.prisma - CLI config:
prisma.config.ts
Firebase is the zero-config default. You only need the steps below if you deliberately
switch the backend to Postgres. If you are staying on Firebase, you do not need Prisma,
a DATABASE_URL, or prisma generate at all.
Prisma Postgres / Accelerate is NOT used
This project does not use Prisma Postgres or Prisma Accelerate — there is no Prisma
account, login, or API key involved anywhere. DATABASE_URL must be a plain
postgresql:// connection string, and it must be present in the environment before you
run npm run prisma:generate.
Prisma 7's CLI has an onboarding flow for its own hosted database (Prisma Postgres +
Accelerate). If DATABASE_URL is missing — or points at a prisma:// /
prisma+postgres:// URL — the CLI falls back to that onboarding and prompts you to log in
and issue an API key. With a real postgresql://... string set, prisma generate and
prisma db push run non-interactively and never ask for an account or key.
Prisma 7 specifics
This project ships Prisma 7, which changes two things the adapter relies on:
- No
datasource.urlinschema.prisma. Prisma 7 removed that property. The connection string the CLI needs lives inprisma.config.tsinstead (datasource.url = env("DATABASE_URL")), and the runtime client gets it through a driver adapter. - The runtime client needs a driver adapter (not a bare URL). This adapter builds a
@prisma/adapter-pgclient overDATABASE_URL. The@prisma/client,@prisma/adapter-pg, andpgpackages ship in the base dependencies — no extra install is needed; you only generate the client once aDATABASE_URLis set.
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
url: env("DATABASE_URL"),
},
});Setup
Set DATABASE_URL
Use a plain PostgreSQL connection string. Any Postgres works. Export it in your shell or
put it in a .env file your process loads — it must be present both for the Prisma CLI (read
via prisma.config.ts) and at runtime (the adapter reads process.env.DATABASE_URL on first
use).
# Local Postgres
DATABASE_URL="postgresql://user:password@localhost:5432/ecommerce"
# Supabase (Project Settings → Database). The pooled "Transaction"
# string works well for serverless:
DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres"Set this before the next step. prisma generate only needs the string to parse (it
does not connect), but if it is missing the CLI drops into Prisma's hosted-database
onboarding and asks for an API key.
Generate the client
npm run prisma:generate # == npx prisma generateThis produces the typed client that @prisma/client re-exports. Re-run it after every schema
change (and after a fresh npm install on a Postgres deploy).
Create the tables
For local development, let Prisma create and track the schema with a migration:
npx prisma migrate dev --name initBoth read the connection string from prisma.config.ts.
Grant admin access
admin.isAdmin(uid) checks row existence in the admins table, keyed by the Firebase Auth
uid. Insert a row per admin:
insert into admins (uid) values ('<firebase-auth-uid>')
on conflict (uid) do nothing;Activate the backend
Backend selection is env-driven — you do not edit any source file. Set both variables so the browser and the server agree:
NEXT_PUBLIC_COMMERCE_BACKEND=postgres # browser: route commerce calls to the RPC layer
COMMERCE_BACKEND=postgres # server: run the Prisma adapterThe browser never talks to Postgres directly. It calls a single RPC route
(POST /api/commerce/<service>/<method>) that verifies the caller's Firebase ID token,
enforces per-method access control (public / user-scoped / admin), and then runs
prismaCommerce with the privileged DATABASE_URL. Because Firebase Auth stays the identity
provider, the FIREBASE_SERVICE_ACCOUNT* credentials are also required to verify tokens — see
environment variables.
Bundling: serverComponentsExternalPackages
The optional SQL-backend packages are kept out of the bundle and require()d at runtime
inside the adapter. next.config.mjs lists them as external so a default (Firebase) build
succeeds even without a generated client, and so the Postgres adapter can load the generated
client at runtime:
experimental: {
serverComponentsExternalPackages: [
"@prisma/client",
"@prisma/adapter-pg",
"pg",
],
},This pairs with the adapter's lazy client: @prisma/client and @prisma/adapter-pg are loaded
via require inside getPrisma() (not a top-level import) so the file type-checks and builds
with no generated client. The instance is cached on globalThis so hot reload does not open a
new pool per edit, and the first actual operation — not import — throws if DATABASE_URL is
unset.
Deploying on Postgres
A fresh CI environment has no generated client, so the Prisma client must be generated during
the build. Set the build command (or add a postinstall) to run generation first:
npm run prisma:generate && npm run buildThe deploy environment must include:
| Variable | Purpose |
|---|---|
DATABASE_URL | Plain postgresql:// string the adapter and CLI connect with. |
NEXT_PUBLIC_COMMERCE_BACKEND=postgres | Browser routes commerce calls to the RPC layer. |
COMMERCE_BACKEND=postgres | Server runs the Prisma adapter. |
FIREBASE_SERVICE_ACCOUNT_JSON | Verifies caller ID tokens (Firebase Auth stays the identity provider). |
Run npm run prisma:push (or your migration step) against the production database once, out
of band, to create the tables.
Deploying on Firebase but the build still asks for a Prisma API key? Some platforms
(notably Vercel) auto-detect prisma/schema.prisma + @prisma/client and inject
prisma generate into the install step. With no DATABASE_URL present, that generate drops
into Prisma's hosted-database onboarding and prompts for a key — even though your app never
uses Postgres at runtime. Two fixes, either works:
- Give the build a placeholder
DATABASE_URL, e.g.postgresql://user:pass@localhost:5432/db.prisma generateonly needs the string to parse, so generation runs non-interactively; the value is never used at runtime whileCOMMERCE_BACKENDis unset/firebase. - Or disable auto-generate on your platform (on Vercel, set the build command explicitly to
npm run build) so Prisma is never invoked.
Data model
The schema (prisma/schema.prisma) mirrors the Firestore layout the Firebase adapter uses,
with loose/schemaless documents stored in Json columns so unknown fields round-trip exactly
like Firestore documents.
| Model | Table | Key | Notes |
|---|---|---|---|
Product | products | caller id (e.g. AUD-001) | Body in data Json; productNameLower and brand are promoted, indexed columns the adapter keeps in sync. Variants/options live inside data. |
FeaturedProduct | featured_products | cuid() | Full copy of a product body; productId promoted back-reference. |
Cart | carts | userId (auth uid) | items is a JSON CartItem[]; clear deletes the row (missing ≠ empty). |
Quote | quotes | cuid() | items/customer/discount are Json; orderNumber claimed atomically on conversion. |
Order | orders | cuid() | Created by promoting quotes; total is a Float. |
Customer | customers | id (auth uid) | Rigid: firstName, lastName, phone, email only. |
Address | addresses | cuid() | Schemaless data Json + indexed customerId (no FK). |
Discount | discounts | trimmed UPPERCASE code | Saves are single-row upserts; lookups case-insensitive by construction. |
Admin | admins | uid | Row existence grants admin. |
Behavioral notes worth knowing:
- Prefix search uses
COLLATE "C".products.searchpins its range bounds and ordering to byte-order collation so results match Firestore's code-point ordering regardless of the database's default collation. - Per-customer quote/order copies are flattened.
listForCustomeris aWHERE customer_id = $uidquery; the copy identity is synthesized on read (id === quoteNumber/orderNumber,mainQuoteId === <main record id>on quotes). carts.listenpolls (immediate fetch + 5s interval) — realtime push is Firestore-only for now. The callback contract is otherwise identical.PageCursoris a base64url-encoded keyset token (JSON-serializable so it survives HTTP round-trips), not a Firestore snapshot.
Using a different database
The schema uses no Postgres-only features (no scalar lists, enums, or @db.* native types), so
the same adapter code runs on MySQL or SQLite. Change datasource.provider in
prisma/schema.prisma, swap the driver-adapter package passed to new PrismaClient({ adapter })
in src/lib/commerce/server/prisma.ts (e.g. @prisma/adapter-mariadb for MySQL,
@prisma/adapter-better-sqlite3 for SQLite), give products.search an engine-appropriate
binary collation (MySQL utf8mb4_bin, SQLite COLLATE BINARY), then regenerate and migrate.