Architecture
This page describes how SnapCart's components fit together at a system level, including request flow, security model, data layer, and key design decisions.
System Overview
┌─────────────────────────────────────────────────────────────────┐
│ INTERNET │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────▼────────┐
│ Nginx / CDN │ Reverse proxy + SSL termination
└───────┬────────┘
┌─────────────┴──────────────┐
│ │
┌────────▼────────┐ ┌─────────▼────────┐
│ React Frontend │ │ Spring Boot API │
│ (Vite SPA) │────────▶│ Port 8080 │
│ Port 5173 │ HTTP └─────────┬─────────┘
└─────────────────┘ │
┌────────▼────────┐
│ MySQL 8.0 │
│ Port 3306 │
└─────────────────┘
Component responsibilities:
| Component | Role |
|---|---|
| Nginx | SSL termination, static file serving, reverse proxy to backend API |
| React SPA | Customer storefront and admin panel UI — single-page application |
| Spring Boot API | All business logic, authentication, data access |
| MySQL | Persistent storage for all store data |
Request Flow
Storefront Page Load
- Browser requests
https://yourstore.com/→ Nginx serves the React SPA'sindex.html. - React Router renders the homepage component.
- React Query fires
GET /api/v1/home→ Nginx proxies to Spring Boot on port 8080. - Spring Boot queries MySQL via JPA/Hibernate, assembles the DTO, and returns JSON.
- React renders the homepage sections using the returned data.
Admin API Request
- Admin user performs an action (e.g., updates an order status).
- React component calls
PUT /api/v1/admin/orders/{id}/statuswithAuthorization: Bearer <jwt>. - Spring Boot's
JwtAuthenticationFilterintercepts the request:- Validates the JWT signature against the configured secret.
- Extracts
userId,role, and sets the Spring Security context.
- Spring Security evaluates
@PreAuthorize("hasAuthority('order.update')")on the controller method. - If authorized, the Service layer executes the update and logs it to the Activity Log.
- A JSON response is returned. React Query invalidates the
orderscache to trigger a refetch.
Authentication and Authorisation
SnapCart uses stateless JWT authentication — no server-side session state is maintained.
Login Flow
Client Spring Boot
│ │
├─── POST /api/v1/admin/auth/login ──►│
│ { email, password } │
│ ├── Validate credentials (BCrypt)
│ ├── Load user + permissions from DB
│ ├── Sign JWT with 24h expiry
│◄── { token, user } ──────────────┤
│ │
├─── GET /api/v1/admin/products ───►│
│ Authorization: Bearer <token> │
│ ├── JwtAuthFilter decodes token
│ ├── Load SecurityContext
│ ├── Check @PreAuthorize
│◄── { data: [...] } ──────────────┤
JWT Payload
{
"sub": "superadmin@demo.io",
"userId": 1,
"role": "SUPER_ADMIN",
"iat": 1716000000,
"exp": 1716086400
}
Token expiry defaults to 24 hours. The frontend stores the token in localStorage and attaches it to every API request via an Axios request interceptor.
RBAC Enforcement
Permission enforcement is dual-layer:
-
Frontend (UI gating): The admin panel reads the user's permission list from the login response and renders only the sidebar items the user is allowed to access. Attempting to navigate to a hidden route redirects to the dashboard.
-
Backend (API enforcement): Every admin endpoint uses
@PreAuthorize("hasAuthority('module.action')"). Even if a user bypasses the UI and calls the API directly, the backend rejects unauthorized requests with403 Forbidden.
Example permission check chain:
// Controller
@PreAuthorize("hasAuthority('product.create')")
@PostMapping("/admin/products")
public ResponseEntity<ApiResponse> createProduct(@RequestBody ProductRequest req) { ... }
// Spring Security evaluates against the user's GrantedAuthority list
// loaded from DB at login time and embedded in the JWT SecurityContext
Dual-Portal Design
The single React SPA contains two distinct portals with separate routing trees:
| Portal | Route Prefix | Auth Guard |
|---|---|---|
| Customer Storefront | / | Optional (guest browsing allowed, account required for orders/wishlist) |
| Admin Panel | /admin | Required — JWT + admin role. Redirects to /admin/login if missing |
Both portals share the same Axios instance, Zustand store, and React Query client, but have separate route definitions and layout components.
Database Layer
Migration Strategy
Flyway manages all schema changes. Migrations run in version order on every application startup:
V1__create_users_table.sql
V2__create_roles_table.sql
V3__create_permissions_table.sql
...
V305__add_flash_deals.sql
Rules for developers:
- Never modify an existing migration file.
- Always create a new file:
V{n+1}__short_description.sql. - Flyway will apply pending migrations automatically on next startup.
ORM Stack
- Hibernate 6 as the JPA provider.
- Spring Data JPA repositories provide standard CRUD without boilerplate.
- Custom queries use
@Query(JPQL or native SQL) where repository methods are insufficient. - MapStruct handles entity ↔ DTO conversion at compile time — no runtime reflection.
Entity Relationships
Key table relationships:
users ─── roles ─── role_permissions ─── permissions
│
(enforced by @PreAuthorize)
products ─── product_variants ─── order_items ─── orders ─── customers
│
├── categories (self-referential tree for sub-categories)
├── brands
└── product_attributes ─── attributes ─── attribute_values
shipping_zones ─── shipping_rates ─── orders
coupons ─── coupon_usages ─── orders
flash_deals ─── flash_deal_products ─── products
File Storage
Uploaded images and documents are handled by a pluggable storage layer:
Local Storage (development default)
Files are saved to ./storage/{year}/{month}/{filename}. The backend serves them at /storage/** as a static resource mapping.
snapcart:
storage:
type: local
local:
upload-dir: ./storage
AWS S3 (production recommended)
Files are uploaded directly to an S3 bucket. The bucket's public URL is stored in the database.
snapcart:
storage:
type: s3
s3:
bucket: your-bucket
region: ap-south-1
The storage implementation is injected as a StorageService Spring bean — controllers and services call storageService.upload(file) and receive back a URL, regardless of which backend is configured.
API Design
URL Structure
/api/v1/{resource} — public storefront endpoints
/api/v1/admin/{resource} — admin-only endpoints (require JWT + permission)
Response Envelope
All responses follow a consistent JSON structure:
{
"success": true,
"message": "Product created successfully",
"data": { ... }
}
Error response:
{
"success": false,
"message": "Validation failed",
"errors": [
{ "field": "name", "message": "Name is required" }
]
}
HTTP status codes: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity, 500 Internal Server Error.
Pagination
List endpoints accept standard query parameters:
| Parameter | Default | Description |
|---|---|---|
page | 0 | Zero-based page index |
size | 20 | Items per page |
sort | varies | Field name, optionally ,asc or ,desc |
Paginated responses include metadata:
{
"success": true,
"data": {
"content": [...],
"totalElements": 1042,
"totalPages": 53,
"page": 0,
"size": 20
}
}
Frontend State Management
| Layer | Tool | Used For |
|---|---|---|
| Server state | React Query | API data, caching, background refetch, mutations |
| Global UI state | Zustand | Auth session, cart, language, currency, UI toggles |
| Local component state | useState / useReducer | Modal open/close, pagination, local form state |
| Form state | React Hook Form + Zod | Form validation, field errors, submit handling |
Caching Strategy
React Query caches all GET responses by query key. Cache is invalidated after mutations using onSuccess callbacks:
useMutation({
mutationFn: updateOrderStatus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] });
queryClient.invalidateQueries({ queryKey: ['order', orderId] });
},
});
Internationalisation
- Frontend:
i18next+react-i18next. Translation keys are JSON files loaded per locale. The active language is stored in Zustand and persisted tolocalStorage. - Backend:
ICU4Jfor formatted messages (plurals, gender, date/time formatting in notification templates and email bodies). - Database: Translation strings are stored in the
translationstable, editable via the admin Localization section. - RTL support: Arabic and Hebrew languages trigger a
dir="rtl"attribute on the<html>element, mirroring the layout automatically via CSS logical properties.
Related
- Tech Stack — Full list of libraries and versions
- Deployment — Production server setup with Nginx and Docker
- API Reference — Full endpoint documentation