This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Full-stack PDF manipulation platform. Stateless Spring Boot REST API + React SPA, with JWT auth via HttpOnly cookies, file storage on disk, and PDF processing via server-side libraries.
# Prerequisites: set JAVA_HOME to Java 21
export JAVA_HOME="C:\Users\omrio\scoop\apps\openjdk21\21.0.2-13" # Windows example
cd backend
./gradlew bootRun # Run backend (port 8080)
./gradlew build # Build
./gradlew test # Run all tests (no DB required — unit tests only)
./gradlew test --tests "com.crystalpdf.backend.service.WatermarkServiceTest" # Single test class
./gradlew test --tests "com.crystalpdf.backend.service.*" # All service tests
./gradlew cleanAll tests are unit tests and run without any external services (no DB, no server needed).
Backend test coverage (backend/src/test/):
helper/PdfTestHelper— Creates minimal valid PDFs in memory for use by all PDF testsservice/JwtServiceTest— Token generation, extraction, validation, expiryservice/FileEncryptionServiceTest— AES-GCM encrypt/decrypt round-tripsservice/AuthServiceTest— Register, login, changePassword, deleteAccount (all with Mockito mocks)service/StorageServiceTest— store (PDF validation, size limits, sanitize), load, delete (uses@TempDir)service/MergeServiceTest— Merge 2–3 PDFs, error casesservice/SplitServiceTest— Extract pages, out-of-range handlingservice/RotateServiceTest— Rotate 90/180/270, multi-page, accumulationservice/DeletePagesServiceTest— Delete pages, prevent deleting all pagesservice/WatermarkServiceTest— All 5 positions, rotation variantsservice/PageNumberServiceTest— All 6 positions, all 4 formats
Frontend test coverage (frontend/src/):
store/useAppStore.test.ts— activeTool, auth (setAuth/clearAuth), theme togglestore/useToastStore.test.ts— addToast, removeToast, auto-dismiss after 6slib/api.test.ts— apiFetch: successful responses, 401 handling, redirect suppression on /loginoverlays/WatermarkOverlay.test.tsx— Rendering, scaling, rotation negation, all positionsoverlays/PageNumberOverlay.test.tsx— All formats, startNumber offset, scaling, all positions
Test setup files:
frontend/vitest.config.ts— Vitest config (jsdom environment, global APIs)frontend/src/test/setup.ts— Imports@testing-library/jest-dommatchers
cd frontend
npm install
npm run dev # Dev server (port 5173)
npm run build
npm run preview
npm test # Run all tests (Vitest, no server needed)
npm run test:watch # Run tests in watch mode
npm run test:ui # Open Vitest UI in browserdocker-compose up postgres -d # Start only DB (recommended for local dev)DB: crystalpdf | User: crystalpdf | Password: crystalpdf_secure_password | Port: 5432
docker-compose up -d # All services (postgres + backend + frontend/nginx)
docker-compose logs -f backend
docker-compose down
docker-compose down -v # Also deletes DB volumeFrontend at http://localhost (Nginx). Backend accessible via Nginx reverse proxy.
com.crystalpdf.backend
├── config/ SecurityConfig (JWT filter, CORS, CSRF disabled), CorsConfig
├── controller/ AuthController, DocumentController, WorkspaceToolController,
│ CompressController, MergeController, SplitController, etc.
├── service/ AuthService, JwtService, StorageService, and one service per tool
├── entity/ User, Document (JPA)
├── repository/ UserRepository, DocumentRepository (Spring Data JPA)
├── security/ JwtService (token gen/validate), JwtAuthFilter (per-request)
├── exception/ GlobalExceptionHandler
└── dto/ Request/response objects
Key backend behaviors:
StorageService.store()validates PDF magic bytes (%PDF) before saving, generates UUID filenames, stores at{STORAGE_PATH}/{userId}/{uuid}.pdf, createsDocumentrecord in DB.- All tool endpoints are under
WorkspaceToolControllerat/api/documents/{id}/tools/{tool}. Each tool loads the original file, processes it, and creates a newDocumentrecord (does not mutate the original). AnnotationFlattenServicetakes normalized (0–1) coordinates from the frontend and uses PDFBox to bake pen/highlight/text annotations into a new PDF.- Compression uses Ghostscript via
ProcessBuilderwith a timeout. GlobalExceptionHandlermaps domain exceptions to HTTP status codes.
Pages: LoginPage, RegisterPage, WorkspacePage (main editor)
State (Zustand):
useAppStore: active tool, current document ID, user email — persisted to localStorageuseToastStore: toast notifications
PDF Viewer:
PdfViewerintegrates PDF.js; renders pages, handles zoom, stores current password for encrypted PDFsPageThumbnailStrip: collapsible right panel with page thumbnailsWorkspaceToolPanel: sliding panel for tool options (split, compress, OCR, protect, etc.)
Annotation system:
useAnnotationshook stores strokes and text boxes per page in memory, using normalized (0–1) coordinatesAnnotationCanvasrenders on top of the PDF pageFloatingAnnotateBarcontrols tool/color/stroke width- On save: POST to
/api/documents/{id}/tools/flatten-annotationswith page data and scale
API layer: All calls go through apiFetch() in src/lib/api.ts, which auto-redirects to /login on 401. credentials: 'include' is set on all requests (needed for HttpOnly cookie auth).
AuthGuard: Wraps protected routes; redirects unauthenticated users to /login.
- Login/register → backend sets HttpOnly
auth_tokencookie (JWT, 24h expiry) JwtAuthFiltervalidates the cookie on every request- Frontend never reads the token directly; auth state is inferred from API response success/failure
- Only user email is stored in Zustand (display only)
- PDF.js prompts for password; it's stored in component state
- Passed to all backend tool endpoints as
sourcePasswordin request body - Backend loads the encrypted PDF with the supplied password before processing
| Variable | Default | Description |
|---|---|---|
DB_URL |
jdbc:postgresql://localhost:5432/crystalpdf |
PostgreSQL connection |
DB_USERNAME |
postgres |
DB username |
DB_PASSWORD |
postgres |
DB password |
STORAGE_PATH |
./storage |
Disk path for uploaded PDFs |
JWT_SECRET |
(auto-generated) | Base64-encoded HMAC-SHA256 key (256 bits). Leave unset for auto-generation. For multi-instance production, set to the same value on all servers. |
ADMIN_EMAIL |
admin@example.com |
Default admin email (only used if no admin exists on startup) |
ADMIN_PASSWORD |
AdminChangeMe123! |
Default admin password — CHANGE IMMEDIATELY AFTER FIRST LOGIN |
CORS_ALLOWED_ORIGINS |
http://localhost:5173 |
Comma-separated CORS origins (e.g., https://example.com,https://api.example.com) |
Rate Limiting:
- Login/register endpoints limited to 10 attempts per 15 minutes per IP
RateLimitFiltertracks attempts in-memory; for production, integrate Redis and external rate-limiting service- Checks
X-Forwarded-Forheader for proxied requests
JWT Secret:
- Auto-generated on startup if
JWT_SECRETenv var is not set - Generated secret is not persisted (session-only)
- For multi-instance deployments, set
JWT_SECRETto the same value on all servers - 256-bit (32-byte) random secrets generated using
SecureRandom
Default Credentials:
- Auto-generated admin user (configurable via
ADMIN_EMAILandADMIN_PASSWORDenv vars) - Defaults: email=
admin@example.com, password=AdminChangeMe123! - MUST be changed immediately after first login
- No auto-reset mechanism (prevents privilege escalation if password is forgotten)
CORS:
- Origins configurable via
CORS_ALLOWED_ORIGINSenv var - Supports multiple origins (comma-separated)
- Credentials flag enabled for cookie-based auth
Request Cancellation (Frontend):
- Use
createAbortController()utility to cancel pending API requests on component unmount - Prevents memory leaks and state updates on unmounted components
Error Boundaries (Frontend):
ErrorBoundarycomponent catches React errors and prevents app crashes- Shows user-friendly error message with retry button
- Wrap critical components or use at app root level
- LibreOffice headless — Word/doc-to-PDF conversion
- Ghostscript — PDF compression
- Tesseract OCR — text recognition
- QPDF — PDF optimization
- Python 3 + OpenCV — image processing