Operators need IntelliJ-style GUI access to Docker Postgres and clear steps for manual prod cleanup without wiping volumes. - Add Database access section with IntelliJ, DBeaver, and SSH tunnel steps - Document dev-only accounts, manual SQL cleanup, and hashPassword task - Note Flyway dev-migration split and admin bootstrap in AGENTS.md |
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| docker | ||
| frontend | ||
| gradle/wrapper | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| build.gradle | ||
| CODING_GUIDELINES.md | ||
| docker-compose.ci.yml | ||
| docker-compose.e2e.yml | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| gradlew | ||
| README.md | ||
| REQUIREMENTS.md | ||
| settings.gradle | ||
BilHej / Bilhälsning.se
Send a physical letter to a Swedish car owner — just by knowing their license plate.
The user enters a registration number, composes a letter (from a template or free text), pays, and BilHej handles the rest: owner address lookup via Transportstyrelsen, printing and mailing via PostNord. The sender never sees the recipient's name or address.
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | Vue.js 3 (Composition API), Vite, Pinia |
| Backend | Java 21, Spring Boot 4, Gradle |
| Database | PostgreSQL 16 |
| Auth | Spring Security + JWT |
| Payments | Stripe (cards + Swish) |
| Deployment | Docker, Docker Compose |
Prerequisites
- Docker & Docker Compose
- Java 21 (for local IDE development)
- Node.js 20+ (for local frontend dev)
- A Stripe account (test mode for development)
Quick Start
git clone <repo-url> bilhej
cd bilhej
cp .env.example .env # fill in your keys
docker compose up -d # or: ./gradlew up
The app will be available at:
- Frontend:
http://localhost:3000 - Backend API:
http://localhost:8080 - PostgreSQL:
localhost:5432
Architecture inside Docker Compose
Browser Docker Compose network
─────── ─────────────────────
│ ┌──────────────────┐
│ http://localhost:3000 │ frontend (Vite) │
├────────────────────────→│ :3000 │
│ │ proxy: /api → │
│ GET /api/orders │ backend:8080 │
│ └────────┬─────────┘
│ │
│ ┌────────▼─────────┐
│ │ backend (Spring) │
│ │ :8080 │
│ │ profile: docker │
│ └────────┬─────────┘
│ │
│ ┌────────▼─────────┐
│ │ postgres (16) │
│ │ :5432 │
│ └──────────────────┘
Vite proxy: The Vite dev server proxies /api/* requests to the backend container.
No CORS configuration needed in development — the browser never calls the backend directly.
Spring profiles:
| Profile | Datasource | Use |
|---|---|---|
| default | H2 in-memory | Local IDE dev (./gradlew :backend:bootRun) |
docker |
PostgreSQL + dev Flyway seeds | Docker Compose dev / CI |
docker,prod |
PostgreSQL, schema only, admin bootstrap | Deploy (docker-compose.prod.yml) |
Environment Variables
Copy .env.example to .env and fill in:
| Variable | Description |
|---|---|
POSTGRES_DB |
Database name (default: bilhej) |
POSTGRES_USER |
Database user |
POSTGRES_PASSWORD |
Database password |
JWT_SECRET |
Secret key for JWT signing |
STRIPE_SECRET_KEY |
Stripe secret key |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret |
STRIPE_PRICE_ID |
Stripe price ID for single letter |
SWISH_NUMBER |
Swish number for payment instructions |
ADMIN_EMAIL |
Production admin login (e.g. admin@bilhej.se) |
ADMIN_PASSWORD |
Strong production admin password (not test1234) |
Dev-only accounts (from db/dev-migration, not used in production):
| Password | Role | |
|---|---|---|
test@bilhej.se |
test1234 |
user (e2e / local) |
admin@bilhalsning.se |
test1234 |
admin (local docker only) |
Database access
PostgreSQL runs in Docker. Any GUI client works (IntelliJ IDEA, DBeaver, TablePlus, pgAdmin) — same idea as a normal remote database.
Local dev (docker compose up)
| Setting | Value |
|---|---|
| Host | localhost |
| Port | 5432 |
| Database | from .env → POSTGRES_DB (default bilhej) |
| User / password | POSTGRES_USER / POSTGRES_PASSWORD |
IntelliJ IDEA: Database tool window → + → Data Source → PostgreSQL → fill in above →
Test Connection → OK.
CLI:
docker exec -it bilhej-postgres psql -U bilhej -d bilhej
Production (server)
Postgres is bound to localhost only on the server (127.0.0.1:5433) so it is not
exposed to the internet. Use an SSH tunnel from your laptop, then point IntelliJ (or
DBeaver) at localhost.
-
On the server, recreate the stack once so the port mapping is active (after deploy).
-
From your laptop:
ssh -N -L 5433:127.0.0.1:5433 you@srvr.nu
- In IntelliJ / DBeaver:
| Setting | Value |
|---|---|
| Host | localhost |
| Port | 5433 |
| Database | prod POSTGRES_DB |
| User / password | prod secrets |
CLI on the server (no GUI):
docker exec -it bilhej-postgres-prod psql -U bilhej -d bilhej
Manual prod cleanup (keep data, remove dev seeds)
DELETE FROM orders
WHERE user_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';
DELETE FROM users
WHERE email IN ('test@bilhalsning.se', 'test@bilhej.se', 'admin@bilhalsning.se');
Then deploy with ADMIN_EMAIL / ADMIN_PASSWORD set — the app creates the production
admin on startup. No need to insert a password hash by hand.
To generate a bcrypt hash manually (optional):
./gradlew hashPassword -Ppassword='your-strong-password'
Project Structure
bilhej/
├── frontend/ # Vue.js 3 SPA
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── composables/ # Shared composition functions
│ │ ├── layouts/ # Page layouts
│ │ ├── pages/ # Route-level page components
│ │ ├── router/ # Vue Router config
│ │ ├── stores/ # Pinia stores
│ │ ├── api/ # API client and endpoints
│ │ ├── assets/ # Static assets, CSS
│ │ ├── App.vue
│ │ └── main.ts
│ ├── index.html
│ ├── vite.config.ts
│ └── package.json
├── backend/ # Spring Boot 4
│ ├── src/main/java/se/bilhalsning/
│ │ ├── BilHejApplication.java
│ │ ├── config/ # Security, CORS, Stripe config
│ │ ├── controller/ # REST controllers
│ │ ├── dto/ # Data transfer objects
│ │ ├── entity/ # JPA entities
│ │ ├── repository/ # Spring Data repositories
│ │ ├── service/ # Business logic
│ │ └── security/ # JWT filter, user details
│ └── src/main/resources/
│ ├── application.yml # default profile (H2)
│ ├── application-docker.yml # docker profile (PostgreSQL)
│ └── db/migration/ # Flyway migrations
├── docker-compose.yml # dev: postgres + backend (bootRun) + frontend (Vite HMR)
├── docker-compose.prod.yml # prod: multi-stage builds, no source mounts, restart: unless-stopped
├── docker/
│ ├── backend.Dockerfile # dev: JDK + gradle :backend:bootRun
│ ├── backend.prod.Dockerfile # prod: multi-stage (Gradle → JRE Alpine, non-root)
│ ├── frontend.Dockerfile # dev: Node + vite dev server
│ ├── frontend.prod.Dockerfile # prod: multi-stage (Node → nginx)
│ ├── nginx.conf # prod: SPA fallback + /api proxy
│ └── entrypoint.sh # prod: self-signed cert generation
├── gradlew # Gradle wrapper (run from repo root)
├── gradle/
│ └── wrapper/
├── settings.gradle # rootProject.name + include 'backend'
├── build.gradle # convenience tasks: check, up, down, reset
├── .env.example
├── README.md
├── REQUIREMENTS.md
└── CODING_GUIDELINES.md
Development vs Production
| Aspect | docker compose up -d |
docker compose -f docker-compose.prod.yml up -d |
|---|---|---|
| Backend | ./gradlew :backend:bootRun (compiles on change) |
Multi-stage build → java -jar app.jar |
| Backend image | eclipse-temurin:21-jdk (~400 MB) |
eclipse-temurin:21-jre-alpine (~200 MB) |
| Backend user | root | bilhej (non-root) |
| Frontend | Vite dev server (HMR, --host 0.0.0.0) |
nginx serving static dist/ |
| API proxy | Vite built-in proxy (/api → backend:8080) |
nginx proxy_pass |
| SSL | None | Self-signed cert auto-generated on first start (.certs/ volume) |
| Source mounts | Yes (live edit) | No (files baked into image) |
| Restart policy | Manual | unless-stopped |
| Purpose | Write code, instant feedback | Verify production build |
Production Deployment
Deployments are fully automated via Forgejo Actions. The pipeline builds production Docker images and starts them on the server.
One-time Setup
Before the first deploy, complete these steps on the production server (srvr.nu):
-
Add Forgejo Actions Secrets
Go to Forgejo → Repository Settings → Actions → Secrets and add:
Secret Description POSTGRES_DBDatabase name (e.g., bilhej)POSTGRES_USERDatabase user POSTGRES_PASSWORDStrong database password JWT_SECRETopenssl rand -hex 32STRIPE_SECRET_KEYStripe secret key STRIPE_WEBHOOK_SECRETStripe webhook signing secret STRIPE_PRICE_IDStripe price ID for single letter SWISH_NUMBERSwish phone number for payment instructions ADMIN_EMAILProduction admin email (e.g. admin@bilhej.se)ADMIN_PASSWORDStrong unique admin password (password manager) Production does not seed
test@bilhej.seor demo orders. On first start, the backend creates one admin fromADMIN_EMAIL/ADMIN_PASSWORDif no admin exists.If prod already has dev seed users, clean them with SQL (see Database access) instead of wiping the volume. Then redeploy with the new secrets so bootstrap can create
ADMIN_EMAIL. -
Point DNS
Set
bilhej.se(andwww.bilhej.se) A record to the server's public IP. -
Obtain SSL Certificate
Run certbot in the nginx container:
docker exec certbot certbot certonly \ --webroot -w /var/www/certbot \ -d bilhej.se -d www.bilhej.se -
Add Nginx Config
Copy the Bilhej server block into the nginx container:
docker cp docker/bilhej.nginx.conf nginx:/etc/nginx/conf.d/bilhej.conf docker exec nginx nginx -s reload
Deploy
- Go to Actions → Deploy to Production in Forgejo.
- Click Run workflow.
- Enter a version tag (e.g.,
v0.1.0). - Click Run workflow.
What Happens
| Step | Action |
|---|---|
| Tag | Git tag v0.1.0 is created and pushed |
| Build | Production backend JAR and frontend bundle are built |
| Images | Multi-stage Docker images are built locally on the server |
| Start | docker compose -f docker-compose.prod.yml up -d (SPRING_PROFILES_ACTIVE=docker,prod) |
| Verify | Health checks confirm backend API and frontend are responding |
Architecture on Server
User
│
│ https://bilhej.se
▼
┌─────────────────────────────────────┐
│ nginx (srvr.nu) │
│ SSL termination (Let's Encrypt) │
│ proxy_pass → bilhej-frontend-prod │
└─────────────┬───────────────────────┘
│ Docker 'web' network
┌─────────────▼───────────────────────┐
│ bilhej-frontend-prod (nginx) │
│ :80 │
│ /api/* → bilhej-backend-prod:8080 │
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ bilhej-backend-prod (Spring Boot) │
│ :8080 │
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ bilhej-postgres-prod (PostgreSQL) │
│ :5432 │
└─────────────────────────────────────┘
Rollback
To rollback to a previous version:
# On srvr.nu
cd /path/to/bilhej/repo
git fetch --tags
git checkout v0.1.0 # or any previous tag
docker compose -f docker-compose.prod.yml up --build -d
Development
All-in-one (from repo root)
./gradlew check # lint → frontend test → backend test → integration test
./gradlew up # docker compose up -d
./gradlew down # docker compose down
./gradlew reset # docker compose down -v && docker compose up -d (full DB reset)
Frontend (dev server with HMR)
cd frontend
npm install # first time only
npm run dev # :3000 with HMR
Backend (IDE or CLI)
./gradlew :backend:bootRun # :8080, profile: default (H2)
Stripe Webhooks (local testing)
stripe listen --forward-to localhost:8080/api/webhooks/stripe
Database reset
./gradlew reset # wipes DB volume and restarts containers
Related Documents
- REQUIREMENTS.md — Full product requirements and business model
- CODING_GUIDELINES.md — Code conventions and standards