A full-stack B2B asset management system with four-eyes dual control, tamper-evident SHA-256 audit trails, and chain-of-custody tracking. Built as an open-source reference application for enterprise Java patterns: token rotation, optimistic locking, role-based access control, workspace isolation, and verifiable audit integrity.
The demo seeds isolated workspaces with role-based users, 20 assets, custody assignments, workflow rows, notifications, and one deliberately tampered audit row. The data is created through the normal service flow, so create, assign, transfer, issue, disposal, notification, and integrity-check paths all exercise real application logic.
![]() Landing Page |
![]() Dashboard |
![]() Asset Detail |
![]() Four-Eyes Approval |
![]() Audit Trail |
![]() User Management |
Vault Manager requests disposal, Auditor approves or rejects. Role separation is enforced with @PreAuthorize, and the service layer also checks initiatorId != approverId so no single user can complete disposal alone.
Every action creates an audit entry with a SHA-256 hash built from the entry data plus the previous hash. The first row anchors to GENESIS. If a stored row is modified, recomputing the chain exposes the break and the Auditor UI highlights the corrupted entry.
Assets can be assigned to a custodian or held unassigned in the vault. Custodians see only assets assigned to them. Vault Managers can assign unassigned assets, recover assets from inactive custodians, and request disposal for eligible assets. Standard transfers require sender initiation and receiver acceptance, and every ownership change writes an audit row.
Access tokens expire after 15 minutes. Refresh tokens last 7 days, are SHA-256 hashed in PostgreSQL, rotate on every use, and are revoked on logout. Angular retries one failed request after a shared refresh call, avoiding duplicate token-rotation collisions.
Every entity carries an indexed workspace_id. Spring Data repository methods include workspace scope, so demo sessions and user data are isolated at the query layer before business rules run.
| Role | Capabilities |
|---|---|
| Admin | Create, enable, disable, and reset users; view audit trail |
| Vault Manager | Create assets, assign/unassign custody, accept/reject issues, request disposal |
| Custodian | Hold assigned assets, report issues, initiate and respond to transfers |
| Auditor | Approve/reject disposal requests, verify audit integrity, inspect full audit trail |
flowchart LR
CREATE_ASSIGNED(["Create VM: assigned"]) --> ACTIVE_ASSIGNED["ACTIVE (assigned)"]
CREATE_UNASSIGNED(["Create VM: unassigned"]) --> ACTIVE_UNASSIGNED["ACTIVE (unassigned)"]
ACTIVE_ASSIGNED -->|Report Issue Custodian| ISSUE_REPORTED["ISSUE_REPORTED"]
ACTIVE_ASSIGNED -->|Transfer Initiate Custodian| IN_TRANSFER["IN_TRANSFER"]
ACTIVE_UNASSIGNED -->|Assign VM| ACTIVE_ASSIGNED
ACTIVE_ASSIGNED -->|Unassign VM if custodian inactive| ACTIVE_UNASSIGNED
ISSUE_REPORTED -->|Request Disposal VM| PENDING_DISPOSAL_ISSUE["PENDING_DISPOSAL"]
ISSUE_REPORTED -->|Reject Issue VM| ACTIVE_ASSIGNED
ACTIVE_UNASSIGNED -->|Request Disposal VM| PENDING_DISPOSAL_DIRECT["PENDING_DISPOSAL"]
PENDING_DISPOSAL_ISSUE -->|Approve Auditor four-eyes| DISPOSED["DISPOSED"]
PENDING_DISPOSAL_ISSUE -->|Reject Auditor| ISSUE_REPORTED
PENDING_DISPOSAL_DIRECT -->|Approve Auditor four-eyes| DISPOSED
PENDING_DISPOSAL_DIRECT -->|Reject Auditor| ACTIVE_UNASSIGNED
IN_TRANSFER -->|Accept Receiving Custodian| ACTIVE_ASSIGNED
IN_TRANSFER -->|Reject Receiving Custodian| ACTIVE_ASSIGNED
Assigned disposal path: Custodian reports issue -> Vault Manager accepts into disposal review -> Auditor approves or rejects. Unassigned disposal path: Vault Manager requests disposal directly -> Auditor still provides the only approval. Disposal is process-based; there is no runtime value-threshold shortcut.
Each demo workspace is seeded through the normal service flow: users, assets, custody assignments, transfer rows, issue rows, disposal approvals, notification rows, and audit entries. The seeded mix includes assigned assets, unassigned vault-held assets, one inactive-custodian recovery case, and one legitimate audit row whose stored hash is overwritten to simulate database tampering.
A disabled-by-default background simulator can generate additional demo transfers and disposal requests through the same services as normal users. It is bounded by workspace and cycle state so it cannot flood a demo session.
Approval status changes, custody transfers, audit entries, and notifications broadcast via STOMP WebSocket to workspace-scoped topics such as /topic/audit/{workspaceId} and /topic/notifications/{workspaceId}. Clients in other workspaces never receive those events.
Notifications are persistent, workspace-scoped, and deep-linkable. The backend resolves each notification target server-side, while the Angular UI waits for the destination row to render before applying focus/highlight state.
Web Audio API synthesized sounds are used for approval, transfer completion, notification receipt, and integrity alerts. No external audio assets are shipped.
Access and refresh tokens are stored in HttpOnly, Secure, SameSite=Strict cookies. The frontend never handles raw tokens. A cross-tab session guard detects when another tab changes user context and redirects stale sessions before confusing mutations can happen.
Vault Overview, Audit Trail, Admin Panel, and Approval Queue persist sort/filter state in URL query parameters. Updates use replaceUrl so refreshes keep state without polluting browser history.
- Jakarta Bean Validation on request DTOs.
- Production-only auth throttles via Bucket4j and forwarded client IP.
- HTML tag stripping before persistence.
- Password pre-hash with pepper plus bcrypt.
- Optimistic locking with
@Versionon assets. - Spring Data JPA repositories only; no raw SQL in application code.
- Admin-created users capped per demo workspace.
Asset lists and audit trails use Spring Data Pageable with stable page wrappers. PostgreSQL returns the requested slice, and Angular Material paginators trigger server requests instead of loading entire ledgers into the browser.
The Janitor runs every 6 hours and hard-deletes non-template data older than 24 hours. Each workspace cleanup runs in its own transaction and deletes in foreign-key-safe order.
The Audit Trail renders a visual hash-chain viewer for the current page. Clicking a node scrolls to the matching row, off-page entries resolve through GET /audit/{id}/page-position, and broken nodes are marked with a tampered badge.
Every major table exports to an AES-256 encrypted ZIP generated client-side with @zip.js/zip.js and Web Crypto-backed password generation. Sensitive export data is packaged in the browser and is not uploaded to a server for encryption.
+----------------------------------------------------------+
| Caddy |
| TLS termination + reverse proxy |
| /backend/* -> API:8080 /* -> UI:3000 |
+----------------+-----------------------+-----------------+
| |
+------------v-----------+ +--------v---------+
| Spring Boot 4 | | Angular 19 |
| REST Controllers | | Standalone UI |
| STOMP WebSocket | | Material |
| Scheduled Tasks | | NgRx Signals |
| Jakarta Validation | | Web Audio |
+-----+------------------+ +------------------+
|
+-----v------+
| PostgreSQL |
| 17 |
+------------+
Four-eyes approval flow:
- Vault Manager requests disposal for an eligible asset.
- Controller validates JWT cookie, role, workspace, and request state.
ApprovalServicecreates the approval row and moves the asset toPENDING_DISPOSAL.NotificationServicerecords and broadcasts reviewer notifications.- Auditor approves or rejects; the service enforces different user.
- Asset state, approval state, audit row, and STOMP broadcast complete in one business flow.
Vault is a compliance system, so relational integrity matters. Foreign keys keep custody transfers, approvals, audit entries, and users consistent. ACID transactions guarantee that an approval, asset status change, and audit entry succeed or roll back together. The hash chain depends on ordered, consistent writes.
- Indexed
workspace_idfields keep tenant-scoped queries bounded. - SHA-256 integrity verification walks audit entries once in insertion order.
@Versionoptimistic locking resolves concurrent custody changes without long-held locks.- Refresh token lookup uses indexed SHA-256 hashes.
- Server-side pagination keeps audit ledgers and asset lists browser-safe.
- STOMP pushes updates to affected workspaces instead of polling every client.
- HikariCP keeps database connections warm.
- Java 25 virtual threads reduce platform-thread pressure for blocking I/O.
Java 25
- Records for immutable DTOs.
- Sealed domain exception hierarchy.
- Pattern matching in global error handling.
- Text blocks for Flyway SQL migrations.
- Virtual threads for request handling.
Spring Boot 4.0.2
- RFC 7807
ProblemDetailvia@RestControllerAdvice. - Spring Security with JWT cookie filter.
- Spring Data JPA repositories and Flyway migrations.
- MapStruct compile-time mappers.
- Jakarta Bean Validation.
- STOMP WebSocket with workspace-scoped topics.
- Security headers, actuator health/info/metrics, and Testcontainers integration tests.
Angular 19
- Standalone components and lazy-loaded routes.
- Signals,
computed, andeffectfor reactive state. - NgRx SignalStore for auth, theme, and notification focus state.
- Angular Material with dark and warm ivory light themes.
- Functional interceptors for auth refresh and errors.
- Reactive forms, skeleton loaders, and accessible dialog flows.
SeederService writes one legitimate workflow audit row, then overwrites that row's stored hash. Auditors can click Verify Integrity to recompute the chain and reveal the corrupted row. The server verifies the full chain, while the UI can jump to off-page entries using page-position endpoints.
vault/
├── backend/
│ ├── config/ # security, rate limits, OpenAPI, WebSocket
│ ├── controller/ # auth, assets, custody, approvals, audit, admin
│ ├── dto/ # request/response contracts and page wrappers
│ ├── exception/ # sealed domain exceptions + ProblemDetail mapping
│ ├── mapper/ # MapStruct entity-to-DTO mapping
│ ├── model/ # assets, users, approvals, transfers, audit entries
│ ├── repository/ # Spring Data JPA repositories
│ ├── scheduler/ # cleanup, simulator, integrity monitor
│ ├── security/ # JWT cookies, token provider, password encoder
│ ├── service/ # domain rules, seeding, notifications, audit chain
│ ├── src/main/resources/ # application config + Flyway migrations
│ └── src/test/ # unit and integration tests with Testcontainers
│
├── frontend/
│ ├── src/app/core/ # guards, interceptors, services, stores
│ ├── src/app/features/ # landing, login, dashboard, detail, legal pages
│ ├── src/app/shared/ # dialogs, notification bell, status badges, pipes
│ ├── src/environments/ # production and development API config
│ ├── e2e/ # Playwright workflow tests
│ └── nginx.conf # SPA routing for Docker
│
├── docker-compose.yml # production stack
├── docker-compose.local.yml # local PostgreSQL
└── README.md
| Component | Technology |
|---|---|
| Framework | Spring Boot 4.0.2 / Spring Framework 7.0.3 |
| Language | Java 25 |
| Database | PostgreSQL 17 |
| ORM | Spring Data JPA + Hibernate 7.2.1 |
| Build | Gradle 9.3.1 |
| Migrations | Flyway |
| Real-time | Spring WebSocket + STOMP |
| Auth | Dual-token JWT in HttpOnly cookies |
| Validation | Jakarta Bean Validation |
| Mapping | MapStruct + Lombok |
| Testing | JUnit 5 + Mockito + Testcontainers |
| Coverage | JaCoCo XML + Codecov |
| API Docs | springdoc-openapi 3.0.1 |
| Component | Technology |
|---|---|
| Framework | Angular 19 |
| Language | TypeScript strict mode |
| UI | Angular Material + SCSS |
| State | NgRx SignalStore |
| Real-time | @stomp/stompjs |
| Testing | Karma + Jasmine + Playwright |
| Audio | Web Audio API |
| Component | Technology |
|---|---|
| Containers | Docker + Docker Compose |
| Reverse Proxy | Caddy |
| CI/CD | GitHub Actions |
| Coverage | JaCoCo + Codecov |
- Docker and Docker Compose.
- JDK 25.
- Node.js 24+ and npm.
- Angular CLI.
- WSL2 recommended on Windows.
git clone https://github.com/vladyslavm-dev/vault.git
cd vault
docker compose -f docker-compose.local.yml up -dRun the backend:
cd backend
./gradlew bootRun --args='--spring.profiles.active=dev'Run the frontend:
cd frontend
npm install
ng serveOpen http://localhost:4200 and use the Admin, Vault Manager, Custodian, or Auditor demo buttons.
- Local Swagger UI: http://localhost:8080/swagger-ui/index.html
- Local health: http://localhost:8080/actuator/health
Docker Compose deployment is represented in CI/CD and runs behind the shared Caddy edge at vault.vladyslavm.dev.
All endpoints return RFC 7807 ProblemDetail responses on error. Auth endpoints are public; all other endpoints require a valid vault_token cookie.
| Area | Endpoint Pattern |
|---|---|
| Auth | login, demo login, refresh, logout, current user |
| Assets | list, detail, create, update, assign, unassign, issue report |
| Custody | initiate transfer, accept transfer, reject transfer |
| Approvals | request disposal, pending queue, approve, reject |
| Audit | paged trail, asset history, integrity verification, page-position |
| Admin | create user, reset password, enable, disable |
| Notifications | list, unread count, mark-read, resolve target |
Three layers of automated tests cover business logic, data integrity, UI behavior, and full user journeys.
(cd backend && ./gradlew test)
(cd frontend && npx ng test --watch=false --browsers=ChromeHeadless)
(cd frontend && npx playwright test)Representative coverage:
- Approval service and integration tests for four-eyes disposal, issue rejection, and role separation.
- Asset and custody services for ownership rules, inactive-custodian recovery, and transfer state.
- Auth, admin, JWT, refresh rotation, disabled-account guards, and request validation.
- Controller suite for role-guarded endpoints and response mapping.
- Audit chain and seeder integration tests with real PostgreSQL via Testcontainers.
cd backend
./gradlew testCoverage:
./gradlew test jacocoTestReportReport: build/reports/jacoco/test/jacocoTestReport.xml
Representative coverage:
- Admin panel user management, password reset, and role filtering.
- Audit trail integrity verification, page-position navigation, and focus handoff.
- Approval queue VM/Auditor flows, issue rejection, disposal dialogs, and route sync.
- Vault overview assignment, inactive-custodian recovery, and VM-only controls.
- Shared UI specs for notifications, exports, badges, pipes, dialogs, and auth validation.
cd frontend
npx ng test --watch=false --browsers=ChromeHeadlessworkflows.spec.ts validates demo auth, ownership recovery, approvals, transfers, notifications, integrity checks, theming, and responsive behavior.
cd frontend
npx playwright testPlaywright expects the backend API at http://localhost:8080.
cd frontend
npx ng build --configuration productionOne GitHub Actions workflow follows the pattern: Test -> Build -> Push -> Deploy.
| Workflow | Trigger | Target |
|---|---|---|
deploy-docker.yml |
Push to main |
Server via Docker Compose |
The workflow runs backend and frontend tests, builds multi-stage Docker images, pushes to Docker Hub, then deploys through SSH.
MIT License. Copyright (c) 2026 Vladyslav Marchenko
See LICENSE for details.
Vladyslav Marchenko
- GitHub: @vladyslavm-dev
- Website: vladyslavm.dev





