A 2D, tick-based space game in the browser — closest comparison is a web-based relative of Elite. Each player controls a mothership on a 100×100 grid with planets and stars. The world ticks in the background, ships move toward their destinations, and the player logs in now and then to make decisions.
Detailed domain model: see DOMAIN.md.
- Backend: Spring Boot 4 (Java 25), Spring MVC, Spring Security, Spring Data JPA
- Database: PostgreSQL 16 + Flyway migrations
- Frontend: React + Vite (SPA) with PixiJS for the 2D map (scaffolded in issue #4)
- Tests: JUnit 5 + Testcontainers (real Postgres, no mocks)
- CI: GitHub Actions
Requirements: JDK 25, Docker (for Postgres), Git.
# 1. Clone and enter
git clone <repo-url>
cd spring-boot-space-game
# 2. Create your local .env (required — compose.yaml has no defaults)
cp .env.example .env
# Open .env and set POSTGRES_PASSWORD to a real value, e.g.
# openssl rand -base64 24
# 3. Start the backend — Spring detects compose.yaml and starts Postgres automatically
./mvnw spring-boot:run
# 4. Run tests (uses Testcontainers, ignores .env / compose.yaml)
./mvnw verify -Dskip.frontend.build=true # skip the SPA build for a faster test loopThe app listens on http://localhost:8080.
If you prefer to start Postgres separately:
docker compose up -d.env is required — compose.yaml deliberately has no default
credentials, so the backend won't start until you create one.
cp .env.example .envThen edit .env and set at minimum:
POSTGRES_DB— any name (e.g.spacegame)POSTGRES_USER— any name (e.g.spacegame)POSTGRES_PASSWORD— must be a real value, generated locally. Don't reuse a value from chat / docs / another machine. Example:openssl rand -base64 24.
Other notes:
.envis gitignored — never commit it.- The dev Postgres binds to
127.0.0.1:5432, not0.0.0.0, so the container isn't reachable from your LAN. - Spring loads
.envviaspring.config.importinapplication.properties. Format is regular properties (key=value). - For future API keys: add the line to
.env.example(without the value) and to your local.env(with the value).
Never share secrets via git, issues, PRs or chat history. Use Signal, a password manager, or say it verbally. If a secret leaks: rotate it immediately, don't just add a commit that "removes it."
If a POSTGRES_PASSWORD ever ends up somewhere it shouldn't (git, chat,
screenshot), rotate it:
# Drop the local database volume — it stores the user with the old password,
# and Postgres can't re-key it after first init.
docker compose down -v
# Set a new password in .env, then start fresh:
./mvnw spring-boot:runFlyway re-runs all migrations on the empty volume — local data is lost, which is fine for dev.
The repo ships a multi-stage Dockerfile and a docker-compose.prod.yaml so a single command brings up Postgres + the app on any host with Docker installed.
# 1. Make sure .env exists with POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD.
cp .env.example .env # then edit and set real values
# 2. Build the image and start the stack
docker compose -f docker-compose.prod.yaml up --build -d
# App on http://localhost:8080 once Postgres has passed its healthcheck.What happens inside the build:
- The
Dockerfile's builder stage runs./mvnw package, which triggersfrontend-maven-pluginto install pnpm + Node intotarget/, runpnpm install --frozen-lockfile, runpnpm build, and copyfrontend/dist/into the jar'sBOOT-INF/classes/static/. The single jar contains both API and SPA. - The runtime stage is a slim Temurin JRE image, runs as a non-root
springuser, and setsSPRING_DOCKER_COMPOSE_ENABLED=false(the dev autodetect is dev-only). docker-compose.prod.yamlinjectsSPRING_DATASOURCE_*env vars so the app talks to the Postgres service over the compose network.
./mvnw clean package
# target/spring-boot-space-game-0.0.1-SNAPSHOT.jar contains everything
java -jar target/spring-boot-space-game-*.jarThis works for any host with a JDK 25 runtime — Docker isn't required for the jar, only for the bundled-Postgres convenience.
CI's Java job runs ./mvnw verify against a workspace that has no need for the SPA bundle (the separate frontend job covers lint + build). Pass -Dskip.frontend.build=true to skip the pnpm install + Vite build during Maven:
./mvnw verify -Dskip.frontend.build=trueThe flag is off by default so a plain ./mvnw package still produces a deployable jar.
We run a strict PR flow — no direct pushes to main.
- Find or create an issue describing what's to be done.
- Create a branch:
feature/<short-description>orfix/<short-description>. - Commit small, focused changes.
- Open a pull request against
main. Link the issue (Closes #N). - The other person reviews and approves.
- CI must be green before merge.
- Merge via "Squash and merge".
More detail on conventions in CLAUDE.md.
.
├── src/main/java/org/example/springbootspacegame/ # Backend code, packaged by domain
├── src/main/resources/
│ ├── application.properties # Configuration
│ ├── db/migration/ # Flyway SQL migrations (V1__...)
│ └── static/ # SPA bundle (populated at package time)
├── src/test/java/ # Tests
├── frontend/ # React + Vite SPA, PixiJS for the 2D map
├── compose.yaml # Dev Postgres (autostarted by Spring)
├── docker-compose.prod.yaml # Prod stack: app + Postgres
├── Dockerfile # Multi-stage build (frontend + backend → jar)
├── pom.xml
└── .github/workflows/ci.yml # CI
The frontend uses PixiJS in a <canvas> for the 2D map; plain DOM React for menus, dashboards and forms.