Skip to main content

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:

ComponentRole
NginxSSL termination, static file serving, reverse proxy to backend API
React SPACustomer storefront and admin panel UI — single-page application
Spring Boot APIAll business logic, authentication, data access
MySQLPersistent storage for all store data

Request Flow

Storefront Page Load

  1. Browser requests https://yourstore.com/ → Nginx serves the React SPA's index.html.
  2. React Router renders the homepage component.
  3. React Query fires GET /api/v1/home → Nginx proxies to Spring Boot on port 8080.
  4. Spring Boot queries MySQL via JPA/Hibernate, assembles the DTO, and returns JSON.
  5. React renders the homepage sections using the returned data.

Admin API Request

  1. Admin user performs an action (e.g., updates an order status).
  2. React component calls PUT /api/v1/admin/orders/{id}/status with Authorization: Bearer <jwt>.
  3. Spring Boot's JwtAuthenticationFilter intercepts the request:
    • Validates the JWT signature against the configured secret.
    • Extracts userId, role, and sets the Spring Security context.
  4. Spring Security evaluates @PreAuthorize("hasAuthority('order.update')") on the controller method.
  5. If authorized, the Service layer executes the update and logs it to the Activity Log.
  6. A JSON response is returned. React Query invalidates the orders cache 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:

  1. 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.

  2. 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 with 403 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:

PortalRoute PrefixAuth Guard
Customer Storefront/Optional (guest browsing allowed, account required for orders/wishlist)
Admin Panel/adminRequired — 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

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:

ParameterDefaultDescription
page0Zero-based page index
size20Items per page
sortvariesField name, optionally ,asc or ,desc

Paginated responses include metadata:

{
"success": true,
"data": {
"content": [...],
"totalElements": 1042,
"totalPages": 53,
"page": 0,
"size": 20
}
}

Frontend State Management

LayerToolUsed For
Server stateReact QueryAPI data, caching, background refetch, mutations
Global UI stateZustandAuth session, cart, language, currency, UI toggles
Local component stateuseState / useReducerModal open/close, pagination, local form state
Form stateReact Hook Form + ZodForm 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 to localStorage.
  • Backend: ICU4J for formatted messages (plurals, gender, date/time formatting in notification templates and email bodies).
  • Database: Translation strings are stored in the translations table, 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.