Bilhälsning.se — license plate letter service
Find a file
Joakim Mörling 93ece8128a Use bilhej.se domain for dev test user email.
Aligns seeded and test login addresses with production branding while
keeping admin@bilhalsning.se for local docker admin seed only.

- Change test@bilhalsning.se to test@bilhej.se in dev migration and all tests
2026-05-21 15:14:11 +02:00
.forgejo/workflows Separate dev database seeds from production and bootstrap prod admin. 2026-05-21 15:14:03 +02:00
backend Use bilhej.se domain for dev test user email. 2026-05-21 15:14:11 +02:00
docker fix: correct COPY paths in backend.prod.Dockerfile 2026-05-20 11:48:29 +02:00
frontend Use bilhej.se domain for dev test user email. 2026-05-21 15:14:11 +02:00
gradle/wrapper refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00
.dockerignore fix: E2E pipeline — vite preview instead of nginx, ts build fixes 2026-05-19 18:53:52 +02:00
.env.example Separate dev database seeds from production and bootstrap prod admin. 2026-05-21 15:14:03 +02:00
.gitignore docs: update README for Gradle at root, add convenience task docs 2026-05-01 18:44:05 +02:00
AGENTS.md docs: add coverage thresholds, ./gradlew coverage, and LSP warning discipline 2026-05-15 12:16:16 +02:00
build.gradle chore: add root gradle coverage and frontendCoverage tasks 2026-05-15 12:16:04 +02:00
CODING_GUIDELINES.md docs: add coverage thresholds, ./gradlew coverage, and LSP warning discipline 2026-05-15 12:16:16 +02:00
docker-compose.ci.yml chore: add Docker CI compose, Gradle E2E task, and .dockerignore 2026-05-13 19:17:55 +02:00
docker-compose.e2e.yml fix: use tmpfs for postgres data in E2E compose to prevent Flyway checksum mismatches 2026-05-19 19:57:58 +02:00
docker-compose.prod.yml Separate dev database seeds from production and bootstrap prod admin. 2026-05-21 15:14:03 +02:00
docker-compose.yml feat: replace Stripe mock with manual Swish payment flow 2026-05-19 19:23:37 +02:00
gradlew refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00
README.md docs: add production deployment guide to README 2026-05-20 11:28:38 +02:00
REQUIREMENTS.md docs: add TDD policy, update Spring Boot 4 references, configure OpenCode tools 2026-05-13 19:18:43 +02:00
settings.gradle refactor: move Gradle wrapper to repo root, add convenience tasks 2026-05-01 18:40:18 +02:00

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 via docker-compose.yml Docker Compose dev
prod PostgreSQL (production config) 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

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 (/apibackend: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):

  1. Add Forgejo Actions Secrets

    Go to Forgejo → Repository Settings → Actions → Secrets and add:

    Secret Description
    POSTGRES_DB Database name (e.g., bilhej)
    POSTGRES_USER Database user
    POSTGRES_PASSWORD Strong database password
    JWT_SECRET openssl rand -hex 32
    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 phone number for payment instructions
  2. Point DNS

    Set bilhej.se (and www.bilhej.se) A record to the server's public IP.

  3. 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
    
  4. 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

  1. Go to Actions → Deploy to Production in Forgejo.
  2. Click Run workflow.
  3. Enter a version tag (e.g., v0.1.0).
  4. 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
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