Remix
Remix connects to DB9 through any standard PostgreSQL driver over pgwire. Loaders and actions run server-side, so your database credentials stay off the client. No special adapter or driver is needed.
Prerequisites
Section titled “Prerequisites”- A DB9 database (create one)
- Node.js 18+
- Remix 2+
Create a DB9 Database
Section titled “Create a DB9 Database”db9 create --name remix-appGet the connection string:
db9 db status remix-appSet the connection string as an environment variable:
DATABASE_URL="postgresql://remix-app.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require"npx create-remix@latest remix-db9cd remix-db9npm install prisma @prisma/clientnpx prisma initDefine your schema:
generator client { provider = "prisma-client-js"}
datasource db { provider = "postgresql" url = env("DATABASE_URL")}
model User { id Int @id @default(autoincrement()) email String @unique name String posts Post[] createdAt DateTime @default(now())}
model Post { id Int @id @default(autoincrement()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId Int createdAt DateTime @default(now())}Push the schema to DB9:
npx prisma db pushnpx prisma generateCreate a singleton client to avoid connection exhaustion during development:
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
declare global { var __prisma: PrismaClient | undefined;}
if (process.env.NODE_ENV === 'production') { prisma = new PrismaClient();} else { if (!global.__prisma) { global.__prisma = new PrismaClient(); } prisma = global.__prisma;}
export { prisma };npx create-remix@latest remix-db9cd remix-db9npm install drizzle-orm pgnpm install -D drizzle-kit @types/pgDefine your schema:
import { pgTable, serial, varchar, text, boolean, integer, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', { id: serial('id').primaryKey(), email: varchar('email', { length: 255 }).unique().notNull(), name: varchar('name', { length: 100 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),});
export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: varchar('title', { length: 500 }).notNull(), content: text('content'), published: boolean('published').default(false), authorId: integer('author_id').notNull().references(() => users.id), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),});Create a singleton client:
import { drizzle } from 'drizzle-orm/node-postgres';import { Pool } from 'pg';
let pool: Pool;
declare global { var __pool: Pool | undefined;}
if (process.env.NODE_ENV === 'production') { pool = new Pool({ connectionString: process.env.DATABASE_URL });} else { if (!global.__pool) { global.__pool = new Pool({ connectionString: process.env.DATABASE_URL }); } pool = global.__pool;}
export const db = drizzle(pool);Loader (Read Data)
Section titled “Loader (Read Data)”Loaders run on the server before a route renders. Use them to fetch data from DB9.
import type { LoaderFunctionArgs } from '@remix-run/node';import { json } from '@remix-run/node';import { useLoaderData } from '@remix-run/react';import { prisma } from '~/db.server';
export async function loader({ request }: LoaderFunctionArgs) { const users = await prisma.user.findMany({ include: { posts: true }, orderBy: { createdAt: 'desc' }, }); return json({ users });}
export default function UsersPage() { const { users } = useLoaderData<typeof loader>(); return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} — {user.posts.length} posts </li> ))} </ul> );}import type { LoaderFunctionArgs } from '@remix-run/node';import { json } from '@remix-run/node';import { useLoaderData } from '@remix-run/react';import { db } from '~/db.server';import { users } from '~/schema.server';
export async function loader({ request }: LoaderFunctionArgs) { const allUsers = await db.select().from(users).orderBy(users.createdAt); return json({ users: allUsers });}
export default function UsersPage() { const { users: allUsers } = useLoaderData<typeof loader>(); return ( <ul> {allUsers.map((user) => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> );}Action (Write Data)
Section titled “Action (Write Data)”Actions handle form submissions and other mutations, also server-side.
import type { ActionFunctionArgs } from '@remix-run/node';import { json, redirect } from '@remix-run/node';import { Form } from '@remix-run/react';import { prisma } from '~/db.server';
export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const email = formData.get('email') as string; const name = formData.get('name') as string;
await prisma.user.create({ data: { email, name } }); return redirect('/users');}
export default function NewUserPage() { return ( <Form method="post"> <input type="text" name="name" placeholder="Name" required /> <input type="email" name="email" placeholder="Email" required /> <button type="submit">Create User</button> </Form> );}import type { ActionFunctionArgs } from '@remix-run/node';import { json, redirect } from '@remix-run/node';import { Form } from '@remix-run/react';import { db } from '~/db.server';import { users } from '~/schema.server';
export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const email = formData.get('email') as string; const name = formData.get('name') as string;
await db.insert(users).values({ email, name }); return redirect('/users');}
export default function NewUserPage() { return ( <Form method="post"> <input type="text" name="name" placeholder="Name" required /> <input type="email" name="email" placeholder="Email" required /> <button type="submit">Create User</button> </Form> );}Production Notes
Section titled “Production Notes”- Server-side only: Loaders and actions run on the server. Never import your
db.server.tsmodule from a client-side file. - Port 5433: DB9 uses port 5433, not the default PostgreSQL port 5432.
- TLS required: Always include
sslmode=requirein the connection string for DB9’s hosted service. - Node.js runtime required: DB9 uses the pgwire protocol over TCP. Deploy Remix on a Node.js server (Express, Hono, etc.), not on an edge runtime without TCP socket support.
- Connection pooling: Start with 5–10 connections. The singleton pattern in
db.server.tsprevents pool exhaustion during development HMR cycles.
Troubleshooting
Section titled “Troubleshooting”ECONNREFUSED on port 5432
Section titled “ECONNREFUSED on port 5432”DB9 uses port 5433, not 5432. Check that your DATABASE_URL includes :5433 in the connection string.
Edge runtime not supported
Section titled “Edge runtime not supported”DB9 requires a TCP connection (pgwire protocol). If you deploy Remix to an edge runtime (e.g., Cloudflare Workers), database queries will fail. Use a Node.js-based server adapter instead.
Connection pool exhaustion in development
Section titled “Connection pool exhaustion in development”Remix’s dev server reloads modules on file changes. Without the singleton pattern in db.server.ts, each reload opens a new connection pool. If you see “too many connections” errors, verify you are using the global caching pattern shown above and restart the dev server.
prisma db push fails
Section titled “prisma db push fails”DB9 has limited information_schema support. If schema push fails, create tables with raw SQL instead. See the Prisma guide for details.
Next Pages
Section titled “Next Pages”- Prisma — full Prisma integration guide
- Drizzle — full Drizzle integration guide
- Connect — connection strings and authentication
- Production Checklist — deployment readiness