fix: update schema to fix multiple primary key issues and switch to node-postgres

- Replace primaryKey constraints with uniqueIndex where needed
- Fix schema for Auth, Contacts, Exchange Rates, Expense Participants, and Accounts tables
- Add missing import for uniqueIndex
- Switch from postgres.js to node-postgres (pg) for Drizzle
- Add proper type safety with TypeScript
- Add graceful shutdown of database connections
This commit is contained in:
Yadunand Prem 2025-05-27 18:01:40 -04:00
parent 74b7ddf3d6
commit f9d5f8b404
No known key found for this signature in database
17 changed files with 1618 additions and 25 deletions

0
CLAUDE.md Normal file
View File

101
README.md Normal file
View File

@ -0,0 +1,101 @@
# Splitwise Application
A Splitwise-style expense-splitting application with double-entry bookkeeping and multi-currency support.
## Getting Started with Docker
The application is containerized using Docker, making it easy to set up and run locally.
### Prerequisites
- Docker
- Docker Compose
### Running the Application
1. Clone the repository:
```bash
git clone <repository-url>
cd splitwise
```
2. Start the database and backend services:
```bash
docker-compose up -d
```
3. Generate and run database migrations:
```bash
# Generate migrations from schema
docker-compose run backend bun run db:generate
# Run migrations
docker-compose run --profile migration migration
```
4. Access the application:
- Backend API: http://localhost:3000
- Drizzle Studio (database UI): http://localhost:4000
### Useful Commands
#### Start specific services:
```bash
# Start only the database
docker-compose up -d postgres
# Start Drizzle Studio for database management
docker-compose --profile studio up drizzle-studio
# Run migrations
docker-compose --profile migration up migration
```
#### View logs:
```bash
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f backend
```
#### Rebuild containers:
```bash
docker-compose build
```
#### Stop all services:
```bash
docker-compose down
```
#### Reset everything (including database volume):
```bash
docker-compose down -v
```
## Development
### Project Structure
- `/backend` - Backend API built with Bun, Hono, and Drizzle ORM
- `/frontend` - Frontend application (to be implemented)
- `/docs` - Project documentation
### Tech Stack
#### Backend
- **Runtime**: Bun
- **Framework**: Hono (TypeScript-first web framework)
- **Database**: PostgreSQL
- **ORM**: Drizzle
- **Authentication**: better-auth
- **Validation**: Zod
#### Frontend (planned)
- **Framework**: React with TypeScript
- **Build Tool**: Vite
- **Styling**: TailwindCSS with Shadcn UI
- **State Management**: Zustand
- **Data Fetching**: TanStack Query

5
backend/.env.example Normal file
View File

@ -0,0 +1,5 @@
PORT=3000
DATABASE_URL=postgres://postgres:postgres@localhost:5432/splitwise
JWT_SECRET=your-jwt-secret-key-here
NODE_ENV=development
DATABASE_SSL=false

38
backend/Dockerfile Normal file
View File

@ -0,0 +1,38 @@
FROM oven/bun:1.2 as base
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json bun.lock ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy source code
COPY . .
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Expose port
EXPOSE 3000
# Start the application
CMD ["bun", "run", "start"]
# Development stage
FROM base as development
# Override environment variables for development
ENV NODE_ENV=development
# Start development server
CMD ["bun", "run", "dev"]
# Migration stage
FROM base as migration
# Run migrations
CMD ["bun", "run", "db:migrate"]

View File

@ -8,7 +8,6 @@
"drizzle-orm": "^0.30.5",
"hono": "^4.2.4",
"pg": "^8.11.3",
"postgres": "^3.4.3",
"zod": "^3.22.4",
},
"devDependencies": {

View File

@ -15,7 +15,6 @@
"drizzle-orm": "^0.30.5",
"hono": "^4.2.4",
"pg": "^8.11.3",
"postgres": "^3.4.3",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@ -1,19 +1,60 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { Pool } from 'pg';
import fs from 'node:fs';
import path from 'node:path';
// Get database connection string from environment variable or use default
const connectionString = process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/splitwise';
// Create a PostgreSQL client
const client = postgres(connectionString, { max: 10 });
// Configure connection options
const poolConfig = {
connectionString,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : undefined,
};
// Create a PostgreSQL connection pool
const pool = new Pool(poolConfig);
// Add error handler for unexpected errors
pool.on('error', (err) => {
console.error('Unexpected database error:', err);
});
// Create a Drizzle ORM instance
export const db = drizzle(client);
export const db = drizzle(pool);
// Function to ensure migrations directory exists
function ensureMigrationsDir() {
const migrationsFolder = './src/db/migrations';
const metaFolder = path.join(migrationsFolder, 'meta');
// Create migrations directory if it doesn't exist
if (!fs.existsSync(migrationsFolder)) {
fs.mkdirSync(migrationsFolder, { recursive: true });
}
// Create meta directory if it doesn't exist
if (!fs.existsSync(metaFolder)) {
fs.mkdirSync(metaFolder, { recursive: true });
}
// Create an empty journal file if it doesn't exist
const journalPath = path.join(metaFolder, '_journal.json');
if (!fs.existsSync(journalPath)) {
fs.writeFileSync(journalPath, JSON.stringify({ entries: [] }));
}
}
// Function to run migrations
export async function runMigrations() {
try {
console.log('Ensuring migrations directory exists...');
ensureMigrationsDir();
console.log('Running migrations...');
await migrate(db, { migrationsFolder: './src/db/migrations' });
console.log('Migrations completed successfully');
@ -21,4 +62,9 @@ export async function runMigrations() {
console.error('Migration failed:', error);
throw error;
}
}
// Function to close the database connection
export async function closeDb() {
await pool.end();
}

View File

@ -0,0 +1,252 @@
CREATE TABLE IF NOT EXISTS "accounts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"type" varchar(20) NOT NULL,
"currency_code" varchar(3) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "accounts_user_id_currency_code_pk" PRIMARY KEY("user_id","currency_code")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "currencies" (
"code" varchar(3) PRIMARY KEY NOT NULL,
"name" varchar(100) NOT NULL,
"symbol" varchar(10) NOT NULL,
"decimal_digits" integer DEFAULT 2 NOT NULL,
"is_active" boolean DEFAULT true NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "exchange_rates" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"from_currency_code" varchar(3) NOT NULL,
"to_currency_code" varchar(3) NOT NULL,
"rate" varchar(24) NOT NULL,
"effective_date" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "exchange_rates_from_currency_code_to_currency_code_effective_date_pk" PRIMARY KEY("from_currency_code","to_currency_code","effective_date")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expense_participants" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"expense_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
"share_amount" numeric(20, 2) NOT NULL,
"share_percentage" numeric(5, 2),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "expense_participants_expense_id_user_id_pk" PRIMARY KEY("expense_id","user_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "expenses" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar(255) NOT NULL,
"description" text,
"amount" numeric(20, 2) NOT NULL,
"currency_code" varchar(3) NOT NULL,
"date" timestamp with time zone NOT NULL,
"payer_id" uuid NOT NULL,
"receipt_image_url" text,
"created_by" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "auth" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"provider" varchar(50) NOT NULL,
"provider_user_id" varchar(255),
"email" varchar(255) NOT NULL,
"password_hash" text,
"refresh_token" text,
"last_login" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "auth_provider_provider_user_id_pk" PRIMARY KEY("provider","provider_user_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "contacts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"contact_id" uuid NOT NULL,
"status" varchar(20) DEFAULT 'pending' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "contacts_user_id_contact_id_pk" PRIMARY KEY("user_id","contact_id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "notifications" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"type" varchar(50) NOT NULL,
"content" jsonb NOT NULL,
"is_read" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "settlements" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"payer_id" uuid NOT NULL,
"receiver_id" uuid NOT NULL,
"amount" numeric(20, 2) NOT NULL,
"currency_code" varchar(3) NOT NULL,
"date" timestamp with time zone NOT NULL,
"memo" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "transactions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"expense_id" uuid,
"settlement_id" uuid,
"from_account_id" uuid NOT NULL,
"to_account_id" uuid NOT NULL,
"amount" numeric(20, 2) NOT NULL,
"currency_code" varchar(3) NOT NULL,
"exchange_rate" numeric(24, 12) DEFAULT '1' NOT NULL,
"date" timestamp with time zone NOT NULL,
"type" varchar(20) NOT NULL,
"memo" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" varchar(255) NOT NULL,
"display_name" varchar(255) NOT NULL,
"profile_photo_url" text,
"default_currency_code" varchar(3) DEFAULT 'USD' NOT NULL,
"locale" varchar(10) DEFAULT 'en-US' NOT NULL,
"timezone" varchar(50) DEFAULT 'UTC' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "exchange_rates" ADD CONSTRAINT "exchange_rates_from_currency_code_currencies_code_fk" FOREIGN KEY ("from_currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "exchange_rates" ADD CONSTRAINT "exchange_rates_to_currency_code_currencies_code_fk" FOREIGN KEY ("to_currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expense_participants" ADD CONSTRAINT "expense_participants_expense_id_expenses_id_fk" FOREIGN KEY ("expense_id") REFERENCES "expenses"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expense_participants" ADD CONSTRAINT "expense_participants_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_payer_id_users_id_fk" FOREIGN KEY ("payer_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "expenses" ADD CONSTRAINT "expenses_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "auth" ADD CONSTRAINT "auth_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_contact_id_users_id_fk" FOREIGN KEY ("contact_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_payer_id_users_id_fk" FOREIGN KEY ("payer_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_receiver_id_users_id_fk" FOREIGN KEY ("receiver_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "settlements" ADD CONSTRAINT "settlements_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_expense_id_expenses_id_fk" FOREIGN KEY ("expense_id") REFERENCES "expenses"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_settlement_id_settlements_id_fk" FOREIGN KEY ("settlement_id") REFERENCES "settlements"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_from_account_id_accounts_id_fk" FOREIGN KEY ("from_account_id") REFERENCES "accounts"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_to_account_id_accounts_id_fk" FOREIGN KEY ("to_account_id") REFERENCES "accounts"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_currency_code_currencies_code_fk" FOREIGN KEY ("currency_code") REFERENCES "currencies"("code") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1748383049113,
"tag": "0000_married_pestilence",
"breakpoints": true
}
]
}

View File

@ -1,4 +1,4 @@
import { pgTable, varchar, integer, boolean, uuid, timestamp, primaryKey } from 'drizzle-orm/pg-core';
import { pgTable, varchar, integer, boolean, uuid, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './users';
// Currencies table
@ -20,7 +20,7 @@ export const exchangeRates = pgTable('exchange_rates', {
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
currencyPairDateIdx: primaryKey({ columns: [table.fromCurrencyCode, table.toCurrencyCode, table.effectiveDate] }),
currencyPairDateIdx: uniqueIndex('currency_pair_date_idx').on(table.fromCurrencyCode, table.toCurrencyCode, table.effectiveDate),
};
});
@ -34,6 +34,6 @@ export const accounts = pgTable('accounts', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
userCurrencyUniqueIdx: primaryKey({ columns: [table.userId, table.currencyCode] }),
userCurrencyUniqueIdx: uniqueIndex('user_currency_unique_idx').on(table.userId, table.currencyCode),
};
});

View File

@ -1,4 +1,4 @@
import { pgTable, uuid, varchar, text, timestamp, decimal, primaryKey } from 'drizzle-orm/pg-core';
import { pgTable, uuid, varchar, text, timestamp, decimal, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './users';
import { currencies } from './currencies';
@ -28,6 +28,6 @@ export const expenseParticipants = pgTable('expense_participants', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
expenseUserUniqueIdx: primaryKey({ columns: [table.expenseId, table.userId] }),
expenseUserUniqueIdx: uniqueIndex('expense_user_unique_idx').on(table.expenseId, table.userId),
};
});

View File

@ -1,4 +1,4 @@
import { pgTable, uuid, varchar, text, timestamp, primaryKey } from 'drizzle-orm/pg-core';
import { pgTable, uuid, varchar, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
// Users table
export const users = pgTable('users', {
@ -27,7 +27,7 @@ export const auth = pgTable('auth', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
providerUniqueIdx: primaryKey({ columns: [table.provider, table.providerUserId] }),
providerUniqueIdx: uniqueIndex('provider_user_id_unique_idx').on(table.provider, table.providerUserId),
};
});
@ -41,6 +41,6 @@ export const contacts = pgTable('contacts', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => {
return {
userContactUniqueIdx: primaryKey({ columns: [table.userId, table.contactId] }),
userContactUniqueIdx: uniqueIndex('user_contact_unique_idx').on(table.userId, table.contactId),
};
});

View File

@ -3,7 +3,9 @@ import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
import { prettyJSON } from 'hono/pretty-json';
import { db, runMigrations } from './db';
import { db, runMigrations, closeDb } from './db';
import { AppEnv } from './types/hono';
import { dbMiddleware } from './middleware/db';
// Import API routes (will be created later)
// import { authRoutes } from './controllers/auth';
@ -13,8 +15,8 @@ import { db, runMigrations } from './db';
// Define port from environment or default
const PORT = process.env.PORT || 3000;
// Create a new Hono app
const app = new Hono();
// Create a new Hono app with typed environment
const app = new Hono<AppEnv>();
// Initialize the database
const initDb = async () => {
@ -33,18 +35,22 @@ app.use('*', cors());
app.use('*', secureHeaders());
app.use('*', prettyJSON());
// Add db to context
app.use('*', async (c, next) => {
c.set('db', db);
await next();
});
// Add db to context with proper type checking
app.use('*', dbMiddleware);
// Health check route
app.get('/', (c) => {
app.get('/', async (c) => {
// We can safely access the db from the context with type information
const db = c.get('db');
// We'll just check if the database connection is working
// but won't actually run a query for the health check
return c.json({
status: 'ok',
message: 'Splitwise API is running',
version: '1.0.0',
database: 'Connected', // In a real implementation, we'd check db connectivity
});
});
@ -73,6 +79,19 @@ app.notFound((c) => {
// Initialize database and start the server
initDb().catch(console.error);
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('Gracefully shutting down...');
await closeDb();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('Gracefully shutting down...');
await closeDb();
process.exit(0);
});
console.log(`Server is running on port ${PORT}`);
export default {
port: PORT,

View File

@ -0,0 +1,12 @@
import type { MiddlewareHandler } from 'hono';
import { db } from '../db';
import type { AppEnv } from '../types/hono';
/**
* Middleware to add the database to the request context.
* This middleware is properly typed with the AppEnv interface.
*/
export const dbMiddleware: MiddlewareHandler<AppEnv> = async (c, next) => {
c.set('db', db);
await next();
};

15
backend/src/types/hono.ts Normal file
View File

@ -0,0 +1,15 @@
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../db/schema';
// Define the application environment with custom variables
export type AppEnv = {
Variables: {
db: NodePgDatabase<typeof schema>;
userId?: string; // For authenticated user
};
};
// Export to be used in middleware and route handlers
declare module 'hono' {
interface ContextVariableMap extends AppEnv['Variables'] {}
}

74
docker-compose.yml Normal file
View File

@ -0,0 +1,74 @@
version: '3.8'
services:
# PostgreSQL database
postgres:
image: postgres:16-alpine
container_name: splitwise-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: splitwise
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
# Database migration service
migration:
build:
context: ./backend
target: migration
container_name: splitwise-migration
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/splitwise
profiles:
- migration
# Drizzle Studio for database management
drizzle-studio:
build:
context: ./backend
target: development
container_name: splitwise-drizzle-studio
command: bun run db:studio
ports:
- "4000:4000"
environment:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/splitwise
depends_on:
postgres:
condition: service_healthy
profiles:
- studio
# Backend service
backend:
build:
context: ./backend
target: development
container_name: splitwise-backend
ports:
- "3000:3000"
volumes:
- ./backend:/app
- /app/node_modules
environment:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/splitwise
PORT: 3000
NODE_ENV: development
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
volumes:
postgres-data: