tools/llm-sequential-upgrade/backends/postgres.md
Instructions for generating, building, and deploying the PostgreSQL backend.
Do NOT read SpacetimeDB SDK rule files. This backend uses standard Node.js/TypeScript patterns.
The server handles:
PostgreSQL is already running in a Docker container.
| Parameter | Value |
|---|---|
| Host | localhost |
| Port | 6432 (mapped from container 5432) |
| User | spacetime |
| Password | spacetime |
| Database | spacetime |
| Container | spacetime-web-postgres-1 |
| Connection URL | postgresql://spacetime:spacetime@localhost:6432/spacetime |
docker exec spacetime-web-postgres-1 psql -U spacetime -d spacetime -c "SELECT 1"
If PostgreSQL is not reachable, STOP and report the error.
<app-dir>/
server/
package.json
tsconfig.json
drizzle.config.ts
.env
src/
schema.ts # Drizzle ORM table definitions
index.ts # Express server + Socket.io + routes
client/
package.json
vite.config.ts
tsconfig.json
index.html
src/
main.tsx # React entry point
App.tsx # Main application component
styles.css # Dark theme styling
Create the Express + Socket.io server:
server/package.json:
{
"name": "chat-server",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts"
},
"dependencies": {
"express": "^4.18.2",
"@types/express": "^4.17.21",
"drizzle-orm": "^0.39.0",
"pg": "^8.13.0",
"@types/pg": "^8.11.0",
"socket.io": "^4.7.4",
"cors": "^2.8.5",
"@types/cors": "^2.8.17",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.30.0",
"tsx": "^4.19.0",
"typescript": "^5.4.0"
}
}
server/tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src/**/*"]
}
server/.env:
DATABASE_URL=postgresql://spacetime:spacetime@localhost:6432/spacetime
PORT=6001
server/drizzle.config.ts:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL || 'postgresql://spacetime:spacetime@localhost:6432/spacetime',
},
});
server/src/schema.ts — Drizzle ORM table definitions for all features
server/src/index.ts — Express server with:
http://localhost:6273Install and push schema:
cd <server-dir> && npm install
npx drizzle-kit push
Skip — PostgreSQL has no binding generation. The client calls REST/Socket.io APIs directly.
client/package.json:
{
"name": "chat-client",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"socket.io-client": "^4.7.4"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.4.0",
"vite": "^6.0.0"
}
}
client/vite.config.ts — port 6273 (NOT 6173 — that's SpacetimeDB), proxy /api and /socket.io to http://localhost:6001
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 6273,
proxy: {
'/api': 'http://localhost:6001',
'/socket.io': {
target: 'http://localhost:6001',
ws: true,
},
},
},
});
client/tsconfig.json
client/index.html
client/src/main.tsx — React entry point
client/src/App.tsx — Main component using fetch('/api/...') + Socket.io client
client/src/styles.css — Dark theme styling
The client connects to the server via the Vite proxy — no hardcoded localhost:6001 in client code.
Critical: Initialize the socket.io client without a hardcoded URL so it routes through the Vite proxy (e.g. io() or io({ path: '/socket.io' })). Hardcoding http://localhost:6001 bypasses the proxy and breaks WebSocket upgrades.
# Server
cd <server-dir> && npm install && npx tsc --noEmit
# Client
cd <client-dir> && npm install && npx tsc --noEmit && npm run build
Both must pass. If either fails:
# Kill any existing servers
npx kill-port 6273 2>/dev/null || true
npx kill-port 6001 2>/dev/null || true
# Start the API server in background
cd <server-dir> && npx tsx src/index.ts &
# Wait for API server to be ready (poll http://localhost:6001 up to 30s)
# Start client dev server in background
cd <client-dir> && npm run dev &
Wait for both servers to be ready:
http://localhost:6001http://localhost:6273npx kill-port 6001 2>/dev/null || true
cd <server-dir> && npx tsx src/index.ts &
cd <server-dir> && npx drizzle-kit push
For context on what makes this backend different (this helps the benchmark comparison):
| Aspect | SpacetimeDB | PostgreSQL |
|---|---|---|
| Real-time | Built-in subscriptions | Socket.io (manual) |
| API layer | Reducers (auto-exposed) | Express routes (manual) |
| Schema | table() + reducer() | Drizzle pgTable() |
| Bindings | Auto-generated types | Manual type definitions |
| Deployment | spacetime publish | Start Express server |
| State sync | Automatic client cache | Manual fetch + Socket.io |
| Online presence | Via lifecycle hooks | Manual Socket.io tracking |
| Typing indicators | Reducer + subscription | Socket.io events |
| Infra dependencies | SpacetimeDB only | PostgreSQL + Express + Socket.io + CORS |
<title> MUST be "PostgreSQL Chat" (not "Chat App", not "SpacetimeDB Chat")| Service | Port | Notes |
|---|---|---|
| PostgreSQL (Docker) | 6432 | Database |
| Express API server | 6001 | REST + Socket.io |
| Vite dev server | 6273 | React client — NOT 6173 (that's SpacetimeDB) |
The language and feature prompt files are provided as absolute paths in the launch prompt. No additional reference files are needed — this backend uses standard Node.js/TypeScript patterns.