A full-stack internal operations system for a software consulting business, built to demonstrate production-oriented engineering decisions across authentication, authorization, lifecycle management, persistence, reporting, auditability, and automated testing.
V1 is feature-complete and is currently undergoing final release validation and demo preparation. The application runs locally; this repository does not claim a cloud deployment or production hosting environment.
Internal tools often begin as simple CRUD applications, but the difficult engineering work appears when business rules start interacting:
- users should see different data based on both role and ownership;
- deactivated clients must stop accepting new work without erasing history;
- completed and archived projects need explicit, auditable transitions;
- deleted operational records must disappear from normal workflows while historical reporting remains meaningful;
- browser controls should reflect permissions, but the server must remain the authorization boundary.
The Internal Operations Platform explores those concerns in a maintainable, explainable codebase. It is also an engineering mentorship simulation, so the implementation favors explicit domain behavior, layered responsibilities, and focused tests over unnecessary abstraction.
The V1 browser application and REST API support:
- user administration, profile updates, role assignment, and soft deletion;
- client creation, editing, deactivation, reactivation, and soft deletion;
- project creation, editing, completion, archival, restoration, and soft deletion;
- personal and administrator-managed time entries in quarter-hour increments;
- personal-hours reports for every role;
- organization-wide project, client, and user hours reports for administrators;
- immutable audit events for significant create, update, lifecycle, role, and delete operations;
- project status history with the actor and timestamp for each transition.
These workflows are intentionally connected. For example, an inactive client remains available to historical reads but cannot accept a new project or new/changed time entry.
flowchart LR
Browser["React + TypeScript SPA"]
API["Spring Boot REST API"]
Security["Spring Security<br/>session + CSRF"]
Service["Application services<br/>authorization + domain rules"]
Data["Spring Data JPA"]
DB[("PostgreSQL")]
Flyway["Flyway migrations"]
Browser -->|"JSON over /api<br/>session cookie"| API
API --> Security
Security --> Service
Service --> Data
Data --> DB
Flyway --> DB
The React single-page application uses route-level authentication, role-aware navigation, typed API modules, and a shared client that handles cookies and CSRF tokens. UI permissions improve usability by hiding inaccessible controls; they are never treated as a security boundary.
Vite serves the local frontend on http://localhost:5173 and proxies /api requests to the backend on port 8080.
The backend preserves a conventional dependency direction:
Controller → Service → Repository → PostgreSQL
- Controllers own HTTP mapping and DTO conversion.
- Services own authorization, application behavior, and lifecycle rules.
- Repositories own persistence queries and soft-delete visibility.
- Entities are not returned directly over HTTP.
Request and response DTOs keep the public contract separate from persistence state, particularly password hashes and internal relationships. Validation, application exceptions, and Spring Security failures use consistent JSON Problem Details without exposing stack traces.
PostgreSQL is the system of record. Flyway applies versioned SQL migrations, while Hibernate runs with ddl-auto: validate; the application verifies the schema instead of silently generating or changing it.
Soft deletion preserves relational history. Normal operational queries exclude deleted records, while administrator project and client reports can still aggregate retained, non-deleted historical time entries for a deleted subject.
Authentication uses local email/password credentials, adaptive password hashing through Spring Security, server-managed HTTP sessions, and CSRF protection for state-changing requests.
The server derives identity from the authenticated principal rather than accepting an acting-user identifier from the client. This prevents callers from selecting another identity in a request and keeps authorization decisions tied to verified credentials.
The following endpoints are intentionally public:
POST /api/auth/loginGET /api/auth/csrfGET /actuator/health
All business endpoints require authentication. Only the Actuator health endpoint is exposed for operational checks.
The application defines USER, MANAGER, and ADMIN roles. V1 deliberately keeps USER and MANAGER permissions equivalent for most workflows; manager assignment, approvals, and delegated ownership are post-V1 concerns.
| Capability | USER / MANAGER | ADMIN |
|---|---|---|
| Users | View the active directory, view/update own profile | Create, view, update, assign roles, and soft-delete other users |
| Clients | View active clients | Manage all non-deleted clients and lifecycle actions |
| Projects | View active projects and their status history | Manage all non-deleted projects and lifecycle actions |
| Time entries | Create, view, and update own entries | View and manage any non-deleted entry |
| Reports | View own hours | View own hours and organization-wide reports |
| Audit history | No centralized access | View audit history |
Administrative safeguards prevent self-demotion, self-deletion, and removal or demotion of the final active administrator. The complete rules are documented in the authorization matrix.
Lifecycle changes are modeled as business operations rather than arbitrary status edits:
- Clients move between
ACTIVEandINACTIVE; deletion is soft. - Projects can move from
ACTIVEtoCOMPLETEDorARCHIVED. - Completed projects may be archived.
- Archived projects may be restored to
ACTIVE. - Completed projects do not have a direct reactivation transition.
- Time entries can be added or changed only while their project and client are active.
- Explicitly deleted time entries are excluded from report totals.
Project status history answers what changed and when. Audit events answer who performed the operation. Keeping these concepts separate makes lifecycle presentation useful without weakening the broader audit trail.
Reports aggregate hours by project, client, or user. Regular users and managers can view only their own hours; administrators can access organization-wide report routes.
Historical reporting is designed around retained business activity:
- soft-deleted projects and clients disappear from ordinary operational lists;
- administrator reports for those project or client identifiers remain available;
- retained time entries continue contributing to those reports;
- explicitly deleted time entries do not contribute.
Spring Boot Actuator exposes only GET /actuator/health, which is accessible without authentication. Docker Compose also uses pg_isready to report PostgreSQL health. Metrics, dashboards, cloud probes, and deployment-specific monitoring are intentionally outside V1.
| Area | Technology |
|---|---|
| Frontend | React 19, TypeScript, React Router, Vite |
| Frontend testing | Vitest, React Testing Library, MSW, Playwright |
| Backend | Java 21, Spring Boot 4, Maven |
| Security | Spring Security, server-managed sessions, CSRF |
| Persistence | PostgreSQL 17, Spring Data JPA, Flyway |
| API documentation | Springdoc OpenAPI and Swagger UI |
| Backend testing | JUnit 5, AssertJ, MockMvc, Testcontainers |
| Local infrastructure | Docker Compose |
| CI | GitHub Actions, Temurin Java 21, Node.js 22 |
Tests are placed at the narrowest useful boundary while retaining real PostgreSQL and HTTP confidence where those integrations matter.
| Layer | Purpose |
|---|---|
| Backend unit | Entity invariants, service permissions, lifecycle branches, reporting orchestration, and security configuration |
| Repository integration | JPA mappings, PostgreSQL queries, constraints, and soft-delete behavior |
| Service integration | Transactional behavior and relationship rules that benefit from real persistence |
| Controller integration | Sessions, CSRF, validation, authorization, JSON contracts, persistence, reporting, and error responses through MockMvc |
| Frontend component | User-visible rendering and interaction with React Testing Library and MSW at the HTTP boundary |
| Browser E2E | Representative workflows through the real frontend and backend with Playwright |
The latest verified backend run passed 356 tests. The frontend component suite contains 307 tests across 30 files. Playwright defines 8 tests across 6 specifications for login, users, clients, projects, time entries, and reports.
GitHub Actions runs:
- the Maven suite on Java 21;
- frontend lint, all component tests, and a production build on Node.js 22.
Playwright is currently a locally orchestrated release check because CI does not provision its backend, PostgreSQL, credentials, and test data.
See the complete testing strategy.
- Java 21
- Docker with Docker Compose
- Node.js 22 and npm
From the repository root:
docker compose up -d
docker compose psDocker Compose starts PostgreSQL 17 on localhost:5432, persists data in a named volume, and waits on a pg_isready health check.
cd backend
./mvnw spring-boot:runFlyway applies the schema migrations at startup. The backend is available at:
- API:
http://localhost:8080/api - Authenticated Swagger UI:
http://localhost:8080/swagger-ui.html - Authenticated OpenAPI JSON:
http://localhost:8080/v3/api-docs - Health:
http://localhost:8080/actuator/health
In a separate terminal:
cd frontend
npm ci
npm run devOpen http://localhost:5173.
Backend tests require Docker because they start PostgreSQL with Testcontainers:
cd backend
./mvnw testRun the frontend CI-equivalent checks:
cd frontend
npm run lint
npm run test:run
npm run buildFor the locally orchestrated browser suite, start PostgreSQL and the backend first, provide valid administrator credentials, and ensure an active client exists:
cd frontend
npx playwright install chromium
E2E_ADMIN_EMAIL=admin@iop.local \
E2E_ADMIN_PASSWORD="<local-demo-password>" \
npm run test:e2eThe backend reads datasource settings from environment variables and supplies explicit local-development defaults:
| Variable | Local default | Purpose |
|---|---|---|
DB_URL |
jdbc:postgresql://localhost:5432/iop |
JDBC connection URL |
DB_USERNAME |
postgres |
Database username |
DB_PASSWORD |
postgres |
Database password |
Environment values take precedence over these defaults. The defaults align with docker-compose.yml and are not production credentials.
Playwright also accepts:
| Variable | Purpose |
|---|---|
E2E_ADMIN_EMAIL |
Administrator email used by business workflow tests |
E2E_ADMIN_PASSWORD |
Password for that administrator |
No secrets file or cloud-specific configuration is included.
With the PostgreSQL container running, reset the local database to the canonical demo dataset from the repository root:
backend/scripts/reset-demo-data.shThe script requires typing RESET before it proceeds. For a non-interactive local reset:
backend/scripts/reset-demo-data.sh --yesThis is a destructive local/demo-only operation. It replaces all application data, preserves the Flyway migration history and the bootstrap administrator admin@iop.local, and gives every seeded @iop-demo.local user the demo password fake-password.
Flyway seeds admin@iop.local with the local demo password fake-password as a bootstrap administrator. The seed exists only for development and demonstrations, is not suitable for production operation, and is not automatically retired in new databases.
A representative demo:
- Sign in with
admin@iop.local/fake-password. - Create a replacement administrator with a new password.
- Use the replacement account for normal administration.
- Create an active client.
- Create a project for that client.
- Create a regular user and record time against the active project.
- Review the user, project, and client hours reports.
- Complete, archive, and restore the project while reviewing status and audit history.
- Deactivate the client and observe that new work is blocked while historical information remains available.
Any real deployment procedure must verify the replacement administrator and then explicitly disable or remove the bootstrap account.
.
├── backend/
│ ├── http/ # Manual API request examples
│ ├── scripts/ # Local password-hash helper
│ └── src/
│ ├── main/
│ │ ├── java/ # Controllers, services, repositories, entities, DTOs
│ │ └── resources/ # Application configuration and Flyway migrations
│ └── test/ # Unit and PostgreSQL-backed integration tests
├── frontend/
│ ├── e2e/ # Playwright browser workflows
│ └── src/
│ ├── api/ # Typed API modules and shared client
│ ├── auth/ # Session state, protected routes, UI permissions
│ ├── components/ # Reusable business and request-state components
│ ├── pages/ # Route-level workflows
│ └── test/ # MSW and component-test support
├── docs/
│ ├── architecture/ # Backend and domain architecture
│ ├── database/ # Schema and persistence design
│ ├── decisions/ # Architecture decision records
│ └── sprints/ # Preserved historical planning records
├── .github/workflows/ # Backend and frontend CI
└── docker-compose.yml # Local PostgreSQL
The ADRs capture why foundational choices were made and which alternatives were rejected:
- Use Spring Boot
- Use PostgreSQL
- Use Flyway
- Use Docker Compose for local PostgreSQL
- Backend testing strategy
- Continuous integration strategy
- API documentation strategy
- Session-based authentication with Spring Security
- Frontend architecture
For current behavior, see the requirements, project state, and release checklist. Historical sprint and log documents provide context but do not override the implementation or current living documentation.
- There is no cloud deployment, production hosting target, or automated deployment pipeline.
- Playwright business workflows require a separately running backend, PostgreSQL database, valid credentials, and suitable test data; they are not run in CI.
- The bootstrap administrator is seeded into every newly migrated database and requires a manual handoff and retirement procedure.
- Local datasource credentials are development defaults, not a production secret-management design.
- Backup, rollback, migration-recovery, HTTPS/reverse-proxy, and production session-topology procedures are not defined without a deployment target.
MANAGERis a reserved role with no assignment, approval, or delegated-ownership workflow in V1.- Completed projects cannot be directly reactivated; only archived projects can be restored to
ACTIVE. - Soft-deleted entities do not have a general restoration workflow or deleted-record browser.
Post-V1 work is intentionally separated from the completed release scope. Candidate directions include:
- manager-owned clients, project assignments, approvals, and delegated workflows;
- billing rates, invoice preparation, and richer utilization reporting;
- deployment-target selection, automated environment provisioning, and CI-driven browser tests;
- production logging, metrics, alerting, backup, rollback, and migration-recovery procedures;
- production-specific session, proxy, HTTPS, and secret-management design.
These are ideas, not claims of current functionality. See the roadmap for the broader staged product direction.
This project is built on the Spring, React, PostgreSQL, Flyway, Testcontainers, Vitest, Testing Library, MSW, and Playwright ecosystems.
It was developed as both a portfolio application and an engineering mentorship exercise, with an emphasis on making architectural reasoning, tradeoffs, and verification visible to reviewers.





