QR Code-based attendance system for recurring communities.
Parusya enables organizations to register members, generate personal QR Codes, and record attendance via scan - replacing paper-and-pencil sign-in sheets with a real-time web platform. Built for weekly frequency, not one-off events.
🌐 Live: parusya.com.br
Three roles, one cohesive flow:
- Participant - self-registers and receives a personal QR Code
- EventStaff - scans QR Codes at the door using a mobile browser
- Organizer - manages events, monitors attendance, and views participant profiles
Each role authenticates independently. Access is scoped to the group at the JWT level, so staff from one group cannot access another group's data.
| Dashboard | QR Code Scan | Participant Profile |
|---|---|---|
![]() |
![]() |
![]() |
┌─────────────────────────────────────────────────────────┐
│ Frontend │
│ React + Vite · Deployed on Vercel │
└───────────────────────────┬─────────────────────────────┘
│ HTTPS / REST
┌───────────────────────────▼─────────────────────────────┐
│ Backend │
│ Spring Boot 3 · Deployed on Railway │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ /auth │ │ /events │ │ /checkins│ ... │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Spring Security (JWT RS256) · Flyway Migrations │
└───────────────────────────┬─────────────────────────────┘
│ JDBC
┌───────────────────────────▼─────────────────────────────┐
│ PostgreSQL │
│ Hosted on Railway │
└─────────────────────────────────────────────────────────┘
- Java 21 + Spring Boot 3
- Spring Security - JWT authentication with RS256 (asymmetric keys), three independent user roles resolved via a composite username strategy (
email:ROLE) - Spring Data JPA + Hibernate - domain model with
@UniqueConstraint,@Index, and lazy loading - Flyway - versioned database migrations
- PostgreSQL - relational storage; native queries for time-series aggregations (5-minute interval bucketing via
date_trunc) - ZXing - QR Code image generation (300×300 PNG, base64-encoded, generated on demand - never stored)
- Apache POI - XLSX export with auto-filter and fixed column widths (headless-safe, no
autoSizeColumn)
- React 18 + Vite
- React Router v6 - protected routes per role
- Recharts - attendance area charts with reference line for mean
- html5-qrcode - camera-based QR Code scanner (environment-first, user-facing fallback)
- React Hook Form + Zod - form validation
- Axios - HTTP client with request/response interceptors (JWT injection, 401 redirect)
- Vercel - frontend hosting with automatic deploys from
main - Railway - backend + PostgreSQL, automatic deploys from
main - Cloudflare - DNS + domain (
parusya.com.br)
Group
├── Organizers (n)
├── EventStaff (n)
├── Events (n)
│ └── Tags (n:m)
└── (scoped access for all entities above)
Participant (global - not tied to a Group)
├── QrCodes (1:n, nullable event_id for v1 global code)
└── CheckIns (n) ──► Event, EventStaff
Key design decisions:
Participantis a global entity - one account works across multiple groupsQrCodestores only the encoded string; images are generated on demandCheckInhas aUNIQUE(participant_id, event_id)constraint enforced at both the application and database layersEventStaffreferences are nullified (not cascaded) on deletion to preserve check-in history- Tags are group-scoped and normalized to lowercase before persistence
All endpoints require a Bearer JWT except registration and login routes.
| Method | Path | Role | Description |
|---|---|---|---|
POST |
/v1/auth/login/{organizer|staff|participant} |
Public | Issue JWT |
POST |
/v1/groups |
Public | Create group + first organizer |
POST |
/v1/participants/register |
Public | Self-register + generate QR Code |
GET |
/v1/participants/me/qrcode |
Participant | Retrieve personal QR Code image |
GET |
/v1/events/active |
EventStaff | List active events for scanning |
POST |
/v1/checkins/scan |
EventStaff | Validate QR Code and record check-in |
GET |
/v1/participants/ranking |
Organizer | Paginated attendance ranking, with absent-only filter |
GET |
/v1/participants/ranking/:id |
Organizer | Full participant profile + attendance grid data |
DELETE |
/v1/participants/ranking/:id |
Organizer | Remove participant and all associated data |
GET |
/v1/checkins/event/:id |
Organizer | Paginated check-in log with name/staff/time filters |
GET |
/v1/stats/events/:id |
Organizer | Per-event stats: totals, 5-min distribution, staff breakdown |
GET |
/v1/stats/events |
Organizer | Aggregated stats across filtered events |
GET |
/v1/export/xlsx |
Organizer | Full data export (events, participants, check-ins) |
backend/
└── src/main/java/com/parusya/
├── domain/
│ ├── checkin/ # CheckIn entity, scan flow, event log, ranking queries
│ ├── event/ # Event CRUD, dynamic Specification filters
│ ├── export/ # XLSX generation (Apache POI)
│ ├── group/ # Group + Organizer management
│ ├── participant/ # Registration, QR Code, ranking, profile
│ ├── staff/ # EventStaff management
│ ├── stats/ # Aggregated and per-event statistics
│ └── tag/ # Tag resolution and normalization
└── infra/
├── exception/ # Global exception handler + error codes
└── security/ # JWT (RS256), roles, SecurityConfig, SecurityUtils
frontend/
└── src/
├── api/ # Axios instance + participants API
├── components/ # AttendanceGrid, LoginPage, PhoneInput, UI primitives
├── contexts/ # AuthContext (token + role persistence)
├── pages/
│ ├── organizer/ # Dashboard, EventDetail, Participants, ParticipantProfile, Staff, Group
│ ├── participant/ # QrCode, Register, Login
│ └── staff/ # Scan
└── routes/ # ProtectedRoute
- Java 21+
- Maven 3.9+
- Node.js 20+
- PostgreSQL 15+ running locally
# Create the database
psql -U postgres -c "CREATE DATABASE parusya;"
# Generate RSA key pair for JWT
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# Export environment variables
export DB_URL=jdbc:postgresql://localhost:5432/parusya
export DB_USERNAME=postgres
export DB_PASSWORD=your_password
export JWT_PRIVATE_KEY=$(cat private.pem | base64 | tr -d '\n')
export JWT_PUBLIC_KEY=$(cat public.pem | base64 | tr -d '\n')
export JWT_EXPIRATION_SECONDS=86400
export PORT=8080
cd backend
mvn spring-boot:runcd frontend
npm install
# Optional: point to local backend
echo "VITE_API_URL=http://localhost:8080/v1" > .env.local
npm run dev
# → http://localhost:3000- Unit and integration tests (Spring Boot Test + Vitest)
- Absent member alerts (2+ consecutive misses → notify organizer)
- Staff scan screen shows member name, age, and birthday flag
- Engagement badges for members (streak tracking)
- WhatsApp integration for outreach lists
Vitor Azevedo Padovani linkedin.com/in/vitorpadovani · vitorpadovani.com.br


