Skip to content

Custom Server Integration

Rebase was built to be completely modular. While the initializeRebaseBackend coordinator provides a full batteries-included backend using Hono, you can completely bypass it and embed the core Database Adapter and Realtime WebSockets directly into your own custom Node.js application (like Express, Fastify, or plain Node.js HTTP).

The @rebasepro/server-postgres package is completely framework-agnostic. It depends only on Drizzle ORM and standard Node.js http.Server.

Rebase provides a centralized loadEnv() utility in @rebasepro/server that validates your environment variables against a strict Zod schema. Call it after loading your .env file:

import dotenv from "dotenv";
import { loadEnv } from "@rebasepro/server";
dotenv.config({ path: "../../.env" });
// Basic — just Rebase env vars:
export const env = loadEnv();
// Extended — add your own typed vars:
import { z } from "zod";
export const env = loadEnv({
extend: z.object({
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.string().default("587").transform(Number),
STRIPE_SECRET_KEY: z.string(),
})
});
// env.SMTP_HOST → string | undefined (fully typed)
// env.STRIPE_SECRET_KEY → string (validated, required)

Key behaviors:

  • Auto-generates ephemeral JWT_SECRET and REBASE_SERVICE_KEY in development so you can start without manual setup.
  • Blocks auto-generated secrets in production — you must set them explicitly.
  • Validates that CORS_ORIGINS or FRONTEND_URL is set in production.

See .env.example in the scaffolded app for the full list of supported variables.

Here is a complete example of how to initialize the Rebase PostgreSQL adapter and Realtime WebSockets inside a standard Express application, manage read replicas, access Drizzle directly, and implement clean server terminations.

Install the required core packages along with Express:

npm install @rebasepro/server-postgres @rebasepro/types express pg

2. Initialization and Graceful Shutdown Example

Section titled “2. Initialization and Graceful Shutdown Example”
import express from "express";
import { createServer } from "http";
import pg from "pg";
import { createPostgresBootstrapper } from "@rebasepro/server-postgres";
async function startServer() {
const app = express();
// 1. WebSocket Upgrade Guard
// WebSockets require hijacking the HTTP Upgrade header. You must bind
// Rebase to a raw Node.js http.Server instance.
const server = createServer(app);
// 2. Configure the connection pool
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Max concurrent database connections
idleTimeoutMillis: 30000
});
// 3. Initialize the Postgres Bootstrapper
const bootstrapper = createPostgresBootstrapper({
connection: pool,
connectionString: process.env.DATABASE_URL,
adminConnectionString: process.env.ADMIN_CONNECTION_STRING, // Required for branching
schema: {
tables: {}, // Place your custom Drizzle tables here
relations: {} // Place your custom Drizzle relations here
}
});
// 4. Initialize the Driver and Services
// Connects to Postgres, verifies connection, starts cross-instance listeners
const { driver, realtimeProvider, internals } = await bootstrapper.initializeDriver({
collections: [] // Pass Rebase EntityCollections if using schema-as-code
});
// Access the underlying schema-aware Drizzle client if needed
const db = internals.db; // Drizzle NodePgDatabase instance
const readDb = internals.readDb; // Read replica Drizzle instance if DATABASE_READ_URL is set
// 5. Mount Realtime WebSockets
await bootstrapper.initializeWebsockets(server, realtimeProvider, driver, {
requireAuth: true // Enforces authentication token checks
});
app.use(express.json());
app.get("/api/health", (req, res) => {
res.json({ status: "healthy" });
});
// Direct Driver CRUD Operation
app.post("/api/products", async (req, res) => {
try {
const result = await driver.saveEntity({
path: "products",
entity: req.body
});
res.status(201).json({ success: true, data: result });
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : "Internal Server Error" });
}
});
// Raw Drizzle SQL Execution (RLS bypass)
app.get("/api/stats", async (req, res) => {
try {
const countResult = await db.select().from(...); // Perform standard Drizzle operations
res.json(countResult);
} catch (error) {
res.status(500).json({ error: error instanceof Error ? error.message : "Internal Server Error" });
}
});
// Start listening (Using the HTTP Server, NOT app.listen)
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log(`🚀 Server and WebSocket engine running on port ${port}`);
});
// 6. Graceful Shutdown Handler
// Terminate listeners and drain connection pools on process termination signals
const handleShutdown = async (signal: string) => {
console.log(`\nShutdown triggered via ${signal}. Cleaning up resources...`);
server.close(async () => {
console.log("✔ HTTP Server closed.");
try {
// Terminate cross-instance pg LISTEN/NOTIFY client
if (realtimeProvider && typeof realtimeProvider.stopListening === "function") {
await realtimeProvider.stopListening();
console.log("✔ Realtime listeners stopped.");
}
// Disconnect dynamic branch connection pools
if (internals.poolManager) {
await internals.poolManager.destroy();
console.log("✔ Branch connection pools evicted.");
}
// End the main database pool
await pool.end();
console.log("✔ Database connection pool drained.");
process.exit(0);
} catch (err) {
console.error("❌ Error during graceful shutdown:", err);
process.exit(1);
}
});
};
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
process.on("SIGINT", () => handleShutdown("SIGINT"));
}
startServer();

Standard path: if your server uses initializeRebaseBackend (like the scaffolded template does), don’t hand-roll the shutdown handler above — use the built-in helper instead. It drains HTTP, stops the cron scheduler, tears down realtime services, guards against repeated signals, and force-exits if shutdown hangs:

import { installShutdownHandlers } from "@rebasepro/server";
const backend = await initializeRebaseBackend({ ... });
installShutdownHandlers(backend, { onCleanup: () => pool.end() });

Do not combine it with your own server.close()backend.shutdown() already closes the server, and a second close deadlocks. The manual handler shown in the example above is only for fully custom setups that bypass initializeRebaseBackend.


If you define the DATABASE_READ_URL environment variable, Rebase automatically spawns a secondary connection pool targeting your read replica. The bootstrapper registers this under internals.readDb. The core EntityFetchService routes all SELECT queries to the replica pool to optimize performance, while mutation queries remain on the primary pool.

You do not have to choose between Rebase and Drizzle. The bootstrapper compiles your schemas dynamically. You can access the compiled Drizzle NodePgDatabase client via internals.db, allowing you to run raw SQL migrations or invoke type-safe Drizzle builders alongside Rebase’s REST services.

In serverless environments or orchestrators (like Kubernetes), terminating pods can result in broken connections. Always implement signal handlers that invoke realtimeProvider.stopListening() (which terminates the dedicated pg LISTEN client) and pool.end() to prevent leaking connection slots in your database server.