diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..d5c844a
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,14 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(td --help)",
+ "Bash(td create:*)",
+ "Bash(EPIC=td-8064d2)",
+ "Bash(__NEW_LINE_51256bdd53dfa89e__ td:*)",
+ "Bash(td list:*)",
+ "Bash(git add:*)",
+ "Bash(git commit:*)",
+ "Bash(td start:*)"
+ ]
+ }
+}
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..f28ec2c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+POSTGRES_USER=budget
+POSTGRES_PASSWORD=budget
+POSTGRES_DB=budget
+DATABASE_URL=postgresql://budget:budget@db:5432/budget
+PORT=3000
diff --git a/CLAUDE.md b/CLAUDE.md
index 2072261..08a1bd6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,3 +24,25 @@ td session --new # force a new session in the same terminal context
```
Task state is stored in `.todos/issues.db` (SQLite).
+
+## Development
+
+**Run production stack (Docker):**
+```bash
+docker compose up
+```
+
+**Run development stack with live reload (Docker):**
+```bash
+docker compose -f docker-compose.yml -f docker-compose.dev.yml up
+```
+
+**Frontend only (Vite dev server):**
+```bash
+cd client && npm install && npm run dev
+```
+
+**Backend only (nodemon):**
+```bash
+cd server && npm install && npm run dev
+```
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..b8bde24
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,27 @@
+# Stage 1: build the React client
+FROM node:20-alpine AS client-build
+
+WORKDIR /app/client
+
+COPY client/package.json ./
+RUN npm install
+
+COPY client/ ./
+RUN npm run build
+
+# Stage 2: production server
+FROM node:20-alpine AS production
+
+WORKDIR /app/server
+
+COPY server/package.json ./
+RUN npm install --omit=dev
+
+COPY server/ ./
+
+# Copy built client assets
+COPY --from=client-build /app/client/dist /app/client/dist
+
+EXPOSE 3000
+
+CMD ["node", "src/index.js"]
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..4cbd44e
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Budget
+
+
+
+
+
+
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..003f89d
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "budget-client",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.23.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^4.3.0",
+ "vite": "^5.2.11"
+ }
+}
diff --git a/client/src/App.jsx b/client/src/App.jsx
new file mode 100644
index 0000000..f0edc5e
--- /dev/null
+++ b/client/src/App.jsx
@@ -0,0 +1,32 @@
+import { Routes, Route, NavLink } from 'react-router-dom';
+import PaycheckView from './pages/PaycheckView.jsx';
+import Bills from './pages/Bills.jsx';
+import Settings from './pages/Settings.jsx';
+import MonthlySummary from './pages/MonthlySummary.jsx';
+import AnnualOverview from './pages/AnnualOverview.jsx';
+
+function App() {
+ return (
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/main.jsx b/client/src/main.jsx
new file mode 100644
index 0000000..973d2df
--- /dev/null
+++ b/client/src/main.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App.jsx';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+);
diff --git a/client/src/pages/AnnualOverview.jsx b/client/src/pages/AnnualOverview.jsx
new file mode 100644
index 0000000..f28d012
--- /dev/null
+++ b/client/src/pages/AnnualOverview.jsx
@@ -0,0 +1,5 @@
+function AnnualOverview() {
+ return Annual Overview
Placeholder — coming soon.
;
+}
+
+export default AnnualOverview;
diff --git a/client/src/pages/Bills.jsx b/client/src/pages/Bills.jsx
new file mode 100644
index 0000000..3aaad13
--- /dev/null
+++ b/client/src/pages/Bills.jsx
@@ -0,0 +1,5 @@
+function Bills() {
+ return Bills
Placeholder — coming soon.
;
+}
+
+export default Bills;
diff --git a/client/src/pages/MonthlySummary.jsx b/client/src/pages/MonthlySummary.jsx
new file mode 100644
index 0000000..264bf7c
--- /dev/null
+++ b/client/src/pages/MonthlySummary.jsx
@@ -0,0 +1,5 @@
+function MonthlySummary() {
+ return Monthly Summary
Placeholder — coming soon.
;
+}
+
+export default MonthlySummary;
diff --git a/client/src/pages/PaycheckView.jsx b/client/src/pages/PaycheckView.jsx
new file mode 100644
index 0000000..3d4fe99
--- /dev/null
+++ b/client/src/pages/PaycheckView.jsx
@@ -0,0 +1,5 @@
+function PaycheckView() {
+ return Paycheck View
Placeholder — coming soon.
;
+}
+
+export default PaycheckView;
diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx
new file mode 100644
index 0000000..9c07995
--- /dev/null
+++ b/client/src/pages/Settings.jsx
@@ -0,0 +1,5 @@
+function Settings() {
+ return Settings
Placeholder — coming soon.
;
+}
+
+export default Settings;
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 0000000..ab4b186
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': 'http://localhost:3001',
+ },
+ },
+});
diff --git a/db/migrations/.gitkeep b/db/migrations/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..0dfe8e1
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,9 @@
+services:
+ app:
+ volumes:
+ - ./server:/app/server
+ - ./client:/app/client
+ ports:
+ - "5173:5173"
+ environment:
+ NODE_ENV: development
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..530238a
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,24 @@
+services:
+ db:
+ image: postgres:16-alpine
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: ${POSTGRES_USER}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ POSTGRES_DB: ${POSTGRES_DB}
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+
+ app:
+ build: .
+ restart: unless-stopped
+ depends_on:
+ - db
+ ports:
+ - "3000:3000"
+ environment:
+ DATABASE_URL: ${DATABASE_URL}
+ PORT: ${PORT}
+
+volumes:
+ pgdata:
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..a6c341e
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "budget-server",
+ "version": "1.0.0",
+ "main": "src/index.js",
+ "scripts": {
+ "start": "node src/index.js",
+ "dev": "nodemon src/index.js"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "pg": "^8.11.5"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.0"
+ }
+}
diff --git a/server/src/db.js b/server/src/db.js
new file mode 100644
index 0000000..f18ec8e
--- /dev/null
+++ b/server/src/db.js
@@ -0,0 +1,7 @@
+const { Pool } = require('pg');
+
+const pool = new Pool({
+ connectionString: process.env.DATABASE_URL,
+});
+
+module.exports = pool;
diff --git a/server/src/index.js b/server/src/index.js
new file mode 100644
index 0000000..1eb1215
--- /dev/null
+++ b/server/src/index.js
@@ -0,0 +1,27 @@
+require('dotenv').config();
+const express = require('express');
+const cors = require('cors');
+const path = require('path');
+const healthRouter = require('./routes/health');
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+
+app.use(cors());
+app.use(express.json());
+
+// API routes
+app.use('/api', healthRouter);
+
+// Serve static client files in production
+const clientDist = path.join(__dirname, '../../client/dist');
+app.use(express.static(clientDist));
+
+// SPA fallback — send index.html for any unmatched route
+app.get('*', (req, res) => {
+ res.sendFile(path.join(clientDist, 'index.html'));
+});
+
+app.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+});
diff --git a/server/src/routes/health.js b/server/src/routes/health.js
new file mode 100644
index 0000000..1001491
--- /dev/null
+++ b/server/src/routes/health.js
@@ -0,0 +1,8 @@
+const express = require('express');
+const router = express.Router();
+
+router.get('/health', (req, res) => {
+ res.status(200).json({ status: 'ok' });
+});
+
+module.exports = router;