A full-stack physical security operations dashboard. The current version manages security devices such as cameras, card readers, alarm panels, and motion sensors.
This project is built as a portfolio-ready app with a Spring Boot API, PostgreSQL persistence, Flyway migrations, JWT authentication, role-based access control, audit logging, integration tests, and a React dashboard frontend.
The deployed app is linked in the repository About section.
Use Try Interactive Demo on the sign-in screen to explore sample device data and audit logs without credentials.
Production deployments should track main (full-stack backend + database).
main: production-oriented full-stack app (Spring Boot API, PostgreSQL, JWT auth, backend-enforced RBAC, persisted audit logs)feature/demo-persistent-crud: portfolio simulation build (frontend-only local persistence with role-based UI behavior and resettable data)
Recommended hosting split:
- Vercel production domain ->
main - Vercel preview deployments -> feature branches (including
feature/demo-persistent-crud)
This keeps the production architecture and the portfolio simulation experience separate by design.
- Java 25
- Spring Boot 4
- Spring Web MVC
- Spring Data JPA
- Spring Security
- PostgreSQL
- Docker Compose
- Swagger / OpenAPI for local development
- Testcontainers
- Maven
- React
- Vite
- Create, read, update, and delete physical security devices
- Filter devices by status, type, and location
- Store optional asset details such as model, MAC address, IP address, and manufacturer
- Validate required request fields
- Use enums for controlled device types and statuses
- Authenticate with JWT bearer tokens and role-based access control
- Record an immutable audit trail for device create, update, and delete actions
- Expose Swagger only in the local dev profile
- Manage database schema with Flyway migrations
- Expose only the Actuator health endpoint in production
- Run integration tests against PostgreSQL with Testcontainers
- Use a React dashboard for login, device metrics, filtering, role-gated device management, and admin audit review
- Java 25
- Docker Desktop
- Maven wrapper included in the project
Create your local environment file:
cp .env.example .envYour .env should include local-only values. These are examples, not production credentials:
POSTGRES_DB=physical_security_dashboard
POSTGRES_HOST=localhost
POSTGRES_USER=local_user
POSTGRES_PASSWORD=change-me-local-postgres-password
POSTGRES_PORT=5432
APP_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
JWT_SECRET=change-me-local-jwt-secret-at-least-32-chars
JWT_EXPIRATION_MS=3600000Always load .env before starting the backend so Spring and Docker Compose use the same database credentials:
set -a; source .env; set +a; docker compose up -d
set -a; source .env; set +a; ./mvnw spring-boot:run -PdevIf Postgres was previously started with different credentials, reset the local volume:
docker compose down -v
set -a; source .env; set +a; docker compose up -dThe API runs at:
http://localhost:8080
Install frontend dependencies:
cd frontend
npm installCreate the optional frontend environment file:
cp .env.example .envFor local development, leave VITE_API_BASE_URL blank so Vite can proxy API requests to the backend:
VITE_API_BASE_URL=Start the frontend dev server:
npm run devThe frontend runs at:
http://localhost:5173
The Vite dev server proxies /api and /actuator requests to the Spring Boot backend at http://localhost:8080.
When the backend runs with the dev profile, Flyway also runs local-only seed data from src/main/resources/db/dev-migration. Sign in with one of the seeded dev users. All three use the password changeme:
admin (ADMIN)
operator (OPERATOR)
viewer (VIEWER)
Swagger is enabled only when running with the dev profile:
http://localhost:8080/swagger-ui.html
In Swagger, call POST /api/auth/login, copy the accessToken, then click Authorize and paste:
Bearer <accessToken>
Local dev example using the seeded admin user:
POST /api/auth/login
Content-Type: application/json{
"username": "admin",
"password": "changeme"
}Example response:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": 3600000,
"username": "admin",
"role": "ADMIN"
}GET /api/auth/me
Authorization: Bearer <accessToken>All protected /api/** endpoints require a valid JWT in the Authorization header.
| Action | VIEWER | OPERATOR | ADMIN |
|---|---|---|---|
GET /api/devices |
yes | yes | yes |
POST /api/devices |
no | yes | yes |
PUT /api/devices/{id} |
no | yes | yes |
DELETE /api/devices/{id} |
no | no | yes |
GET /api/audit-logs |
no | no | yes |
GET /api/devices
Authorization: Bearer <accessToken>Optional query parameters:
status=ONLINE
type=CAMERA
location=lobby
Example:
GET /api/devices?status=ONLINE&type=CAMERA&location=lobbyGET /api/devices/{id}
Authorization: Bearer <accessToken>POST /api/devices
Authorization: Bearer <accessToken>
Content-Type: application/json{
"name": "Front Entrance Camera",
"type": "CAMERA",
"location": "Main Lobby",
"status": "ONLINE",
"model": "Axis P3265-LV",
"macAddress": "00:1A:2B:3C:4D:5E",
"ipAddress": "192.168.1.10",
"manufacturer": "Axis"
}PUT /api/devices/{id}
Authorization: Bearer <accessToken>
Content-Type: application/jsonUses the same request body shape as create.
DELETE /api/devices/{id}
Authorization: Bearer <accessToken>Returns 204 No Content when successful.
GET /api/audit-logs
Authorization: Bearer <accessToken>Admin only. Optional query parameters:
action=CREATE
entityType=DEVICE
Supported device types:
CAMERA
CARD_READER
ALARM_PANEL
MOTION_SENSOR
Supported device statuses:
ONLINE
OFFLINE
MAINTENANCE
ALERTING
Run the test suite:
./mvnw testThe integration tests use Testcontainers, so Docker must be running.
The deployed app uses Railway for the Spring Boot API and PostgreSQL database, and Vercel for the React frontend. Both deployments can use the same GitHub repository.
- Create a Railway project from this repository.
- Deploy the backend service from the repo root.
railway.jsontells Railway to use the rootDockerfileand healthcheck/actuator/health. - Add a Railway PostgreSQL database to the same project.
- In the backend service's Variables tab, set:
SPRING_PROFILES_ACTIVE=prodJWT_SECRET=<random-32+-character-secret>JWT_EXPIRATION_MS=3600000APP_CORS_ALLOWED_ORIGINS=https://<your-vercel-domain>PGHOST=${{ Postgres.PGHOST }}PGPORT=${{ Postgres.PGPORT }}PGDATABASE=${{ Postgres.PGDATABASE }}PGUSER=${{ Postgres.PGUSER }}PGPASSWORD=${{ Postgres.PGPASSWORD }}
- Deploy the backend and generate a public Railway domain.
- Verify the backend:
curl https://<your-railway-domain>/actuator/healthThe first boot runs Flyway migrations and creates the devices, users, and audit_logs tables automatically. Production does not seed login users. Create production users manually with private credentials before expecting login to work.
After creating a production user, verify login with that private username and password:
curl -s -X POST https://<your-railway-domain>/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"<your-production-username>","password":"<your-production-password>"}'- Import the same repository into Vercel.
- Set the project root to
frontend/(Vercel auto-detects Vite). - Add the environment variable:
VITE_API_BASE_URL=https://<your-railway-domain>
- Deploy. The
vercel.jsonrewrite sends all routes toindex.htmlso the SPA handles navigation. - Copy the Vercel frontend URL.
- In Railway, update the backend variable:
APP_CORS_ALLOWED_ORIGINS=https://<your-vercel-domain>,https://<your-project>-*-<your-team>.vercel.app- The second entry is optional and uses a wildcard to match Vercel preview deployments, which get a unique build-hash subdomain on every deploy.
- Redeploy or restart the backend, then sign in through the Vercel frontend.
- Keep
.envlocal. It is gitignored and Railway/Vercel do not read it. - Use a long, random
JWT_SECRETin production. Do not reuse the local example value. - Do not use Railway's
DATABASE_PUBLIC_URLfor the deployed backend. Use the privatePG*variables. APP_CORS_ALLOWED_ORIGINSaccepts comma-separated origin patterns (exact URLs or patterns containing*wildcards). Add your production Vercel URL, plus a wildcard pattern if you want preview deployments to work.- HTTPS is required for production deployments. Railway and Vercel provide HTTPS by default.
- The frontend stores JWTs in
sessionStoragefor this portfolio SPA. That is acceptable for a demo, but httpOnly cookies are a stronger choice for high-risk production apps. - Swagger and OpenAPI are disabled outside the
devprofile. - Database schema is managed by Flyway in
src/main/resources/db/migration. - Never set
spring.jpa.hibernate.ddl-autotoupdateorcreatein production. Theprodprofile usesvalidateso Hibernate checks the Flyway-managed schema.
- Polish the frontend dashboard
- Add backend dashboard summary metrics
- Add security event tracking
- Add alert severity filtering
- Add admin user management for production accounts