Skip to content

Commit a1a7539

Browse files
committed
feat: migrate database from SQLite to Supabase PostgreSQL
- Replace node:sqlite with pg (node-postgres) - All DB operations now async with connection pooling - Supabase project: CodeAbyss (ap-southeast-1) - Add DATABASE_URL to render.yaml - Token blacklist cached in-memory for sync middleware - SQLite syntax converted to PostgreSQL ( params, ON CONFLICT, NOW())
1 parent e9a6223 commit a1a7539

7 files changed

Lines changed: 454 additions & 240 deletions

File tree

package-lock.json

Lines changed: 160 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

render.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ services:
1414
generateValue: true
1515
- key: ALLOWED_ORIGINS
1616
value: https://codeabyss.vercel.app,https://web-cyan-psi-exrofqp3g8.vercel.app
17+
- key: DATABASE_URL
18+
value: postgresql://postgres.nnivlxbtuovxytwleotf:AOsdlQAWyW4ETUoA@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres
1719
- key: NTFY_TOPIC
1820
value: codeabyss-admin-alerts
1921
- key: OPENROUTER_API_KEYS

services/api-node/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
"helmet": "^8.0.0",
2323
"jsonwebtoken": "^9.0.2",
2424
"nanoid": "^5.0.9",
25+
"pg": "^8.20.0",
2526
"typescript": "^5.7.2",
2627
"ws": "^8.18.0",
2728
"zod": "^3.24.1"
2829
},
2930
"devDependencies": {
31+
"@types/pg": "^8.20.0",
3032
"tsx": "^4.19.2"
3133
}
3234
}

services/api-node/src/auth.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createHash, timingSafeEqual } from "node:crypto";
22
import jwt from "jsonwebtoken";
33
import { nanoid } from "nanoid";
44
import { config } from "./config.js";
5-
import { db } from "./db.js";
5+
import { queryOne, queryAll, execute } from "./db.js";
66
import type { Request, Response, NextFunction } from "express";
77

88
export type UserRow = {
@@ -36,22 +36,23 @@ function verifyPassword(password: string, hash: string): boolean {
3636
}
3737
}
3838

39-
export function registerUser(username: string, password: string, displayName?: string): UserRow | null {
39+
export async function registerUser(username: string, password: string, displayName?: string): Promise<UserRow | null> {
4040
const lowerUsername = username.toLowerCase();
41-
const existing = db.prepare("SELECT id FROM users WHERE username = ?").get(lowerUsername);
41+
const existing = await queryOne("SELECT id FROM users WHERE username = $1", [lowerUsername]);
4242
if (existing) return null;
4343

4444
const id = nanoid();
4545
const hash = hashPassword(password);
46-
db.prepare(
47-
"INSERT INTO users (id, username, password_hash, display_name, role, plan) VALUES (?, ?, ?, ?, ?, ?)"
48-
).run(id, lowerUsername, hash, displayName || username, "user", "free");
46+
await execute(
47+
"INSERT INTO users (id, username, password_hash, display_name, role, plan) VALUES ($1, $2, $3, $4, $5, $6)",
48+
[id, lowerUsername, hash, displayName || username, "user", "free"]
49+
);
4950

50-
return db.prepare("SELECT * FROM users WHERE id = ?").get(id) as UserRow;
51+
return await queryOne<UserRow>("SELECT * FROM users WHERE id = $1", [id]) ?? null;
5152
}
5253

53-
export function loginUser(username: string, password: string): { token: string; user: Omit<UserRow, "password_hash"> } | null {
54-
const user = db.prepare("SELECT * FROM users WHERE username = ?").get(username.toLowerCase()) as UserRow | undefined;
54+
export async function loginUser(username: string, password: string): Promise<{ token: string; user: Omit<UserRow, "password_hash"> } | null> {
55+
const user = await queryOne<UserRow>("SELECT * FROM users WHERE username = $1", [username.toLowerCase()]);
5556
if (!user) return null;
5657
if (!verifyPassword(password, user.password_hash)) return null;
5758

@@ -67,23 +68,33 @@ export function loginUser(username: string, password: string): { token: string;
6768

6869
export function verifyToken(token: string) {
6970
try {
70-
if (isTokenRevoked(token)) return null;
71+
if (isTokenRevokedSync(token)) return null;
7172
return jwt.verify(token, config.jwtSecret) as { userId: string; username: string; role: string; plan: string };
7273
} catch {
7374
return null;
7475
}
7576
}
7677

77-
export function revokeToken(token: string) {
78+
// Token blacklist cache for synchronous checks in middleware
79+
const revokedTokens = new Set<string>();
80+
81+
export async function revokeToken(token: string) {
7882
const hash = createHash("sha256").update(token).digest("hex");
7983
const decoded = jwt.decode(token) as { exp?: number } | null;
8084
const expiresAt = decoded?.exp ? new Date(decoded.exp * 1000).toISOString() : new Date(Date.now() + 7 * 86400000).toISOString();
81-
db.prepare("INSERT OR IGNORE INTO token_blacklist (token_hash, expires_at) VALUES (?, ?)").run(hash, expiresAt);
85+
await execute("INSERT INTO token_blacklist (token_hash, expires_at) VALUES ($1, $2) ON CONFLICT DO NOTHING", [hash, expiresAt]);
86+
revokedTokens.add(hash);
8287
}
8388

84-
function isTokenRevoked(token: string): boolean {
89+
function isTokenRevokedSync(token: string): boolean {
8590
const hash = createHash("sha256").update(token).digest("hex");
86-
return !!db.prepare("SELECT 1 FROM token_blacklist WHERE token_hash = ?").get(hash);
91+
return revokedTokens.has(hash);
92+
}
93+
94+
// Load revoked tokens into cache on startup
95+
export async function loadRevokedTokens() {
96+
const rows = await queryAll<{ token_hash: string }>("SELECT token_hash FROM token_blacklist WHERE expires_at > NOW()");
97+
for (const row of rows) revokedTokens.add(row.token_hash);
8798
}
8899

89100
// Optional auth middleware - attaches user if token present, doesn't block
@@ -125,20 +136,20 @@ export function requireAdmin(req: Request, res: Response, next: NextFunction) {
125136
}
126137

127138
// Check AI rate limits
128-
export function checkAILimit(userId?: string): { allowed: boolean; remaining: number } {
139+
export async function checkAILimit(userId?: string): Promise<{ allowed: boolean; remaining: number }> {
129140
if (!userId) return { allowed: true, remaining: 50 }; // anonymous gets 50/day
130141

131-
const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId) as UserRow | undefined;
142+
const user = await queryOne<UserRow>("SELECT * FROM users WHERE id = $1", [userId]);
132143
if (!user) return { allowed: true, remaining: 50 };
133144

134145
// Admin bypasses limits
135146
if (user.role === "admin") return { allowed: true, remaining: 9999 };
136147

137148
// Reset counter if new day
138149
const today = new Date().toISOString().split("T")[0];
139-
const resetDay = user.ai_requests_reset.split("T")[0];
150+
const resetDay = new Date(user.ai_requests_reset).toISOString().split("T")[0];
140151
if (today !== resetDay) {
141-
db.prepare("UPDATE users SET ai_requests_today = 0, ai_requests_reset = CURRENT_TIMESTAMP WHERE id = ?").run(userId);
152+
await execute("UPDATE users SET ai_requests_today = 0, ai_requests_reset = CURRENT_TIMESTAMP WHERE id = $1", [userId]);
142153
return { allowed: true, remaining: user.plan === "pro" ? 500 : 50 };
143154
}
144155

@@ -147,15 +158,15 @@ export function checkAILimit(userId?: string): { allowed: boolean; remaining: nu
147158
return { allowed: remaining > 0, remaining };
148159
}
149160

150-
export function incrementAIUsage(userId: string) {
151-
db.prepare("UPDATE users SET ai_requests_today = ai_requests_today + 1 WHERE id = ?").run(userId);
161+
export async function incrementAIUsage(userId: string) {
162+
await execute("UPDATE users SET ai_requests_today = ai_requests_today + 1 WHERE id = $1", [userId]);
152163
}
153164

154-
export function getAnalytics() {
155-
const users = (db.prepare("SELECT COUNT(*) as count FROM users").get() as any).count;
156-
const projects = (db.prepare("SELECT COUNT(*) as count FROM projects").get() as any).count;
157-
const agentRuns = (db.prepare("SELECT COUNT(*) as count FROM agent_runs").get() as any).count;
158-
const donations = (db.prepare("SELECT COALESCE(SUM(amount), 0) as total FROM donations").get() as any).total;
159-
const proUsers = (db.prepare("SELECT COUNT(*) as count FROM users WHERE plan = 'pro'").get() as any).count;
165+
export async function getAnalytics() {
166+
const users = (await queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users"))?.count || 0;
167+
const projects = (await queryOne<{ count: number }>("SELECT COUNT(*) as count FROM projects"))?.count || 0;
168+
const agentRuns = (await queryOne<{ count: number }>("SELECT COUNT(*) as count FROM agent_runs"))?.count || 0;
169+
const donations = (await queryOne<{ total: number }>("SELECT COALESCE(SUM(amount), 0) as total FROM donations"))?.total || 0;
170+
const proUsers = (await queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users WHERE plan = 'pro'"))?.count || 0;
160171
return { users, projects, agentRuns, totalDonations: donations, proUsers };
161172
}

services/api-node/src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ if (jwtSecret === "CodeAbyss-dev-only-change-me" && process.env.NODE_ENV === "pr
1313
export const config = {
1414
port: Number(process.env.NODE_API_PORT || 8787),
1515
jwtSecret,
16-
sqlitePath: process.env.SQLITE_PATH || path.resolve(process.cwd(), "data/CodeAbyss.db"),
16+
databaseUrl: process.env.DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/codeabyss",
1717
workspaceRoot: process.env.WORKSPACE_ROOT || path.resolve(process.cwd(), "workspaces"),
1818
allowedOrigins: (process.env.ALLOWED_ORIGINS || "http://localhost:3000,http://127.0.0.1:3000").split(","),
1919
pythonServiceUrl: process.env.PYTHON_SERVICE_URL || "http://127.0.0.1:8788",

0 commit comments

Comments
 (0)