Bilhälsning.se — license plate letter service
fix: add preview.allowedHosts and preview.host to vite.config.ts Vite preview server blocks requests from non-localhost hosts by default. In the E2E Docker Compose stack, Playwright accesses the frontend via http://frontend (container hostname). Without allowedHosts, Vite returns "Blocked request. This host is not allowed." and the SPA never mounts, causing all 59 E2E tests to fail with blank pages and missing elements. - Add preview.host: true (bind to 0.0.0.0) - Add preview.allowedHosts: ['frontend', 'localhost'] test: update payment-redirect E2E tests to match current UI The payment page was redesigned to a two-step confirmation flow: "Jag har betalat" → confirmation → "Ja, jag har betalat". The E2E tests still referenced the old single-step "Genomför testbetalning" button and a removed .payment__note CSS class. - Update 'payment button marks order as paid' to click through both steps - Rename 'shows mock payment note' to 'shows Swish payment instructions' and assert on actual UI elements (Swish label + payment button) Result: E2E suite now passes 59/59 tests in the Docker Compose CI stack. |
||
|---|---|---|
| .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 | ||
| opencode.json | ||
| 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 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 (/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 |
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