Full-stack TypeScript app with cookie-based authentication.
- Frontend: React + Vite + React Router (
front-end) - Backend: Express + PostgreSQL + JWT in HTTP-only cookie (
back-end) - Database: PostgreSQL 17 (Docker service)
- Register, login, logout, and session restore (
/auth/me) - First-login flow that requires monthly budget setup before dashboard access
- Protected and public route gates on the frontend
- Automatic SQL migrations on backend startup (
back-end/src/db/migrations) - Health endpoint for backend + database connectivity checks (
/health)
- Copy environment variables:
cp .env.example .env-
Make sure
FRONTEND_ORIGINin.envmatches your frontend URL (default frontend URL ishttp://localhost:5173). -
Start all services:
docker compose up --build- Frontend:
http://localhost:5173 - Backend:
http://localhost:3000 - Backend health endpoint:
http://localhost:3000/health - Postgres: runs inside Docker network as
pmh-db:5432
Note: Postgres host port is currently not exposed in docker-compose.yml. If you need direct local access, uncomment the db.ports block.
Use the deploy compose file to build frontend assets and serve them from nginx.
docker compose -f docker-compose.deploy.yml up --build -dDeploy endpoints:
- App:
http://<host> - API health via nginx proxy:
http://<host>/api/health
In deploy mode, nginx serves static files and proxies /api/* to the backend service.
Set DEPLOY_FRONTEND_ORIGIN in .env to your public app URL (for cookie/CORS correctness).
When using the deploy nginx setup, prepend /api (example: POST /api/auth/login).
POST /auth/register- body:
{ "name": "Alex Rivera", "email": "user@example.com", "password": "password123" } - validations: name required (2-80 chars), valid email, password min 8 chars
- success:
201+{ user }and auth cookie set
- body:
POST /auth/login- body:
{ "email": "user@example.com", "password": "password123" } - success:
200+{ user }and auth cookie set
- body:
GET /auth/me- requires auth cookie
- success:
200+{ user }
POST /auth/logout- clears auth cookie
GET /budget/me- requires auth cookie
- returns current month budget
- success:
200+{ "budgetAmountCad": number | null }
POST /budget/me- requires auth cookie
- body:
{ "budgetAmountCad": 3000 } - upserts budget for current month (
one budget per month) - validations: integer, greater than 0
- success:
200+{ "budgetAmountCad": number }
GET /budget/history- requires auth cookie
- success:
200+{ "history": [{ "monthStart": "YYYY-MM-01", "budgetAmountCad": number }] }
Set these in .env (see .env.example):
POSTGRES_HOST(defaultpmh-dbin Docker)POSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DBPOSTGRES_PORT(default5432)BACKEND_PORT(default3000)FRONTEND_PORT(default5173)DEPLOY_PORT(default80, used bydocker-compose.deploy.yml)DEPLOY_FRONTEND_ORIGIN(public app origin for deploy CORS/cookies)FRONTEND_ORIGIN(must match the frontend URL used in browser)VITE_API_URL(frontend API base URL; use/apiin deploy mode)JWT_SECRETJWT_EXPIRES_IN(default1d)COOKIE_SECURE(truein production HTTPS)COOKIE_MAX_AGE_MS(default86400000)
./front-endis mounted into the frontend container./back-endis mounted into the backend container- Frontend runs
vitein dev mode - Backend runs
tsx watchin dev mode
Changes on local files are reflected inside containers.
Frontend (front-end/package.json):
npm run devstarts Vite dev servernpm run buildbuilds production assetsnpm run previewpreviews production buildnpm run typecheckruns TypeScript type checking
Backend (back-end/package.json):
npm run devstarts backend withtsx watch src/index.tsnpm run startstarts backend withtsx src/index.tsnpm run typecheckruns TypeScript type checking