BackendForge is a production-hardened Spring Boot starter kit for building secure, maintainable REST APIs. It is built on Java 21, Maven, PostgreSQL, Spring Data JPA, Spring Security, Bean Validation, Flyway database migrations, and OpenAPI v3 documentation with Swagger UI.
This foundation includes PostgreSQL persistence, forward-only schema migrations, user registration with BCrypt password hashing, stateless JWT authentication with timing equalization, configurable rate limiting, safe request correlation IDs, non-root Docker containerization, and comprehensive health and readiness probes.
Application code is rooted at com.thecreatorco.backendforge:
common.correlation: CorrelationIdFilter captures or generates safe correlation IDs (X-Correlation-ID), propagates them to SLF4J MDC, attaches them to responses, and sanitizes CRLF/oversize inputs.common.error: Centralized exception handling via GlobalExceptionHandler returning standardized ApiError responses while excluding sensitive details from logs and client responses.config: ProductionConfiguration and ProductionConfigurationProperties providing fail-fast validation of required production database and JWT settings, along with OpenAPI specification setup in OpenApiConfig.ratelimit: In-memory sliding-window limiter (ClientRateLimiter), interceptor (RateLimitInterceptor), and validated configuration properties (RateLimitProperties) keyed by direct remote address.security: Stateless security filter chain (SecurityConfig), HMAC-SHA256 signing and claims validation (JwtService), and request authentication (JwtAuthenticationFilter).user: User JPA entity (UserEntity), Flyway migrations, repository, controller (UserController), password UTF-8 byte boundary validator (PasswordByteLengthValidator), and registration/login service (UserService) with constant-time failure equalization.
- Java: OpenJDK 21 LTS or newer.
- Build Tool: Apache Maven 3.9+ is bundled via the included Maven Wrapper (mvnw / mvnw.cmd). A globally installed Maven distribution is not required.
- Database: PostgreSQL 15 or 16 (local native service or Docker container).
- Container Runtime: Docker Desktop or Docker Engine (required for Docker Compose and Testcontainers integration tests).
java -version
./mvnw -version
docker --version
docker compose versionjava -version
.\mvnw.cmd -version
docker --version
docker compose versionVerifying Java 21: The output of
java -versionmust report version21(e.g.,openjdk version "21.0.x"). If multiple Java versions are installed, ensure yourJAVA_HOMEenvironment variable points to your JDK 21 installation (echo $JAVA_HOMEon Linux/macOS or$env:JAVA_HOMEon Windows PowerShell).
BackendForge provides safe development defaults when running with the default profile. In production (SPRING_PROFILES_ACTIVE=prod), all database and JWT parameters must be explicitly provided.
Security Rule:
JWT_SECREThas no hardcoded fallback in any profile. It must be provided via an environment variable before the application will start.
| Variable | Default (Local / Dev) | Production (prod profile) |
Description |
|---|---|---|---|
SPRING_PROFILES_ACTIVE |
default |
prod |
Active Spring profile (prod activates strict startup validation) |
DB_URL |
jdbc:postgresql://localhost:5432/backendforge |
Required | JDBC connection URL for PostgreSQL |
DB_USERNAME |
backendforge |
Required | Database user |
DB_PASSWORD |
backendforge |
Required | Database password |
SERVER_PORT |
8080 |
8080 (or target port) |
HTTP application port |
JWT_SECRET |
(Required via env) | Required | Base64-encoded secret decoding to |
JWT_EXPIRATION_SECONDS |
900 |
900 |
Access-token lifetime in seconds (15 minutes) |
JWT_ISSUER |
backendforge |
Required | Required iss claim for tokens |
JWT_AUDIENCE |
backendforge-api |
Required | Required aud claim for tokens |
RATE_LIMIT_LOGIN_MAX_REQUESTS |
30 |
30 |
Login attempts allowed per remote address per window |
RATE_LIMIT_LOGIN_WINDOW |
60s |
60s |
Sliding window duration for login limiter |
RATE_LIMIT_REGISTER_MAX_REQUESTS |
30 |
30 |
Registration attempts allowed per remote address per window |
RATE_LIMIT_REGISTER_WINDOW |
60s |
60s |
Sliding window duration for registration limiter |
RATE_LIMIT_MAX_ENTRIES |
10000 |
10000 |
Maximum active client rate-limit windows kept in memory |
POSTGRES_DB |
backendforge |
(Compose only) | PostgreSQL database name used by Docker Compose |
POSTGRES_USER |
backendforge |
(Compose only) | PostgreSQL username used by Docker Compose |
POSTGRES_PASSWORD |
(Must be set in .env) |
(Compose only) | PostgreSQL password used by Docker Compose |
BackendForge signs and verifies JWT access tokens using HMAC-SHA256 (HS256). The implementation in JwtService enforces strict validation rules at startup:
- Format: The string must be valid Base64.
- Key Length: Decoded bytes must be at least 32 bytes (256 bits).
- Startup Exceptions: If
JWT_SECRETis blank, missing, invalid Base64, or shorter than 32 decoded bytes, startup terminates immediately with anIllegalStateException:"JWT_SECRET must be configured with a Base64-encoded secret""JWT_SECRET must be valid Base64""JWT_SECRET must decode to at least 32 bytes"
-
Linux / macOS (OpenSSL):
openssl rand -base64 32
-
Windows PowerShell:
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
-
Safe Example for Local Development Only:
VGhpcyBpcyBhIHNhZmUgZGV2ZWxvcG1lbnQgc2VjcmV0IGZvciBCYWNrZW5kRm9yZ2U=(Never use example keys in production environments).
- Development (
defaultprofile): Allows quick local execution with fallback database credentials (backendforge:backendforgeatlocalhost:5432/backendforge) and default JWT issuer/audience claims. - Production (
SPRING_PROFILES_ACTIVE=prod): ProductionConfiguration activates strict fail-fast validation. IfDB_URL,DB_USERNAME,DB_PASSWORD,JWT_SECRET,JWT_ISSUER, orJWT_AUDIENCEare missing or empty, application startup halts immediately.
Note on
.envfiles: Spring Boot does not automatically load.envfiles when running locally on the host. The.envfile is used by Docker Compose. When running on the host, export environment variables in your shell or IDE run configuration.
If PostgreSQL is installed directly on your machine (listening on the default port 5432):
-
Create the Database and User: Open a terminal and connect as the PostgreSQL administrator:
psql -U postgres
Execute the following SQL statements:
CREATE DATABASE backendforge; CREATE USER backendforge WITH ENCRYPTED PASSWORD 'your_secure_password'; GRANT ALL PRIVILEGES ON DATABASE backendforge TO backendforge; \c backendforge GRANT ALL ON SCHEMA public TO backendforge;
-
Configure Connection Properties: Set
DB_URL=jdbc:postgresql://localhost:5432/backendforge,DB_USERNAME=backendforge, andDB_PASSWORD=your_secure_password.
The provided compose.yaml runs an isolated PostgreSQL 16.4 container:
- Compose Host Port:
5434(mapped to container port5432). - Why Port 5434? Compose maps PostgreSQL to host port
5434to prevent port collisions with any local PostgreSQL instance already running on default port5432or secondary port5433. - Connecting from Host to Compose Database:
If you run the Spring Boot application on your host while using Compose for PostgreSQL, use:
DB_URL=jdbc:postgresql://localhost:5434/backendforge - Connecting Inside Compose Network:
When running both backend and database inside Docker Compose, the backend container connects internally using:
DB_URL=jdbc:postgresql://postgres:5432/backendforge
- Database migrations are located in
src/main/resources/db/migration/and execute automatically at application startup. - Important Prerequisite: Flyway automatically creates application tables (such as
users) and metadata tables (flyway_schema_history), but Flyway cannot create the PostgreSQL database catalog itself. The target database (backendforge) must exist and be reachable before starting the application. - Hibernate is configured with
ddl-auto: validate. It verifies that JPA entity mappings match the database schema created by Flyway without executing automatic schema mutations.
Ensure your local PostgreSQL database is running and the backendforge database exists.
export DB_URL="jdbc:postgresql://localhost:5432/backendforge"
export DB_USERNAME="backendforge"
export DB_PASSWORD="your-local-password"
export JWT_SECRET="VGhpcyBpcyBhIHNhZmUgZGV2ZWxvcG1lbnQgc2VjcmV0IGZvciBCYWNrZW5kRm9yZ2U="
./mvnw spring-boot:run$env:DB_URL = "jdbc:postgresql://localhost:5432/backendforge"
$env:DB_USERNAME = "backendforge"
$env:DB_PASSWORD = "your-local-password"
$env:JWT_SECRET = "VGhpcyBpcyBhIHNhZmUgZGV2ZWxvcG1lbnQgc2VjcmV0IGZvciBCYWNrZW5kRm9yZ2U="
.\mvnw.cmd spring-boot:runThe application will start on http://localhost:8080.
Docker Compose launches PostgreSQL 16.4 and builds the containerized backend using the production profile:
- Copy the example configuration file:
- Linux / macOS:
cp .env.example .env - Windows PowerShell:
Copy-Item .env.example .env
- Linux / macOS:
- Update
.envwith a secure password forPOSTGRES_PASSWORDand generate a fresh Base64 secret forJWT_SECRET. - Start the environment:
docker compose up --build
- Access endpoints:
- API endpoints:
http://localhost:8080 - Swagger UI:
http://localhost:8080/swagger-ui.html - Health Probe:
http://localhost:8080/actuator/health
- API endpoints:
- Stop Compose without removing database data:
(Data persists in named volume
docker compose down
backendforge-compose-postgres-data).
Build an executable production JAR:
./mvnw clean package -DskipTests
java -jar target/backendforge-1.0.1.jar.\mvnw.cmd clean package -DskipTests
java -jar target/backendforge-1.0.1.jarBuild and run the non-root Docker container image:
docker build -t backendforge:latest .
docker run -d \
--name backendforge-api \
--restart unless-stopped \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_URL="jdbc:postgresql://host.docker.internal:5432/backendforge" \
-e DB_USERNAME="backendforge" \
-e DB_PASSWORD="your-local-password" \
-e JWT_SECRET="<your-base64-secret>" \
-e JWT_ISSUER="backendforge" \
-e JWT_AUDIENCE="backendforge-api" \
backendforge:latestBackendForge includes unit tests and containerized integration tests:
Execute unit tests (e.g., JwtServiceTest, UserServiceTest, UserControllerTest, OpenApiConfigTest) without needing a Docker daemon:
-
Linux / macOS:
./mvnw test "-Dtest=*Test,!*IntegrationTest"
-
Windows PowerShell:
.\mvnw.cmd test "-Dtest=*Test,!*IntegrationTest"
Execute all tests, including integration tests:
-
Linux / macOS:
./mvnw clean test -
Windows PowerShell:
.\mvnw.cmd clean test
Testcontainers Requirement: Integration tests (
DatabaseMigrationIntegrationTest,UserRepositoryIntegrationTest,UserRegistrationIntegrationTest,UserAuthenticationIntegrationTest) automatically spin up temporary PostgreSQL containers using Testcontainers. Docker Desktop or Docker Engine must be running. Testcontainers creates and destroys isolated temporary containers; existing databases or volumes are never modified.
Verify formatting, CRLF hygiene, and whitespace rules:
git diff --checkAll API endpoints exchange JSON payloads unless noted. Protected endpoints require an Authorization: Bearer <access-token> header.
| Method | Endpoint | Access | Rate-Limited | Description |
|---|---|---|---|---|
POST |
/api/v1/auth/register |
Public | Yes (30/min) | Register a new user account |
POST |
/api/v1/auth/login |
Public | Yes (30/min) | Authenticate credentials and receive JWT |
GET |
/api/v1/users/me |
Authenticated | No | Retrieve authenticated user profile |
GET |
/v3/api-docs |
Public | No | OpenAPI v3 specification (JSON) |
GET |
/v3/api-docs.yaml |
Public | No | OpenAPI v3 specification (YAML) |
GET |
/swagger-ui.html |
Public | No | Interactive Swagger UI API documentation |
GET |
/actuator/health |
Public | No | Application health status (show-details: never) |
GET |
/actuator/health/readiness |
Public | No | Kubernetes / container readiness probe |
GET |
/actuator/health/liveness |
Public | No | Kubernetes / container liveness probe |
-
Endpoint:
POST /api/v1/auth/register -
Request Body:
{ "email": "user@example.com", "password": "correct-horse-battery" } -
Validation Constraints:
- Email must be valid and
$\le 320$ characters. It is automatically trimmed and normalized to lowercase. - Password must be between 12 and 128 characters and
$\le 72$ UTF-8 bytes (enforced by PasswordByteLengthValidator due to BCrypt truncation limits).
- Email must be valid and
-
Response (201 Created):
- Headers:
Location: /api/v1/auth/register/{id},X-Correlation-ID: <id> - Body:
{ "id": "c1f7a85e-049d-4762-b918-977464a4b277", "email": "user@example.com", "enabled": true, "createdAt": "2026-10-27T04:45:00.000Z" }
- Headers:
-
Conflict (409 Conflict): Returns code
EMAIL_ALREADY_REGISTEREDif the email address is already registered.
-
Linux / macOS cURL:
curl -X POST http://localhost:8080/api/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","password":"correct-horse-battery"}'
-
Windows PowerShell:
$body = @{ email = "user@example.com"; password = "correct-horse-battery" } | ConvertTo-Json Invoke-RestMethod -Method Post -Uri "http://localhost:8080/api/v1/auth/register" -ContentType "application/json" -Body $body
- Endpoint:
POST /api/v1/auth/login - Request Body:
{ "email": "user@example.com", "password": "correct-horse-battery" } - Response (200 OK):
{ "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJiYWNrZW5kZm9yZ2UiLCJzdWIiOiJjMWY3YTg1ZS0wNDlkLTQ3NjItYjkxOC05Nzc0NjRhNGIyNzciLCJhdWQiOiJiYWNrZW5kZm9yZ2UtYXBpIiwiZXhwIjoxNzYxNTU0NDAwLCJpYXQiOjE3NjE1NTM1MDAsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsInRva2VuX3VzZSI6ImFjY2VzcyJ9...", "tokenType": "Bearer", "expiresIn": 900 } - Authentication Failure (401 Unauthorized):
Returns code
INVALID_CREDENTIALSfor unknown email, incorrect password, or disabled account.Timing Equalization: To prevent account enumeration through response time variance, login verification against nonexistent emails executes against a precomputed dummy hash.
-
Linux / macOS cURL:
curl -X POST http://localhost:8080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","password":"correct-horse-battery"}'
-
Windows PowerShell:
$body = @{ email = "user@example.com"; password = "correct-horse-battery" } | ConvertTo-Json $response = Invoke-RestMethod -Method Post -Uri "http://localhost:8080/api/v1/auth/login" -ContentType "application/json" -Body $body $token = $response.accessToken
- Endpoint:
GET /api/v1/users/me - Headers:
Authorization: Bearer <access-token> - Response (200 OK):
{ "id": "c1f7a85e-049d-4762-b918-977464a4b277", "email": "user@example.com", "enabled": true, "createdAt": "2026-10-27T04:45:00.000Z" } - Failure (401 Unauthorized): Returns code
UNAUTHORIZEDif token is missing, expired, or invalid.
-
Linux / macOS cURL:
curl -X GET http://localhost:8080/api/v1/users/me \ -H "Authorization: Bearer <your-access-token>" -
Windows PowerShell:
Invoke-RestMethod -Method Get -Uri "http://localhost:8080/api/v1/users/me" -Headers @{ Authorization = "Bearer $token" }
Swagger UI is available at http://localhost:8080/swagger-ui.html. To test protected endpoints directly in your browser:
- Open
http://localhost:8080/swagger-ui.htmlin your web browser. - Locate and expand
POST /api/v1/auth/login. Click Try it out, enter your credentials, and click Execute. - Copy the string value of
accessTokenfrom the response body. - Scroll to the top of the Swagger page and click the green Authorize button (displaying an open padlock icon).
- In the
bearerAuthdialog, enter your token (orBearer <your-token>) and click Authorize, then click Close. - The padlock icon will show as locked. You can now execute
GET /api/v1/users/medirectly within the browser interface.
In the default security configuration (SecurityConfig.java), Swagger UI (/swagger-ui.html, /swagger-ui/**) and OpenAPI endpoints (/v3/api-docs/**, /v3/api-docs.yaml) are publicly accessible (.permitAll()) to facilitate local development and API exploration.
When deploying to production, operators should assess whether exposing interactive API documentation to the public internet aligns with their organizational security requirements:
- Disabling Documentation in Production:
Operators can disable Swagger UI and OpenAPI generation entirely via Springdoc configuration properties (for example, in
application-prod.yml):springdoc: api-docs: enabled: false swagger-ui: enabled: false
- Restricting Access Behind Authentication:
Operators who need documentation available in production should apply an intentional access-control policy, such as restricting access at the network/ingress level (e.g., private subnet, VPN, or reverse-proxy firewall) or updating
SecurityConfig.javato require authentication and specific administrative roles for Swagger and OpenAPI endpoints.
All error responses share a standard JSON structure defined by ApiError:
{
"timestamp": "2026-10-27T04:45:00.000Z",
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"fieldErrors": {
"password": "Password must be between 12 and 128 characters"
}
}| HTTP Status | Error Code (code) |
Trigger Condition |
|---|---|---|
400 Bad Request |
VALIDATION_ERROR |
Validation failure on request body fields |
400 Bad Request |
MALFORMED_JSON |
Syntax error in incoming JSON payload |
400 Bad Request |
UNKNOWN_JSON_PROPERTY |
Request contains undeclared JSON property |
400 Bad Request |
BAD_REQUEST |
Disallowed request format or Spring firewall rejection |
401 Unauthorized |
UNAUTHORIZED |
Missing, invalid, or expired Bearer token on protected route |
401 Unauthorized |
INVALID_CREDENTIALS |
Invalid email/password during login |
409 Conflict |
EMAIL_ALREADY_REGISTERED |
Attempted registration with an existing email |
415 Unsupported |
UNSUPPORTED_MEDIA_TYPE |
Request Content-Type is not application/json |
429 Too Many |
RATE_LIMIT_EXCEEDED |
Exceeded 30 requests/min; response contains Retry-After header |
500 Server Error |
INTERNAL_SERVER_ERROR |
Unhandled exception (sanitized in client response) |
When connecting a single-page application (SPA) running on a different origin (such as a Next.js, React, or Vue application running on http://localhost:3000 or https://app.yourdomain.com) to BackendForge running on http://localhost:8080, web browsers enforce the Same-Origin Policy (SOP). Browsers issue an HTTP OPTIONS preflight request. If the backend does not return valid CORS response headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers), the browser blocks the response.
Important: CORS is not configured out of the box in BackendForge. Inspection of SecurityConfig confirms that no
cors()filter orCorsConfigurationSourcebean is registered.
When integrating a frontend application, configure explicit allowed origins in your security configuration.
To enable CORS safely in your derived application:
- Add a
CorsConfigurationSourcebean in SecurityConfig:@Bean public CorsConfigurationSource corsConfigurationSource( @Value("${app.cors.allowed-origins:http://localhost:3000}") List<String> allowedOrigins) { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(allowedOrigins); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Correlation-ID")); configuration.setExposedHeaders(List.of("X-Correlation-ID", "Location", "Retry-After")); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; }
- Enable CORS in the security filter chain:
http.cors(cors -> cors.configurationSource(corsConfigurationSource(allowedOrigins)))
Security Warning:
- Never configure wildcard origins (
"*") in conjunction with credentials (allowCredentials(true)). Doing so exposes authenticated sessions to cross-site credential exploitation and is prohibited by modern browsers.- Maintain allowed origins as an environment-configurable property so development origins (
http://localhost:3000) and production origins (https://app.yourdomain.com) are kept strictly segregated.
BackendForge includes an in-memory, synchronized sliding-window rate limiter implemented in ClientRateLimiter and RateLimitInterceptor:
- Protected Routes: Rate limiting applies to authentication endpoints:
POST /api/v1/auth/loginandPOST /api/v1/auth/register(default: 30 requests per 60-second window). - Table Capacity & Eviction: Tracks active client windows in memory up to a default capacity of 10,000 client entries (
RATE_LIMIT_MAX_ENTRIES/backendforge.rate-limit.max-entries). Stale, expired entries are evicted automatically as new requests are checked. - Capacity Saturation Behavior: If the in-memory window table reaches capacity due to a sudden volume of unique client addresses within an active window, requests from new, unrecorded client addresses may receive an HTTP 429 (
RATE_LIMIT_EXCEEDED) response until expired entries are cleaned up. Existing recorded clients within their permitted window continue normally.
- In-Memory & Instance-Local: Rate-limit counters are held in local JVM memory. Counters are not shared across horizontally scaled application replicas and reset when an application instance restarts.
- Multi-Instance Strategy: For distributed, clustered, or multi-replica production deployments, operators should implement an appropriate shared or edge-level rate-limiting strategy (such as Cloudflare, AWS WAF, an API gateway rate limiter, or an external Redis-backed distributed limiter) to enforce consistent global request thresholds across all nodes.
- Direct Remote Address: RateLimitInterceptor calls
request.getRemoteAddr()directly to identify clients.
When deploying BackendForge behind a reverse proxy, load balancer, or CDN (such as NGINX, HAProxy, AWS ALB, or Cloudflare):
- The Shared Proxy IP Problem:
Because
request.getRemoteAddr()returns the immediate TCP socket peer, it will see the IP address of the reverse proxy, not the originating client. If left unconfigured, all incoming users will share a single rate-limiting bucket, leading to false-positive rate limit rejections (HTTP 429). - Do Not Trust Arbitrary Forwarded Headers:
Reading client-supplied
X-Forwarded-Forheaders directly without a trusted proxy is unsafe. Attackers can forge arbitraryX-Forwarded-Forheader values to bypass rate limits or pollute in-memory tables. Forwarded headers must only be processed when received from verified, trusted proxies. - Production Reverse Proxy Configuration:
BackendForge does not enable
server.forward-headers-strategyunconditionally by default. When deploying behind a trusted reverse proxy, operators must configure proxy handling intentionally:- Framework Forward Headers Strategy:
If your ingress infrastructure terminates SSL and properly strips untrusted client headers before proxying:
server: forward-headers-strategy: framework # or native
- Tomcat Remote IP Valve (with Verified Proxy Topology):
When using embedded Tomcat behind a trusted proxy, configure explicit trusted IP ranges matching your verified infrastructure:
server: tomcat: remoteip: remote-ip-header: x-forwarded-for protocol-header: x-forwarded-proto # Replace with your verified proxy IP addresses or CIDR blocks: # internal-proxies: "10\\.0\\.0\\.\\d{1,3}"
Security Warning: Do not insert guessed proxy CIDRs or blindly copy unverified private network ranges. Ensure the trusted proxy configuration matches your exact deployment topology so untrusted clients cannot spoof remote addresses.
- Network Isolation: Ensure your network firewall or security group prevents public traffic from reaching the backend port directly, guaranteeing that only the trusted reverse proxy can forward requests.
- Framework Forward Headers Strategy:
If your ingress infrastructure terminates SSL and properly strips untrusted client headers before proxying:
BackendForge uses Flyway Community for forward-only, immutable database migrations.
To add a new database table or alter an existing schema:
-
Determine the Next Version Number: Inspect existing migrations in
src/main/resources/db/migration/. The baseline migration is V1__create_users_table.sql. The next migration must be namedV2__<description>.sql. -
Create the Migration Script: Create a new file:
src/main/resources/db/migration/V2__create_projects_table.sql:CREATE TABLE projects ( id UUID PRIMARY KEY, user_id UUID NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_projects_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT ck_projects_name_not_blank CHECK (btrim(name) <> '') ); CREATE INDEX idx_projects_user_id ON projects(user_id);
-
Hibernate Schema Validation Synchronization: BackendForge configures
spring.jpa.hibernate.ddl-auto: validate.- Execution Order: Flyway executes new migrations during application startup before Hibernate initializes.
- Entity Validation: Hibernate compares your Java
@Entityclasses against the database schema. If an@Entityhas a column or table that does not exist in PostgreSQL, or if column types/lengths mismatch, startup terminates with aSchemaManagementException. - Rule: Always create and verify the Flyway SQL migration script before or simultaneously with updating JPA entity classes.
-
Backward-Compatible Schema Evolution (Expand-Contract):
- Immutable History: Never modify, delete, or reorder a migration file that has already run in any environment. Flyway validates migration checksums against
flyway_schema_history; altering a deployed script halts application startup. - Expand: Add new columns as
NULLor with explicit database defaults so existing application instances continue functioning during rolling deployments. - Contract: Author a subsequent migration (e.g.,
V3__...sql) to drop unused columns once older application versions are fully decommissioned.
- Immutable History: Never modify, delete, or reorder a migration file that has already run in any environment. Flyway validates migration checksums against
- Symptom:
or:
org.postgresql.util.PSQLException: Connection to localhost:5432 refusedorg.postgresql.util.PSQLException: FATAL: database "backendforge" does not exist - Causes:
- PostgreSQL service is stopped or not listening on the configured port.
- Application is pointing to
localhost:5432while Compose PostgreSQL is running on host port5434. - The database
backendforgehas not been created.
- Resolution:
- For local PostgreSQL, verify service status and create the database:
psql -U postgres -c "CREATE DATABASE backendforge;" - If using Docker Compose PostgreSQL, set
DB_URL=jdbc:postgresql://localhost:5434/backendforge. - Ensure network firewalls permit TCP connections on the target port.
- For local PostgreSQL, verify service status and create the database:
- Symptom:
or Docker error:
Web server failed to start. Port 8080 was already in use.Bind for 0.0.0.0:5434 failed: port is already allocated - Why Port 5434?
BackendForge's compose.yaml maps PostgreSQL to host port
5434intentionally to avoid collisions with standard PostgreSQL (5432) and common secondary instances (5433). - Identify the Conflicting Process:
- Windows PowerShell:
Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue netstat -ano | findstr :8080
- Linux / macOS:
lsof -i :8080 # or: ss -tulpn | grep 8080
- Windows PowerShell:
- Resolution:
- Stop the conflicting application or container.
- Or reconfigure
SERVER_PORT(e.g.,export SERVER_PORT=8081or$env:SERVER_PORT = "8081").
- Symptom:
or:
java.lang.IllegalStateException: JWT_SECRET must be configured with a Base64-encoded secretjava.lang.IllegalStateException: JWT_SECRET must decode to at least 32 bytes - Causes:
JWT_SECRETis unset or blank.- Secret contains non-Base64 characters.
- Decoded byte array is less than 32 bytes (256 bits).
- Resolution:
Generate a compliant 32-byte Base64 key:
- Linux/macOS:
openssl rand -base64 32 - Windows PowerShell:
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))
- Linux/macOS:
- Symptom:
java.lang.IllegalStateException: Application failed to start with active profile 'prod': DB_URL must be configured - Causes:
SPRING_PROFILES_ACTIVE=prodactivates ProductionConfiguration, which forbids development fallbacks. - Resolution:
Ensure all mandatory production variables are exported in your environment:
DB_URL,DB_USERNAME,DB_PASSWORD,JWT_SECRET,JWT_ISSUER, andJWT_AUDIENCE.
- Symptom:
HTTP 401 Unauthorized:
{ "status": 401, "code": "UNAUTHORIZED", "message": "Authentication is required" } - Causes:
- Token lifetime expired (default: 900 seconds / 15 minutes).
- Secret mismatch (token signed with a different
JWT_SECRETthan currently configured). - Claim mismatch: Token
issorauddoes not match serverJWT_ISSUERorJWT_AUDIENCE. - Missing
Bearerprefix in theAuthorizationheader.
- Resolution:
Re-authenticate via
POST /api/v1/auth/loginand provide the fresh token.
- Symptom:
java.lang.IllegalStateException: Could not find a valid Docker environment (see https://www.testcontainers.org/supported_docker_environment/) - Causes: Docker Desktop or Docker Engine is not running when executing integration tests.
- Resolution:
- Start Docker Desktop / Docker Engine.
- Or run unit tests only (which do not require Docker):
- Linux/macOS:
./mvnw test "-Dtest=*Test,!*IntegrationTest" - Windows PowerShell:
.\mvnw.cmd test "-Dtest=*Test,!*IntegrationTest"
- Linux/macOS:
- Symptom:
- Linux/macOS:
bash: ./mvnw: Permission denied - Windows:
.\mvnw.cmd : File ... cannot be loaded because running scripts is disabled on this system.
- Linux/macOS:
- Resolution:
- Linux / macOS: Grant executable permissions:
chmod +x mvnw
- Windows PowerShell: Set process execution policy:
Or run
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\mvnw.cmddirectly fromcmd.exeor PowerShell. - Network / Proxy Issues: If the wrapper fails downloading Maven, verify your proxy settings or use an installed Maven distribution (
mvn).
- Linux / macOS: Grant executable permissions:
- HTTP 401 (Unauthorized):
- Route requires authentication and token is missing, expired, or invalid.
- Or login failed with
INVALID_CREDENTIALS.
- HTTP 403 (Forbidden):
- BackendForge enforces stateless authentication (
anyRequest().authenticated()). An unauthenticated request triggers 401. If custom role authorization rules are added and an authenticated user lacks permissions, Spring Security returns 403 Forbidden.
- BackendForge enforces stateless authentication (
- HTTP 429 (Too Many Requests):
- Returns code
RATE_LIMIT_EXCEEDED. - Triggered when a client exceeds 30 login or registration requests per 60 seconds from the same remote IP.
- Inspect the
Retry-After: <seconds>response header before repeating requests.
- Returns code
-
Request Correlation (
X-Correlation-ID):- Every request is assigned a correlation ID by CorrelationIdFilter.
- Clients can pass an
X-Correlation-ID(orX-Request-ID) header. Valid IDs (alphanumeric, hyphens, underscores, dots,$\le 64$ chars) are retained; multiline or invalid IDs are replaced with a fresh UUID. -
X-Correlation-IDis returned on all HTTP responses and logged via SLF4J MDC (correlationId).
-
Health Probes:
-
/actuator/health/readinessand/actuator/health/livenessprovide container readiness/liveness checks. -
management.endpoint.health.show-details: neverprevents leakage of database details or connection strings. - All other Actuator management endpoints are unexposed.
-
-
Log Sanitization & Hygiene:
- Exception handlers log only HTTP method, URI, remote IP, and exception class names.
- Passwords, authorization tokens, JWT claims, request bodies, and connection credentials are never logged.
Before deploying BackendForge to production:
- 1. Active Profile: Set
SPRING_PROFILES_ACTIVE=prodto enforce fail-fast production validation. - 2. Database Credentials & Connectivity:
- Supply production
DB_URL,DB_USERNAME, andDB_PASSWORD. - Ensure the database user has permissions for Flyway migration history (
flyway_schema_history). - Keep
spring.jpa.hibernate.ddl-auto: validateenabled.
- Supply production
- 3. JWT Signing Key & Claims:
- Generate a secure, unique Base64 secret (
$\ge 32$ bytes) using a cryptographically secure random source. - Set
JWT_ISSUERandJWT_AUDIENCEto your organization's domain.
- Generate a secure, unique Base64 secret (
- 4. Trusted Reverse Proxy & Network Security:
- Terminate TLS at your ingress, load balancer, or reverse proxy.
- Configure
server.forward-headers-strategy=frameworkor trusted remote IP properties when running behind an internal reverse proxy. - Restrict backend network access so public traffic cannot bypass the proxy.
- 5. Ingress Rate Limiting:
- Because ClientRateLimiter is instance-local, deploy edge rate limiting (Cloudflare, AWS WAF, or NGINX) for multi-replica production environments.
- 6. Container Operations:
- Run the container as non-root
appuser(UID10001). - Configure container health checks against
/actuator/health/readiness. - Allow at least 25–30 seconds for graceful shutdown (
timeout-per-shutdown-phase: 20s).
- Run the container as non-root
- Starter Kit Nature: BackendForge is a production-hardened backend starter kit and architectural foundation authored by The Creator Co. It is intended to be customized, extended, and integrated into your organization's deployment environment. It requires environment-specific security reviews, secrets management, and infrastructure configuration before production use. It is sold as a source-code starter kit, not a hosted SaaS or cloud service.
- Commercial Licensing Model: BackendForge is licensed under an unlimited-project model (allowing unlimited personal, SaaS, and client project deployments, with strict prohibitions on reselling or redistributing the starter kit itself). The included LICENSE.md file is a preliminary commercial license draft provided for legal review and customization. It does not constitute legal advice and must be finalized by licensed legal counsel before commercial release. See COMMERCIAL-RELEASE-BLOCKERS.md for the active checklist of legal, business, and verification decisions.
- Update Policy: Licenses grant perpetual permission to use the purchased version (
1.0.1). Future major versions and feature updates are sold separately; no free lifetime updates or ongoing maintenance obligations are included. - Commercial Launch Preparation: Pre-launch product listing copy is maintained in GUMROAD-PRODUCT-LISTING-DRAFT.md, and operational merchant setup steps are tracked in GUMROAD-LAUNCH-CHECKLIST.md.
- Third-Party Dependency Notices: The included THIRD-PARTY-NOTICES.md file provides comprehensive open-source licensing attributions. Third-party dependencies are fetched directly from Maven Central and are not bundled in binary form in the release archive. If distributing precompiled binaries or container images, distributors must perform an audit to ensure complete compliance with upstream licenses (such as LGPL-2.1 and EPL).
- Support and Warranties: BackendForge is provided on an "AS IS" basis without express or implied warranties or technical support guarantees unless separately contracted.
- Spring Boot:
3.5.6 - Java:
21(LTS) - Springdoc OpenAPI:
2.8.13 - PostgreSQL:
16.4(Compose) /15(Testcontainers) - JJWT (Java JWT):
0.12.6 - Flyway:
11.7.2 - Testcontainers:
2.0.5