From 18382ee54567e1874dbdd38e326bebfb160bb0c8 Mon Sep 17 00:00:00 2001 From: codebestia Date: Tue, 12 May 2026 13:37:28 +0100 Subject: [PATCH 1/5] feat(backend): install drizzle-orm + postgres driver, add db connection --- apps/backend/package.json | 2 ++ apps/backend/src/db/index.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 apps/backend/src/db/index.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 3c74587e..f0f86761 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -17,8 +17,10 @@ "dependencies": { "cors": "^2.8.6", "dotenv": "^17.3.1", + "drizzle-orm": "^0.45.2", "express": "^5.2.1", "morgan": "^1.10.1", + "postgres": "^3.4.9", "socket.io": "^4.8.3" }, "devDependencies": { diff --git a/apps/backend/src/db/index.ts b/apps/backend/src/db/index.ts new file mode 100644 index 00000000..32b6b981 --- /dev/null +++ b/apps/backend/src/db/index.ts @@ -0,0 +1,13 @@ +import postgres from 'postgres'; +import { drizzle } from 'drizzle-orm/postgres-js'; +import * as schema from './schema.js'; + +const connectionString = process.env['DATABASE_URL']; + +if (!connectionString) { + throw new Error('DATABASE_URL is not set'); +} + +const client = postgres(connectionString); + +export const db = drizzle(client, { schema }); From e02d527a7c4a6762fe494ab649068c79eef42a89 Mon Sep 17 00:00:00 2001 From: codebestia Date: Tue, 12 May 2026 13:37:47 +0100 Subject: [PATCH 2/5] feat(backend): add users and wallets schema --- apps/backend/src/db/schema.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 apps/backend/src/db/schema.ts diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts new file mode 100644 index 00000000..7d69fc01 --- /dev/null +++ b/apps/backend/src/db/schema.ts @@ -0,0 +1,24 @@ +import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: uuid('id').primaryKey().defaultRandom(), + username: text('username').unique(), + avatarUrl: text('avatar_url'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); + +export const wallets = pgTable('wallets', { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + address: text('address').notNull().unique(), + isPrimary: boolean('is_primary').notNull().default(false), + createdAt: timestamp('created_at').notNull().defaultNow(), +}); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; +export type Wallet = typeof wallets.$inferSelect; +export type NewWallet = typeof wallets.$inferInsert; From 626c46982bd17c52beeb93420ff2ff85e9e8db59 Mon Sep 17 00:00:00 2001 From: codebestia Date: Tue, 12 May 2026 13:38:20 +0100 Subject: [PATCH 3/5] feat(backend): add drizzle config and db scripts --- apps/backend/drizzle.config.ts | 10 ++++++++++ apps/backend/package.json | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 apps/backend/drizzle.config.ts diff --git a/apps/backend/drizzle.config.ts b/apps/backend/drizzle.config.ts new file mode 100644 index 00000000..0c0c5e24 --- /dev/null +++ b/apps/backend/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env['DATABASE_URL'] ?? '', + }, +}); diff --git a/apps/backend/package.json b/apps/backend/package.json index f0f86761..9ce57655 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -8,7 +8,11 @@ "test": "echo \"Error: no test specified\" && exit 1", "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/index.js", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" }, "keywords": [], "author": "", @@ -28,6 +32,7 @@ "@types/express": "^5.0.6", "@types/morgan": "^1.9.10", "@types/node": "^20.19.37", + "drizzle-kit": "^0.31.10", "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "^5.9.3" From 04f74bc7e912b802c42f26e9a1f52dda8a9e9b14 Mon Sep 17 00:00:00 2001 From: codebestia Date: Tue, 12 May 2026 13:38:55 +0100 Subject: [PATCH 4/5] feat(backend): wire db into server, add db check to /health --- apps/backend/src/index.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 1772a856..15384255 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -4,6 +4,8 @@ import dotenv from 'dotenv'; import { createServer } from 'http'; import { Server } from 'socket.io'; import morgan from 'morgan'; +import { db } from './db/index.js'; +import { sql } from 'drizzle-orm'; dotenv.config(); @@ -17,8 +19,13 @@ app.use(cors()); app.use(express.json()); app.use(morgan('dev')); -app.get('/health', (req, res) => { - res.json({ status: 'ok' }); +app.get('/health', async (_req, res) => { + try { + await db.execute(sql`SELECT 1`); + res.json({ status: 'ok', db: 'connected' }); + } catch { + res.status(503).json({ status: 'error', db: 'unreachable' }); + } }); io.on('connection', (socket) => { From d1ae630248d6ab3c706a262575b76bda1af5b839 Mon Sep 17 00:00:00 2001 From: codebestia Date: Tue, 12 May 2026 13:46:43 +0100 Subject: [PATCH 5/5] feat(backend): generate initial migration (users, wallets) --- apps/backend/drizzle/0000_init.sql | 19 +++ apps/backend/drizzle/meta/0000_snapshot.json | 141 +++++++++++++++++++ apps/backend/drizzle/meta/_journal.json | 13 ++ 3 files changed, 173 insertions(+) create mode 100644 apps/backend/drizzle/0000_init.sql create mode 100644 apps/backend/drizzle/meta/0000_snapshot.json create mode 100644 apps/backend/drizzle/meta/_journal.json diff --git a/apps/backend/drizzle/0000_init.sql b/apps/backend/drizzle/0000_init.sql new file mode 100644 index 00000000..268ee098 --- /dev/null +++ b/apps/backend/drizzle/0000_init.sql @@ -0,0 +1,19 @@ +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "username" text, + "avatar_url" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "users_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "wallets" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "address" text NOT NULL, + "is_primary" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "wallets_address_unique" UNIQUE("address") +); +--> statement-breakpoint +ALTER TABLE "wallets" ADD CONSTRAINT "wallets_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..969e45b0 --- /dev/null +++ b/apps/backend/drizzle/meta/0000_snapshot.json @@ -0,0 +1,141 @@ +{ + "id": "ccda4e45-b486-4477-892a-b4ff308b3326", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wallets": { + "name": "wallets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "wallets_user_id_users_id_fk": { + "name": "wallets_user_id_users_id_fk", + "tableFrom": "wallets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "wallets_address_unique": { + "name": "wallets_address_unique", + "nullsNotDistinct": false, + "columns": [ + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json new file mode 100644 index 00000000..335366f9 --- /dev/null +++ b/apps/backend/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1778589700461, + "tag": "0000_init", + "breakpoints": true + } + ] +} \ No newline at end of file