This long-form deep dive unpacks how ForkMyFolio’s backend turns a simple personal portfolio into a multi-user, slug-driven, production-ready platform built on Java 21 and Spring Boot 3.
What You’ll Learn
-
Layered Spring Boot Architecture
How controllers, services, repositories, DTOs, and mappers fit together.
-
Multi-User, Slug-Driven Design
Designing users, UUIDs, and slugs so one backend can host many portfolios safely.
-
Security and Roles
JWT auth with refresh cookies, OAuth2 login, and role-based zones.
-
Portfolio Features
PDF / Markdown / vCard exports, analytics, contact messages, and backups.
1. From “Just a Portfolio” to a Small SaaS
Most developer portfolios start as a static page. Then reality happens: you want a projects gallery, a blog, analytics,
a downloadable PDF resume, maybe even multi-user support for friends or clients. At that point you’re quietly building
a small SaaS whether you meant to or not.
ForkMyFolio embraces this reality from day one. Instead of hard-coding a single person’s site,
the backend is designed as a multi-user, slug-driven portfolio backend with:
- Multiple users, each with their own portfolio and public slug
- A clear separation between public, authenticated, and admin APIs
- JWT authentication with secure refresh tokens in HttpOnly cookies
- Dynamic portfolio configuration and PDF template selection
- Non-intrusive visitor analytics and robust backup / restore flows
The frontend is a single-page application (SPA). The backend is a clean, versioned REST API that enforces the rules,
handles security, and keeps the data model consistent.
2. High-Level Architecture: A Clean, Layered Spring Boot Core
Under the hood, ForkMyFolio is a classic layered Spring Boot application with a strong opinion about boundaries.
The documentation in TECHNICAL_DOCUMENTATION.md and ARCHITECTURE_RULES.md formalizes this into
project-wide rules, but the core idea is simple:
Controllers handle HTTP and DTOs. Services handle business logic and entities. Repositories handle persistence. Never mix these responsibilities.
ForkMyFolio Architecture Rules
2.1 Technology Stack
| Concern | Technology |
|---|---|
| Language & Runtime | Java 21 |
| Application Framework | Spring Boot 3 (Web, Security, Validation, OAuth2 Client) |
| Persistence | Spring Data JPA, MySQL (with Flyway), H2 for development |
| Security | Spring Security, JWT via jjwt, OAuth2 Login |
| API Documentation | Springdoc OpenAPI (Swagger UI) |
| PDF Generation | iText 7 |
| Packaging & Deployment | Fat JAR via Maven, Dockerfile + docker-compose |
2.2 Package Layout
The code lives under the root package com.forkmyfolio and is split into focused subpackages:
-
configSpring configuration such as
SecurityConfig, OpenAPI configuration, etc. -
controllerREST controllers grouped by resource: portfolios, auth, admin, user profile.
-
model&repositoryJPA entities and Spring Data JPA repositories that map to the underlying database.
-
service&mapperBusiness logic and manual DTO mappers that keep the API surface explicit and stable.
-
securityJWT filters, authentication entrypoints, OAuth2 integration, and custom user details.
3. Layered Design and One-Way Data Flow
The value of the architecture is not just in package names; it’s in how requests actually move through the system.
3.1 The Three Layers
- Controller Layer —
@RestControllerclasses that speak HTTP and JSON, accept and return DTOs, and never call the database directly. - Service Layer —
@Servicecomponents that encapsulate business rules, operate on JPA entities, and orchestrate multiple repositories. - Repository Layer —
@Repositoryinterfaces powered by Spring Data JPA, responsible only for persistence.
A typical request flows like this:
Request Flow
HTTP Request (JSON DTO) → Controller → Service → Repository → Database → Service → Controller →
HTTP Response (DTO wrapped in ApiResponseWrapper)
Even for file-heavy features like PDF or vCard downloads, controllers remain thin: they set headers and content type,
while services prepare the domain data and call specialized generators.
4. Designing for Multi-User Slugs and UUIDs
ForkMyFolio V2 pivots from a single-portfolio mindset to a true multi-user platform. That decision ripples through the
data model, URL design, and security rules.
4.1 The User Entity as the Anchor
The User entity in com.forkmyfolio.model is more than a login record; it is the root of a user’s
entire portfolio universe:
- Internal primary key: numeric
id(never exposed publicly). - External identifier:
uuid, used in APIs and backups. - Public routing identifier:
slug, used in URLs like/api/v1/portfolios/{slug}. - Authentication details:
email,password,AuthProvider,providerId. - Lifecycle flags:
active,emailVerified,termsAcceptedAt,termsVersion. - Relations to portfolio content: profile, projects, experiences, skills, qualifications, testimonials, contact messages, settings, refresh tokens.
- Roles via
Set<Role>, mapped to Spring Security authorities likeROLE_USER,ROLE_ADMIN.
Because User implements UserDetails, it plugs directly into Spring Security. The role set becomes a
collection of GrantedAuthority instances, which SecurityConfig then uses when enforcing access rules.
4.2 UUIDs and Slugs as Public Contracts
The architecture rules explicitly require that raw database IDs never appear in the public API. Instead:
- All public-facing references use UUIDs for entities.
- Public portfolio routing uses a human-readable slug per user.
This prevents simple enumeration attacks (guessing /users/1, /users/2, etc.) and makes URLs
stable across database migrations.
4.3 PortfolioService.getPublicPortfolioUserBySlug
Public portfolio endpoints never query the User table directly. They instead call a single, well-defined
service method:
User getPublicPortfolioUserBySlug(String slug)
This method guarantees two things before any data is returned:
- The user exists and is active, otherwise a
ResourceNotFoundExceptionis thrown. - The user’s portfolio is public, otherwise a
PermissionDeniedExceptionis thrown.
By centralizing that logic, the platform makes it impossible for a controller to accidentally leak private portfolios
just by forgetting a filter.
5. Public, User, and Admin API Zones
The API surface is deliberately split into three access zones, each with different guarantees and responsibilities.
5.1 Public Zone
Base paths like:
/api/v1/portfolios/{slug}– portfolio shell and section summaries/api/v1/portfolios/{slug}/projects,/experience,/skills,/qualifications,/testimonials/api/v1/portfolios/{slug}/pdf,/markdown,/vcardfor exports/api/v1/portfolios/{slug}/contact-messagesfor contact form submissions/api/v1/settings,/api/v1/settings/pdf-templatesfor global public settings/api/v1/policies/...for privacy policy and terms of service
In SecurityConfig, these routes are explicitly marked permitAll() for the relevant methods (GET for
most, POST for contact messages). That makes the public zone browseable without credentials while keeping
sensitive actions behind auth.
5.2 Authenticated User Zone
Authenticated users operate primarily under /api/v1/me and /api/v1/auth:
/api/v1/auth/register,/login,/refresh-token,/logout/api/v1/mefor account details and password changes/api/v1/me/profilefor portfolio profile/api/v1/me/projects,/experiences,/skills,/qualifications,/testimonialsfor content management/api/v1/me/settingsfor per-user configuration/api/v1/me/contact-messagesas a personal inbox/api/v1/me/backupfor self-service backup and restore
These endpoints enforce ownership: a user can only touch their own portfolio data, even if they know someone else’s
UUID or slug.
5.3 Admin Zone
Anything under /api/v1/admin is restricted to ROLE_ADMIN users:
- User management (
/users) - Global settings (
/settings) - Site-wide contact message moderation
- Visitor statistics and analytics (
/stats) - Full-system backup and restore, including per-user restores
This zone is designed with operational tasks in mind: migrations, incident recovery, and platform-wide tuning.
6. Security: JWT, Refresh Cookies, and OAuth2
Security is not an afterthought in ForkMyFolio; it’s baked into the architecture. The combination of JWTs,
HttpOnly cookies, and Spring Security makes authentication predictable and auditable.
6.1 JWT and Refresh Flow
The application uses a dual-token strategy:
- Access token — short-lived JWT sent in the
Authorization: Bearer <token>header. - Refresh token — longer-lived, stored in an HttpOnly, Secure cookie.
When a user authenticates via /auth/login or /auth/register, the backend returns:
- A JSON payload with the access token.
- A
Set-Cookieheader that stores the refresh token in an HttpOnly cookie.
Once the access token expires, the frontend calls /auth/refresh-token. The browser automatically sends the
refresh cookie, the backend validates it, issues a new access token, and rotates the refresh token (rolling tokens).
6.2 SecurityConfig in Practice
The SecurityConfig class wires everything together:
- Disables CSRF for the pure JSON API surface.
- Sets session creation policy to stateless.
- Registers a
JwtAuthenticationFilterbefore the standard username/password filter. - Configures an
JwtAuthenticationEntryPointto handle unauthorized errors consistently. - Wires up OAuth2 login using
CustomOAuth2UserServiceand custom success/failure handlers.
6.3 CORS and Cookies
Given that the frontend often runs on a different origin (e.g., http://localhost:3000 in development),
CORS and cookie configuration are crucial. The corsConfigurationSource bean:
- Reads allowed origins from
app.cors.allowed-origins. - Enables credentials so the refresh cookie can be sent.
- Whitelists the necessary headers and exposes
Content-Dispositionfor file downloads.
7. DTOs, Manual Mappers, and Response Wrappers
Rather than exposing JPA entities directly over the wire, ForkMyFolio embraces DTOs everywhere.
The mapper package contains hand-written mappers that translate between
entities and API-friendly representations.
7.1 Why Manual Mappers?
- Explicitness — you see exactly which fields are exposed.
- Contextual enrichment — experience and project DTOs can include computed views like skill proficiency.
- Controlled evolution — as the API evolves, mappers make it obvious what changed and why.
7.2 A Uniform Response Envelope
Every controller returns an ApiResponseWrapper<T>, so the frontend always deals with the same shape.
{
"status": "success",
"data": { ... },
"errors": []
}
or, on failure:
{
"status": "fail",
"data": null,
"errors": [
{ "field": "email", "message": "Email is already in use" }
]
}
8. Portfolio Content, Settings, and Exports
The heart of ForkMyFolio is the portfolio itself. The backend models portfolio data with a set of focused entities:
- PortfolioProfile — name, headline, summary, social links.
- Project — title, description, tech stack, links, highlights.
- Experience — work or volunteer history.
- Qualification — education and certifications.
- UserSkill — user-specific skill relations often mapped to platform-wide skills.
- Testimonial — quotes from collaborators and clients.
Public /portfolios/{slug}/... endpoints aggregate these into read-optimized DTOs, enforcing visibility
and sort order. Hidden or draft content stays private.
8.1 Dynamic Settings and Feature Flags
Settings exist at two levels:
- Global settings controlled by admins.
- User settings that override specific behaviors per portfolio.
The /api/v1/me/settings endpoint returns the effective settings, combining both sources so the frontend
doesn’t need to merge them manually.
8.2 PDF, Markdown, and vCard Downloads
Visitors can export a portfolio in several formats:
- PDF — a resume-style representation powered by iText 7.
- Markdown — a portable document format ideal for GitHub READMEs or static site generators.
- vCard — a quick import format for contact apps.
Each export endpoint follows the same pattern: controller handles HTTP details, service composes the data and calls
a generator, then the controller streams the result.
9. Analytics, Contact Messages, and Backups
9.1 Non-Intrusive Analytics
ForkMyFolio tracks key events (page views, project views, engagement) without resorting to invasive tracking.
The goal is to give portfolio owners actionable insights while respecting visitor privacy.
9.2 Contact Messages
Contact messages start in the public zone:
- Visitors submit a message via
/api/v1/portfolios/{slug}/contact-messages. - The backend resolves the slug, stores a
ContactMessagelinked to the owning user. - The owner reads and manages their inbox via
/api/v1/me/contact-messages.
9.3 Backup and Restore
The platform treats portfolios as first-class data that users should be able to take with them:
- Users can download a full JSON backup of their portfolio and restore from it later.
- Admins can generate system-wide backups, restore single users, or restore the entire platform in a disaster scenario.
10. Profiles, Environment Variables, and Docker
Environment-driven configuration ensures the same codebase runs safely in development and production.
- Development profile — H2 in-memory database, developer-friendly CORS, and a non-sensitive JWT secret.
- Production profile — MySQL/PostgreSQL, strict CORS, secure cookies, and a strong JWT secret from the environment.
Running locally is as simple as mvn spring-boot:run. For production, a Docker image wraps the app with the
right environment variables for database, JWT, and CORS settings.
11. Lessons Learned
ForkMyFolio’s backend shows what happens when you treat a “simple portfolio” as a platform from the start:
- Separating public, user, and admin zones keeps permissions comprehensible.
- Leaning into DTOs and manual mappers makes the API easy to evolve safely.
- Using UUIDs and slugs avoids migration headaches and security pitfalls.
- Building backup and export paths in early makes users trust the system with their data.
- Taking security seriously (JWTs, refresh cookies, OAuth2, proper CORS) makes frontend integration a lot less painful.
If you’re designing your own multi-user app on Spring Boot, ForkMyFolio offers a concrete blueprint: start with
clean boundaries, respect your users’ data, and let your backend grow from “my portfolio” into “our platform” without
losing control.