Spring Boot Backend

The nthtime backend runs on Spring Boot 3.5 with Java 25 and PostgreSQL 16. The backend code lives in services/api/ and handles pack/challenge storage, user attempts, settings persistence, and GitHub OAuth authentication.

All browser traffic reaches Spring Boot through Next.js API route proxies -- the browser never communicates directly with Spring Boot.

Schema

The database schema is managed by Flyway SQL migrations in services/api/src/main/resources/db/migration/:

  • V1 -- 6 tables: app_users, auth_accounts, packs, challenges, attempts, user_settings
  • V2 -- PostgreSQL tsvector search index on challenge titles
  • V3 -- Spring Session JDBC tables for session storage

JPA Entities

EntityTableKey Columns
AppUserapp_usersid, name, email, image, github_id
AuthAccountauth_accountsid, user_id (FK), provider, provider_account_id
Packpacksid, slug, name, description, language, framework, version, author, tags
Challengechallengesid, pack_id (FK), slug, title, prompt, difficulty, order, assertions, reference_solution
Attemptattemptsid, user_id (FK), challenge_id (FK), passed, assertion_results, hints_used, time_seconds
UserSettingsuser_settingsid, user_id (FK), feedback, keybindings, dark_mode, formatter

JSONB columns (assertions, reference_solution, formatter, assertion_results) use hypersistence-utils JsonType. PostgreSQL text arrays (tags, hints) use StringArrayType.

REST Controllers

11 REST controllers map to /api/* endpoints:

ControllerBase PathDescription
PackController/api/packsList packs, get pack by slug
ChallengeController/api/challengesGet challenge by ID, list by pack
AttemptController/api/attemptsCreate and list attempts
SettingsController/api/settingsGet/update user settings
AuthController/api/authOAuth2 login/logout endpoints
SessionController/api/auth/sessionSession status check
SearchController/api/searchFull-text search (tsvector)
AuthorPackController/api/author/packsPack authoring CRUD
AuthorChallengeController/api/author/challengesChallenge authoring CRUD
AdminController/api/adminSeed/sync operations
HealthController/api/healthHealth check
CliController/api/cliCLI-specific pack/challenge endpoints

Response DTOs use @JsonProperty("_id") to match frontend expectations.

Services

9 service classes contain all business logic:

ServiceResponsibilities
PackServicePack listing, filtering, slug lookup, challenge counts
ChallengeServiceChallenge retrieval, per-user attempt status
AttemptServiceAttempt creation with rate limiting, attempt history
SettingsServiceSettings get/update with defaults, rate limiting
SearchServicePostgreSQL tsvector full-text search
AuthorPackServicePack CRUD for authors, slug validation
AuthorChallengeServiceChallenge CRUD, reordering
AdminServicePack seeding and sync (delete stale)
UserServiceUser resolution from OAuth2 principal

Rate Limiting

Rate limiting uses Bucket4j with in-memory token buckets:

EndpointLimit
POST /api/attempts10 per minute per user
PUT /api/settings20 per minute per user
Author write operations30 per minute per user

GitHub OAuth

Authentication uses Spring Security OAuth2 Client with Spring Session JDBC.

Flow

  1. Browser navigates to GET /api/auth/signin (Next.js route)
  2. Next.js proxies to Spring Boot's OAuth2 authorization endpoint
  3. Spring Boot redirects to GitHub with redirect_uri pointing back to Next.js
  4. GitHub callback reaches GET /api/auth/callback/github (Next.js) and is forwarded to Spring Boot
  5. Spring Boot creates/resolves the user, sets JSESSIONID cookie via Spring Session
  6. Next.js forwards Set-Cookie to the browser

Session Management

Sessions are stored in PostgreSQL via Spring Session JDBC (tables created by V3 migration). The JSESSIONID cookie is forwarded by the Next.js proxy layer on every API request.

The useAuthSession() hook (TanStack Query) fetches /api/v1/auth/session to check auth status. Components use this instead of any provider-based auth context.

Environment Variables

Spring Boot

VariableDescription
DB_HOSTPostgreSQL host
DB_PORTPostgreSQL port
DB_NAMEDatabase name
DB_USERDatabase user
DB_PASSWORDDatabase password
GITHUB_CLIENT_IDGitHub OAuth app client ID
GITHUB_CLIENT_SECRETGitHub OAuth app client secret
ADMIN_SECRETSecret for seed/sync operations
FRONTEND_URLPublic URL for OAuth redirects

Next.js

VariableDescription
SPRING_BOOT_URLInternal URL to Spring Boot (default: http://api:8080)
FRONTEND_URLPublic URL (default: http://localhost:3000)

Database Setup

Local development

Use Docker Compose to start PostgreSQL and Spring Boot together:

bash
docker compose up

This starts PostgreSQL 16 (internal, port 5432), Spring Boot API (internal, port 8080), and Next.js (public, port 3000).

Manual setup

  1. Start a PostgreSQL 16 instance
  2. Create a database (e.g., nthtime)
  3. Set environment variables and run Spring Boot:
bash
cd services/api
DB_HOST=localhost DB_PORT=5432 DB_NAME=nthtime DB_USER=postgres DB_PASSWORD=postgres ./gradlew bootRun

Flyway runs migrations automatically on startup.

Testing

Testcontainers

Spring Boot tests use Testcontainers -- no local PostgreSQL instance required. Each test run spins up an ephemeral container, applies Flyway migrations, and tears it down automatically.

bash
cd services/api && ./gradlew test