Rootedz is a community platform built around a simple hierarchy:
Root
└── Branch
└── Leaf
- Root is a community, category, or public topic.
- Branch is a post published inside a Root.
- Leaf is a comment attached to a Branch.
The product is designed as a focused Reddit-style forum with a light green visual identity, practical moderation controls, and a production-ready deployment stack.
- Username-based sign in.
- Case-insensitive usernames and email addresses.
- Email verification flow.
- Automatic country detection from trusted edge headers during registration.
- Root, Branch, and Leaf CRUD operations.
- Branch likes with persistent like state and counters.
- Helpful marks on comments.
- Profile statistics for posts, comments, likes, and helpful marks.
- JWT authentication with browser session storage.
- Pagination for feeds and comment lists.
- PostgreSQL persistence with versioned SQL migrations.
- RabbitMQ outbox publishing, retries, and dead-letter queues.
- Nginx security headers, request limits, and static frontend serving.
- Docker Compose environments for local and production use.
- Cloudflare Tunnel deployment without exposing application ports publicly.
- Prometheus metrics and Grafana dashboards.
- Java 17
- Spring Boot 4
- Spring Web, Spring Data JPA, and Hibernate
- Spring Security with JWT
- PostgreSQL
- RabbitMQ
- Gradle
- React
- Vite
- React Router
- TanStack Query
- Vitest
- CSS
- IBM Plex typography
- Docker and Docker Compose
- Nginx
- Cloudflare Tunnel
- Prometheus
- Grafana
Rooted_API/
├── src/main/java/ Backend source code
├── src/main/resources/ Spring configuration and migrations
├── frontend/ React frontend
├── deploy/
│ ├── postgres/ PostgreSQL migrations and helpers
│ ├── monitoring/ Prometheus and Grafana configuration
│ ├── nginx/ Production reverse proxy configuration
│ └── secrets/ Local-only certificates and secrets
├── docker-compose.yml Local application dependencies
├── compose.production.yml Production stack
├── compose.cloudflare.yml Cloudflare Tunnel service
├── Dockerfile Backend image
├── .env.example Environment variable template
└── gradlew.bat Gradle wrapper for Windows
- JDK 17 or newer
- Docker Desktop with Docker Compose
- Node.js 20 or newer
- npm
- A PostgreSQL database
- RabbitMQ for local development, unless it is already available elsewhere
Copy the template and fill in local values:
Copy-Item .env.example .envNever commit .env, production credentials, JWT secrets, database certificates, or tunnel tokens.
docker compose up -d rabbitmqIf you already have RabbitMQ and PostgreSQL running, configure their connection values in .env and skip this step.
.\gradlew.bat bootRunThe backend listens on http://localhost:8080.
Useful endpoints:
- API base:
http://localhost:8080/api - Health:
http://localhost:8080/actuator/health - Metrics:
http://localhost:8080/actuator/prometheus
Set-Location frontend
npm.cmd install
npm.cmd run devThe Vite development server listens on http://localhost:5173.
For a production-like frontend build:
npm.cmd run build
npm.cmd run previewRegistration and login use a username, not an email address.
Usernames and email addresses are normalized to lower case for lookup, so these values are treated as equivalent:
RootedUser
rooteduser
ROOTEDUSER
Example registration request:
POST /auth/register
Content-Type: application/json{
"username": "rooteduser",
"email": "person@example.com",
"password": "change-me"
}Example login request:
POST /auth/login
Content-Type: application/json{
"username": "rooteduser",
"password": "change-me"
}The frontend stores the authenticated session in browser session storage. Clear the session by signing out or removing the application session from browser storage.
The registration form does not ask the user to enter a country code. The backend resolves the country from a trusted reverse-proxy header.
Supported headers include:
CF-IPCountryfor CloudflareCloudFront-Viewer-Countryfor Amazon CloudFrontX-Vercel-IP-Countryfor Vercel- A custom header configured with
COUNTRY_HEADER_NAME
Production configuration should enable trusted headers only behind a proxy that controls and overwrites those headers:
TRUST_COUNTRY_HEADERS=true
COUNTRY_HEADER_NAME=CF-IPCountryIf no trusted country value is available, the backend stores the fallback code ZZ.
Like a Branch:
POST /branch/{branchId}/like
Authorization: Bearer <token>Remove a like:
DELETE /branch/{branchId}/like
Authorization: Bearer <token>The API returns the current like state and count. The frontend updates the Branch card immediately and then synchronizes with the server.
Mark a Leaf as helpful:
POST /leaves/like/{leafId}
Authorization: Bearer <token>Remove a helpful mark:
DELETE /leaves/unlike/{leafId}
Authorization: Bearer <token>Helpful marks are persisted per user. Profile counters and comment lists use the stored values instead of decorative placeholders.
Feed and comment endpoints expose paginated results. A typical response contains:
{
"content": [],
"page": 0,
"size": 20,
"totalElements": 0,
"totalPages": 0,
"last": true
}Important counters include:
commentsCounton Branch responseshelpfulMarkson Leaf responses- Branch like count and the current user's like state
- Profile totals for created Roots, Branches, Leaves, likes, and helpful marks
Versioned PostgreSQL scripts are stored in deploy/postgres/:
001_case_insensitive_users.sql
002_branch_likes.sql
003_leaf_helpful_marks.sql
004_profile_statistics.sql
Run them in numeric order against the target database. Back up the database before applying migrations in production. The application is safe to restart after a completed migration, but each migration should be applied only once.
Domain events are published through RabbitMQ using an outbox workflow. This keeps database writes and event publication recoverable when the broker is temporarily unavailable.
Relevant environment variables include:
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=rooted
RABBITMQ_PASSWORD=rooted
RABBITMQ_RETRY_MAX_ATTEMPTS=5
RABBITMQ_RETRY_INITIAL_DELAY_MS=1000Failed messages are retried and eventually moved to dead-letter queues. Inspect the RabbitMQ management UI when diagnosing delivery failures.
The production deployment uses three Compose files:
docker compose -f compose.production.yml -f compose.cloudflare.yml up -d --buildThe stack contains:
- Backend API
- React frontend served by Nginx
- RabbitMQ
- Cloudflare Tunnel
- Prometheus
- Grafana
Only the monitoring services bind to loopback ports by default. The public application traffic goes through Cloudflare Tunnel, so backend and frontend containers do not need public host port mappings.
Create a protected environment file on the server:
cp .env.example deploy/production.env
chmod 600 deploy/production.envSet production values, including:
APP_PUBLIC_BASE_URL=https://rootedz.eu
APP_FRONTEND_BASE_URL=https://rootedz.eu
CORS_ALLOWED_ORIGINS=https://rootedz.eu,https://www.rootedz.eu
TRUST_COUNTRY_HEADERS=true
COUNTRY_HEADER_NAME=CF-IPCountryDo not store the Cloudflare token in the repository. Pass it through the server environment or an ignored production environment file.
Configure the tunnel to route both hostnames to the frontend service:
rootedz.eu -> http://frontend:80
www.rootedz.eu -> http://frontend:80
The frontend Nginx configuration proxies API requests to the backend container over the private Compose network.
For a managed PostgreSQL provider that requires certificate verification, mount the provider CA certificate as a Docker secret and use a JDBC query similar to:
DB_JDBC_QUERY=sslmode=verify-full&sslrootcert=/run/secrets/postgres-ca.pem
DB_CA_CERT_FILE=./deploy/secrets/postgres-ca.pemKeep the CA certificate outside Git unless it is explicitly safe to publish.
Prometheus is available on loopback port 9090 and Grafana on loopback port 3000. For remote access, use an SSH tunnel instead of exposing these ports publicly:
ssh -L 3000:127.0.0.1:3000 -L 9090:127.0.0.1:9090 user@serverThen open:
- Grafana:
http://localhost:3000 - Prometheus:
http://localhost:9090
Email delivery requires valid DNS records for the mail provider, including MX, SPF, DKIM, DMARC, and any provider-specific autodiscovery or MTA-STS records. Keep mail DNS records separate from the web application tunnel records.
The frontend includes the following main views:
/for the Root feed/r/:rootTitlefor a Root and its Branches/b/:branchIdfor a Branch and its Leaves/u/:usernamefor user statistics and activity/loginand/registerfor authentication/auth/verifyfor email verification/roots/new,/r/:rootTitle/new, and/settingsfor protected actions
The frontend uses TanStack Query for server state, React Router for navigation, and a shared API client for authentication and request errors.
Backend tests:
.\gradlew.bat testFrontend tests:
Set-Location frontend
npm.cmd run testFrontend lint and build:
npm.cmd run lint
npm.cmd run buildBefore a production release, also verify:
- Registration with a mixed-case username and email.
- Login using the normalized username.
- Country detection through the configured proxy header.
- Creating and deleting a Root, Branch, and Leaf.
- Liking and unliking a Branch.
- Adding and removing a helpful mark from a Leaf.
- Profile counters after each interaction.
- Cloudflare Tunnel health and both public hostnames.
- Database backup and migration status.
- Rotate all credentials that have ever been shared in chat, shell history, screenshots, or logs.
- Use a unique, high-entropy JWT secret in production.
- Keep
.env,deploy/production.env, certificates, tunnel tokens, and database dumps out of Git. - Terminate public TLS at Cloudflare and keep the origin reachable only through the tunnel.
- Do not trust country headers from direct public clients.
- Restrict Grafana, Prometheus, RabbitMQ, and actuator endpoints to private access.
- Apply database backups and test restoration regularly.
No public license has been declared yet. Add a license file before accepting external contributions or distributing Rootedz under explicit terms.