Product6 MIN READ124 / 129

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-image and 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 /hello proves JVM and HTTP startup without database or Google access.
  • GET /v3/api-docs proves Springdoc and controller annotations are reachable.
  • A real login requires a Google ID token whose audience equals GOOGLE_CLIENT_ID.
  • GOOGLE_TOKEN_REQUIRED means the request body was blank.
  • INVALID_GOOGLE_TOKEN means Google signature, issuer, audience, or time validation failed.
  • UNVERIFIED_GOOGLE_IDENTITY means the token lacked a subject, email, verified email, or expiry.
  • A missing login_identity or login_session table means the schema was not applied.
  • Startup failure about a missing or weak JWT_SECRET means the key is absent or does not decode to at least 32 bytes.
  • AUTHENTICATION_REQUIRED, INVALID_ACCESS_TOKEN, and ACCESS_DENIED are authentication failures, not missing routes.
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.