Eruscent is a production-grade, multi-tenant B2B SaaS platform connecting university students with peer tutors. It's an institutional-scale system: universities are tenants, campuses are subdivisions, departments are the unit of academic scope, and every user belongs to that hierarchy. The entire system enforces this boundary at every layer — not by convention, but by design. Engineered and shipped end-to-end, solo.
Core Architecture: Multi-Tenant Hierarchy
The tenant model is four levels deep: University → Campus → Department → User. This isn't just a data model — it's an enforcement contract. Every service method, every repository query, every CORS-allowed origin is scoped to this hierarchy. A student in Department A cannot see, access, or interact with data in Department B, even within the same university.
Identity: JWKS, JIT Provisioning & Self-Healing
Authentication is fully decoupled from the application. Clerk acts as the identity provider, issuing JWTs signed with RSA keys published at a public JWKS endpoint. The backend verifies tokens locally — the JWKS URI is used once at startup to fetch public keys, then cached. No round-trips to Clerk on every request.
- Just-In-Time provisioning: When a user authenticates via Clerk but has no record in the local Postgres database,
userService.createEmptyUserFromClerk()auto-provisions their account in the same request. No separate registration step required. - Identity self-healing: On every authenticated request, if the JWT claims contain a different name or profile picture URL than what's stored in the database, the backend automatically updates the local record. The local state never drifts from the identity provider.
- Dual role aggregation: A principal who is both a student and an approved tutor gets both
ROLE_STUDENTandROLE_TUTORmapped into their Spring Security context simultaneously. Method-level security annotations handle the rest.
Institutional Domain Gating
Registration is restricted in real time. You can't create an account on Eruscent unless your email domain is on the approved list for your institution. The EmailDomainService implements a 3-tier fallback strategy:
- Tier 1 — Database: Check the
allowed_email_domainstable for active domain entries matching the registering institution. This is the primary control — super admins add or remove domains through the admin portal in real time. - Tier 2 — Environment variable: If no active DB domains exist (bootstrap scenario), fall back to the
app.registration.allowed-domainsenvironment property for initial seeding. - Tier 3 — Deny all: If neither tier resolves, registration is denied by default. The platform is closed until explicitly opened. There is no default-open state.
Dual Session Architecture & Conflict Engine
Eruscent supports two distinct tutoring session modes, both with real-time conflict prevention and state lifecycle management.
- 1-on-1 Private Sessions: Calendar-based time slot booking with direct tutor acceptance or rejection. Pending requests auto-expire after 15 minutes. Single student per booking, enforced at the service layer.
- Group Session Lobbies: Multi-student group sessions with dynamic capacity bounds (enrolled vs. max), locked group pricing (50% rate multiplier), and an hourly low-attendance watchdog that fires JavaMailSender alerts when enrollment falls below threshold.
- Schedule Conflict Engine: Both session types validate against existing bookings at the time of request. Overlap detection runs before any write reaches the database.
Concurrency: Optimistic Locking Guard
During high-demand registration windows — a popular tutor opens a group lobby, multiple students try to enroll simultaneously — the database faces concurrent mutation attempts on the same record. Eruscent handles this with JPA optimistic locking:
- Each enrollment-capable entity carries a
@Versionfield managed by JPA. - When two concurrent transactions attempt to modify the same record, the first one succeeds and increments the version. The second one reads a stale version and throws
ObjectOptimisticLockingFailureException. - The
GlobalExceptionHandlercatches this exception and maps it to HTTP 409 Conflict with a structured error body. The client shows the user a clear message and prompts a retry — rather than silently corrupting the enrollment count.
Zero-Trust Peer Chat
The in-session messaging system doesn't trust the request alone. Every chat access attempt goes through validateUserAccess in MessageService:
- 1-on-1 sessions: Only the session's student and tutor can access the chat thread. Principal is verified against both participants on every message fetch.
- Group lobbies: Access requires the principal to be the tutor or an actively enrolled student — not a student who was previously enrolled and then cancelled. Enrollment status is checked at message-fetch time, not at login time.
- Instant revocation: When a student cancels their group lobby enrollment, they lose chat access on the next request. There is no grace period.
Security: OWASP Hardening & Rate Limiting
The backend was subjected to a comprehensive security engineering audit covering the OWASP Top 10. Key controls implemented:
- Tamper-evident audit pipeline: Every security event is logged asynchronously via the
auditExecutorthread pool. Before persistence, logs are PII-scrubbed with regex redaction, sanitized against XSS and CRLF injection, and signed with HMAC-SHA256. A tampered log entry is detectable. - Multi-subdomain CORS matrix: CORS allowed origins are configured per campus subdomain — not globally open, not wildcard. Cross-origin requests from unlisted origins are rejected at the filter chain level.
- Structured exception contract: The
GlobalExceptionHandler(@RestControllerAdvice) enforces RFC 7807-style error responses across all endpoints. Internal stack traces never reach the client. Every error has astatus,error,message, and ISO-8601timestamp.
| Profile | Protected Endpoints | Limit | Action |
|---|---|---|---|
| Auth | /api/v1/auth/**, login, register |
5 req / min per IP | HTTP 429 |
| Public Contact | /api/v1/public/contact |
2 req / hr per IP | HTTP 429 |
| State Mutation | All POST, PUT, DELETE, PATCH |
60 req / min per IP | HTTP 429 |
| Read Bypass | All GET and OPTIONS preflight |
Unthrottled | Allowed |
Analytics & Caching Architecture
- Spring @Cacheable layer: Global KPI aggregations and university heatmap node trees are cached in memory. When a super-admin dashboard reloads, it hits the cache — not the database. High-traffic admin reloads don't trigger cascading SQL aggregation queries across the entire multi-tenant dataset.
- Institutional analytics: Department heads see session volume trends, bottleneck subject areas, tutor coverage ratios, and student attendance rates — all scoped to their department. Aggregate queries are backed by the Spring @Cacheable layer to handle concurrent admin sessions.
- Dynamic JPA Criteria API search: The tutor marketplace uses
TutorProfileSpecifications.java— a runtime predicate builder over campusId, searchTerm (OR across name/bio/subject/course code), maxPrice, and minRating. Queries usequery.distinct(true)to prevent duplicates from JOIN paths. - Timezone-aware gamification streaks: Daily activity streak calculation converts UTC session timestamps to the user's local timezone (e.g.,
Asia/Manila,America/New_York) before comparing dates. A streak break in Manila isn't a streak break in New York.
Background Engines & Automated Maintenance
- Thread pool isolation: Two dedicated pools —
auditExecutor(2 core / 5 max / 100 queue depth) for HMAC audit log persistence, andtaskExecutor(5 core / 10 max / 500 queue depth) for transactional email via JavaMailSender. Audit logging never competes with email for threads. - Nightly DatabaseCleanupService: Scheduled cron purges expired time slots, auto-completes sessions that should have ended, and applies audit log retention policies.
- GroupSessionScheduler watchdog: Runs hourly. If a group session's enrollment drops below the minimum attendance threshold, it fires a low-attendance alert email to the tutor and department head.
- Resilient Axios client: The Next.js frontend uses a custom Axios interceptor with exponential backoff retries on transient errors (502, 503, 504, 429, timeouts) — max 2 retries with a 1-second base delay. Retries are transparent to the user; exhausted retries surface a structured error toast.
DevOps: CI/CD Pipeline
- GitHub Actions — Integration CI: On every push or PR to main, spins up a live PostgreSQL 16 service container, compiles Java 21, and runs the full integration test suite against a real database.
- GitHub Actions — Weekly Security Audit: Automated cron (0 0 * * 0) runs
npm audit --audit-level=highon the frontend andmvn clean verifyon the backend. Dependency vulnerabilities are caught before they reach production. - Flyway schema migrations: Database structure evolves through versioned, reproducible migration scripts. No manual DDL in a shared environment.
- OpenAPI 3.0 / Swagger UI: The backend auto-generates interactive API documentation via
springdoc-openapi-starter-webmvc-ui— the spec is always in sync with the code, not maintained separately.
Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend framework | Next.js 16 App Router | Server & client components, multi-role dashboards |
| Styling | Tailwind CSS v4 | Utility-first design system |
| Backend framework | Spring Boot 3.4 | OWASP-hardened REST API, security filter chain |
| Language | Java 21 | Virtual threads, records, pattern matching |
| ORM | Spring Data JPA + Hibernate | Criteria API specifications, optimistic locking |
| Database | PostgreSQL 16 | Relational integrity, Flyway migrations |
| Schema migrations | Flyway | Versioned, reproducible schema evolution |
| Authentication | Clerk + JWKS | Stateless JWT verification with local key cache |
| Rate limiting | Bucket4j | Per-IP token bucket with configurable profiles |
| API documentation | OpenAPI 3.0 / Swagger UI | Auto-generated, always in sync with code |
| CI/CD | GitHub Actions | Live Postgres 16 container tests + weekly security audits |
| JavaMailSender / SMTP | Transactional email: session alerts, low-attendance notifications |
Notes
The multi-tenant hierarchy sounds simple on paper — university, campus, department, user — but enforcing it consistently across 28 REST API endpoints, 4 primary role types (Student, Tutor, Department Admin, Super Admin), and a caching layer that aggregates across tenants without leaking data between them required treating the hierarchy as a first-class architectural primitive, not a filter condition bolted on at the end.
The hardest single decision was the optimistic locking guard. The tempting alternative is pessimistic locking — lock the row, wait, write. It's simpler to reason about. But under the enrollment load a popular tutor's lobby generates at the moment it goes live, pessimistic locking turns into a queue backed by a timeout, and users get errors that feel like bugs rather than honest "you got there second" responses. The 409 Conflict + structured error message is the honest design: it tells the client exactly what happened and why.