Local development
Status: implemented local PostgreSQL and backend workflow. Updated: 29 October 2026.
Prerequisites
- JDK 25 for the JVM build.
- Docker with Compose for the local PostgreSQL stack.
- GraalVM 25 with
native-imageand the platform C toolchain only when building the native executable. - A Google OAuth client ID whose ID tokens target this backend.
- A Base64 HMAC secret containing at least 32 random bytes.
Environment file
Create apps/product-backend/local.env. It is ignored by Git and excluded from the Docker build.
POSTGRES_DB=product_backend
POSTGRES_USER=postgres
POSTGRES_PASSWORD=<local database password>
JWT_SECRET=<Base64 secret, at least 32 decoded bytes>
GOOGLE_CLIENT_ID=<Google OAuth client ID>
Generate a suitable secret with:
openssl rand -base64 32
Use a real Google client ID for sign-in testing. The Compose stack substitutes local-development.invalid only when GOOGLE_CLIENT_ID is absent, which allows startup but not real login.
| Variable | Required | Default | Used by |
|---|---|---|---|
POSTGRES_DB |
No | product_backend |
Compose database and JDBC URL. |
POSTGRES_USER |
No | postgres |
Documented local role; Compose uses postgres. |
POSTGRES_PASSWORD |
Yes | — | Compose database and backend. |
JWT_SECRET |
Yes | — | Backend HMAC signing key. |
GOOGLE_CLIENT_ID |
Yes for login | local-development.invalid in Compose |
Google token audience. |
JWT_ACTIVE_KID |
No | primary |
Signing key ID. |
JWT_ACCESS_DURATION |
No | 1 |
Access-token duration value. |
JWT_ACCESS_DURATION_UNIT |
No | HOURS |
Access unit: SECONDS, MINUTES, HOURS or DAYS. |
JWT_REFRESH_DURATION |
No | 30 |
Refresh-token/session duration value. |
JWT_REFRESH_DURATION_UNIT |
No | DAYS |
Refresh unit; resulting lifetime must exceed access lifetime. |
PRODUCT_POSTGRES_PORT |
No | 5432 |
Loopback database port. |
PRODUCT_BACKEND_PORT |
No | 8080 |
Loopback backend port. |
PRODUCT_DB_POOL_SIZE |
No | 5 |
HikariCP maximum pool size. |
PRODUCT_DB_MIN_IDLE |
No | 1 |
HikariCP minimum idle connections. |
PORT |
No | 8080 |
Backend HTTP port when running outside Compose. |
Outside the local profile, supply SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, and SPRING_DATASOURCE_PASSWORD directly. For hosted Supabase, use a reachable direct or session-pooler JDBC endpoint with certificate-verified TLS.
Apply the schema manually
Flyway is a test-only dependency. The application does not create or migrate the schema at startup, and the Compose image does not run Flyway.
After PostgreSQL is healthy, apply the baseline once:
cd apps/product-backend
docker compose --env-file local.env up -d --wait postgres
docker compose --env-file local.env exec -T postgres \
psql -U postgres -d product_backend -v ON_ERROR_STOP=1 \
< src/main/resources/database/migrations/V1__product_schema.sql
Change -d product_backend if POSTGRES_DB uses another name. The Compose image creates the postgres role, so keep POSTGRES_USER=postgres for the local stack. Apply later migration files in filename order. Do not edit an applied migration; add a forward migration instead.
The Compose database mounts postgres-bootstrap.sql once during initial database setup to grant the local role permission to create product tables in the public schema. That grant does not create the product tables.
Run with Compose
Build and start both containers:
cd apps/product-backend
docker compose --env-file local.env up -d --build
curl http://127.0.0.1:8080/hello
docker compose --env-file local.env down
The backend waits for PostgreSQL readiness, not for the schema. Apply the schema before starting the backend, or restart the backend after applying it.
Named volumes preserve database data and Supabase database configuration when containers stop. down without -v keeps the data.
Run on the host
Start PostgreSQL, apply the schema, then run Spring on the host:
cd apps/product-backend
docker compose --env-file local.env up -d --wait postgres
./mvnw spring-boot:run -Dspring-boot.run.profiles=local
The local profile imports optional:file:./local.env[.properties] from the backend directory and overrides the JDBC URL with jdbc:postgresql://127.0.0.1:${PRODUCT_POSTGRES_PORT:5432}/${POSTGRES_DB:product_backend}. Run the command from apps/product-backend.
Commands
Run these from the repository root unless noted. The pnpm scripts wrap the Maven commands.
| Command | Action |
|---|---|
pnpm build:product-backend |
Package the JVM JAR, skipping tests. |
pnpm check:product-backend |
Run Maven verification, including tests and packaging. |
pnpm test:product-backend |
Run JVM tests. |
pnpm start:product-backend |
Start the JVM app through Maven with externally supplied configuration. |
pnpm start:product-backend:local |
Start PostgreSQL, then run Spring on the host using local.env. |
pnpm build:product-backend:native |
Build the GraalVM executable. |
pnpm start:product-backend:native |
Start the already-built native executable. |
pnpm dev:product-backend:local |
Build and start backend and PostgreSQL containers. |
pnpm stop:product-backend:local |
Stop containers, preserving PostgreSQL volumes. |
From apps/product-backend:
./mvnw verify
./mvnw spring-boot:run -Dspring-boot.run.profiles=local
curl http://127.0.0.1:8080/hello
verify runs the entity JSON tests, Google decoder tests, token service tests, and HTTP integration tests, then packages target/product-backend.jar. The HTTP tests start a disposable PostgreSQL container through Testcontainers and apply the test schema through Flyway, so Docker must be running. The checked-in Maven Wrapper selects Maven 3.9.16. Builds target Java 25 and use Spring Boot 4.1.2 with Kotlin 2.4.21.
IDE setup
Open the repository root in IntelliJ IDEA. The root atstrology-designs module owns the pnpm workspace; the separate product-backend Java module owns this directory, its Kotlin source and test roots, resources, and Maven dependencies. Keep the backend directory visible in the root module; only target is excluded.
The root IntelliJ project links this app’s pom.xml and selects the graalvm-ce-25 SDK. Ensure that SDK points to an installed JDK 25 under File → Project Structure → Project. Keep the Node interpreter and pnpm package manager configured independently under Settings → Languages & Frameworks → JavaScript Runtime.
If module files change while the project is open, reopen the root project, then use Reload All Maven Projects. Confirm that Project Structure → Modules lists both atstrology-designs and product-backend, and that the backend has a Kotlin facet and Maven libraries. If the POM is absent from the Maven tool window, right-click pom.xml and choose Add as Maven Project. Kotlin completion requires the imported module and resolved dependencies; linking the POM alone is insufficient.
Verification and troubleshooting
GET /helloproves JVM and HTTP startup without database or Google access.GET /v3/api-docsproves Springdoc and controller annotations are reachable.- A real login requires a Google ID token whose audience equals
GOOGLE_CLIENT_ID. GOOGLE_TOKEN_REQUIREDmeans the request body was blank.INVALID_GOOGLE_TOKENmeans Google signature, issuer, audience, or time validation failed.UNVERIFIED_GOOGLE_IDENTITYmeans the token lacked a subject, email, verified email, or expiry.- A missing
login_identityorlogin_sessiontable means the schema was not applied. - Startup failure about a missing or weak
JWT_SECRETmeans the key is absent or does not decode to at least 32 bytes. AUTHENTICATION_REQUIRED,INVALID_ACCESS_TOKEN, andACCESS_DENIEDare authentication failures, not missing routes.
Related files
| Path | Owns |
|---|---|
compose.yaml |
Local PostgreSQL and backend containers. |
Dockerfile |
JVM image build and runtime. |
local.env |
Ignored local credentials and configuration. |
postgres-bootstrap.sql |
One-time local schema grant for the PostgreSQL role. |
src/main/resources/application.yaml |
Shared datasource, server, Google, and token configuration. |
src/main/resources/application-local.yaml |
Local profile import and host JDBC defaults. |
src/test/resources/application-test.yaml |
Test-only Flyway locations. |
src/test/kotlin/com/astrology/product/integration/LoginHttpIntegrationTests.kt |
Real HTTP, security, and JDBC login tests with Testcontainers PostgreSQL. |
src/test/kotlin/com/astrology/product/account/GoogleDecoderTests.kt |
Google issuer and audience validation tests. |
src/test/kotlin/com/astrology/product/account/TokenDecoderTests.kt |
Token signing, key selection, role, expiry, and issuer tests. |
pom.xml |
Maven, Kotlin, Spring Boot, and native build configuration. |