Finance Harbour is a personal budget planning app built with Django REST Framework, PostgreSQL, React, TypeScript, Tailwind CSS, Vite, Yarn, and Docker.
Copy the example environment file:
cp .env.example .envDo not commit .env.
Fill out .env with local development values:
POSTGRES_DB=finance_harbour
POSTGRES_USER=finance_harbour
POSTGRES_PASSWORD=your_password_here
POSTGRES_HOST=database
POSTGRES_PORT=5432
PGADMIN_DEFAULT_EMAIL=admin@financeharbour.ca
PGADMIN_DEFAULT_PASSWORD=admin123
PGADMIN_PORT=5050
BACKEND_PORT=8000
FRONTEND_PORT=5173
DJANGO_SECRET_KEY=your-local-django-secret-here
SIMPLE_JWT_SIGNING_KEY=your-local-jwt-signing-secret-here
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,backend,finance-harbour.test,api.finance-harbour.test
DJANGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://finance-harbour.test:5173
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://finance-harbour.test:5173,http://api.finance-harbour.test:8000
GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id
GOOGLE_OAUTH_CLIENT_SECRET=your-google-oauth-client-secret
GOOGLE_OAUTH_CALLBACK_URL=http://localhost:5173/auth/google/callback
VITE_GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id
VITE_GOOGLE_OAUTH_REDIRECT_URI=http://localhost:5173/auth/google/callbackUse different secret values for each environment:
local != staging != production != GitHub Actions
Add the local development domains once:
sudo sh -c 'printf "\n127.0.0.1 finance-harbour.test api.finance-harbour.test\n" >> /etc/hosts'This lets you visit finance-harbour.test for normal frontend development, while api.finance-harbour.test is reserved for local API endpoints.
For local Google OAuth testing, use:
http://localhost:5173
Google OAuth does not accept .test as an authorized JavaScript origin.
Install the frontend dependencies on the host machine:
cd frontend && corepack enable && yarn installThen go back to the project root:
cd ..Install the backend dependencies for your editor on the host machine:
cd backend && PIPENV_VENV_IN_PROJECT=1 pipenv sync --devThen go back to the project root:
cd ..Your editor should use this backend Python interpreter:
backend/.venv/bin/python
Start the app:
docker compose up -d --buildRun database migrations:
docker compose exec backend python manage.py migrateTo create new migrations:
docker compose exec backend python manage.py makemigrationsBefore opening the frontend, check that the backend is running:
http://localhost:8000/api/health/
That URL should return:
{ "status": "ok" }Then open the frontend:
http://finance-harbour.test:5173
For Google OAuth testing, open the frontend through:
http://localhost:5173
DJANGO_SECRET_KEY is required in .env.
For local development, generate a key with:
docker run --rm python:3.14-slim python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"Then add it to .env:
DJANGO_SECRET_KEY=generated-value-hereDJANGO_SECRET_KEY is used by Django signing and security internals.
SIMPLE_JWT_SIGNING_KEY is required in .env.
For local development, generate a key with:
docker run --rm python:3.14-slim python -c "import secrets; print(secrets.token_urlsafe(64))"Then add it to .env:
SIMPLE_JWT_SIGNING_KEY=generated-value-hereDo not reuse DJANGO_SECRET_KEY for SIMPLE_JWT_SIGNING_KEY.
The keys have separate purposes:
DJANGO_SECRET_KEY = Django signing/security internals
SIMPLE_JWT_SIGNING_KEY = JWT access/refresh token signing
Google OAuth requires backend and frontend environment values.
Backend values:
GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id
GOOGLE_OAUTH_CLIENT_SECRET=your-google-oauth-client-secret
GOOGLE_OAUTH_CALLBACK_URL=http://localhost:5173/auth/google/callbackFrontend values:
VITE_GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id
VITE_GOOGLE_OAUTH_REDIRECT_URI=http://localhost:5173/auth/google/callbackThe Google client ID must match on the backend and frontend:
GOOGLE_OAUTH_CLIENT_ID == VITE_GOOGLE_OAUTH_CLIENT_ID
The Google callback URL must match the frontend redirect URI:
GOOGLE_OAUTH_CALLBACK_URL == VITE_GOOGLE_OAUTH_REDIRECT_URI
The Google client secret is backend-only. Never expose it to the frontend.
For local development, configure the Google OAuth client with:
Authorized JavaScript origins:
http://localhost:5173
Authorized redirect URIs:
http://localhost:5173/auth/google/callback
After changing Google OAuth .env values, recreate the affected containers:
docker compose up -d --force-recreate backend frontendFor local development:
DJANGO_DEBUG=TrueFor staging and production:
DJANGO_DEBUG=FalseWhen DJANGO_DEBUG=False, production security settings are enabled, including secure cookies, HTTPS redirect, and HSTS.
After changing backend .env values, recreate the backend container:
docker compose up -d --force-recreate backendAfter changing frontend .env values, recreate the frontend container:
docker compose up -d --force-recreate frontendRebuild only when Dockerfile or dependency changes happen.
pgAdmin is available through Docker for viewing the local Postgres database.
Required .env values:
PGADMIN_DEFAULT_EMAIL=admin@financeharbour.ca
PGADMIN_DEFAULT_PASSWORD=admin123
PGADMIN_PORT=5050Make sure the containers are running:
docker compose up -dAccess pgAdmin here:
http://localhost:5050
When setting up the connection in pgAdmin, use the following:
Name: Finance Harbour Local
Host name/address: database
Port: 5432
Maintenance database: value of POSTGRES_DB
Username: value of POSTGRES_USER
Password: value of POSTGRES_PASSWORD
The frontend source code is mounted into Docker from the host machine.
That means you edit files locally, and the frontend container sees the changes.
The host machine owns:
frontend/node_modules
frontend/package.json
frontend/yarn.lock
So the editor does not need to attach to the Docker container to see frontend packages.
Normal frontend changes should reload through Vite.
To add a new frontend dependency, run this from the project root:
cd frontend && yarn add package-nameFor dev dependencies:
cd frontend && yarn add -D package-nameThen go back to the project root:
cd ..Restart the frontend container:
docker compose restart frontendThen run the frontend cleanup command:
docker compose exec frontend yarn cleanupThis updates:
frontend/package.json
frontend/yarn.lock
On the host machine.
The backend source code is mounted into Docker from the host machine.
That means you edit files locally, and the backend container sees the changes.
The editor should use the local Pipenv virtual environment:
backend/.venv/bin/python
Docker does not use that .venv folder. Docker installs backend dependencies into the image when the backend image is built.
So the setup is:
Host editor uses backend/.venv
Docker backend uses packages installed during image build
Pipfile and Pipfile.lock keep both sides aligned
Normal backend changes should reload through Django runserver.
To add a new backend dependency, run this from the project root:
cd backend && PIPENV_VENV_IN_PROJECT=1 pipenv install package-nameFor dev dependencies:
cd backend && PIPENV_VENV_IN_PROJECT=1 pipenv install --dev package-nameThen go back to the project root:
cd ..Rebuild the backend container:
docker compose up -d --build backendThen run the backend checks:
docker compose exec backend python manage.py check && docker compose exec backend ruff check . && docker compose exec backend ruff format --check .This updates:
backend/Pipfile
backend/Pipfile.lock
You can also manually edit backend/Pipfile.
If you do that, update the lock file before rebuilding:
cd backend && PIPENV_VENV_IN_PROJECT=1 pipenv lock && pipenv sync --devThen go back to the project root:
cd ..Rebuild the backend container:
docker compose up -d --build backendDo not rebuild from a changed Pipfile without updating Pipfile.lock because the build will fail.
Testing is one of the most important parts of Finance Harbour. It verifies that the app is functioning correctly.
Run all backend tests with coverage:
docker compose exec backend coverage run \
--source=. \
--omit="*/migrations/*,*/tests/*,manage.py,config/*" \
manage.py test
docker compose exec backend coverage report -mRun backend tests without coverage:
docker compose exec backend python manage.py testRun tests for one app:
docker compose exec backend python manage.py test authenticationBecause the backend uses Ruff, format before committing:
docker compose exec backend ruff format .Run the full backend check before pushing:
docker compose exec backend python manage.py check && docker compose exec backend ruff check . && docker compose exec backend ruff format --check .Run the frontend cleanup command before pushing:
docker compose exec frontend yarn cleanupRun this before pushing:
docker compose exec backend ruff format . && \
docker compose exec backend python manage.py check && \
docker compose exec backend ruff check . && \
docker compose exec backend ruff format --check . && \
docker compose exec backend python manage.py test && \
docker compose exec backend coverage run --source=. --omit="*/migrations/*,*/tests/*,manage.py,config/*" manage.py test && \
docker compose exec backend coverage report -m && \
docker compose exec frontend yarn cleanup