Skip to content

Repository files navigation

flowboard-api

A real-time Kanban-style task management API built with Spring Boot, featuring JWT authentication with role-based access control, WebSocket-powered live updates, and fine-grained authorization at the resource level.

Live board demo

Why this project

This is the second project in a two-project backend portfolio (alongside finance-api), built specifically to cover ground the first project didn't: Spring Security with roles, real-time communication via WebSocket, and many-to-many relational modeling. Where finance-api demonstrates message queues, caching, and batch processing, flowboard-api demonstrates authentication/authorization depth and real-time architecture.

Features

  • JWT authentication — stateless auth with BCrypt password hashing, HMAC-signed tokens (HS256), and explicit claim scoping (no sensitive data in the payload)
  • Role-based + ownership-based authorization — global roles (ADMIN/MEMBER) combined with per-resource ownership checks (board owner vs. board member have different permissions on the same board)
  • Real-time updates via WebSocket (STOMP) — card moves, creations, and membership changes broadcast live to everyone viewing a board, authenticated via JWT at the STOMP CONNECT frame
  • Activity log — every meaningful action (card created/moved, member added/removed) is persisted and streamed live
  • Many-to-many board membership — users can belong to multiple boards; boards can have multiple members, modeled with a proper join table
  • Clean REST/DTO boundary — entities are never serialized directly; every endpoint uses purpose-built request/response DTOs to avoid LazyInitializationException and prevent over-posting (e.g. a client can never set their own role)
  • Correct HTTP semantics401 for missing/invalid authentication vs. 403 for authenticated-but-unauthorized, via a custom AuthenticationEntryPoint

Note: in production, set JWT_SECRET as an environment variable — never rely on the default value in application.properties.

Tech stack

Layer Technology
Language / Runtime Java 21
Framework Spring Boot 3.5.3
Security Spring Security, JWT (jjwt 0.12)
Persistence Spring Data JPA, PostgreSQL
Real-time Spring WebSocket (STOMP over SockJS)
Event streaming Apache Kafka
Resilience Resilience4j (retry, rate limiting)
Docs springdoc-openapi (Swagger UI)
Build Maven
Local infra Docker Compose (PostgreSQL, Kafka)

Architecture notes

Entity relationships

  • User owns zero or more Boards, and can be a member of many boards (@ManyToMany, via a board_members join table)

  • Board contains multiple BoardColumns (e.g. "To Do", "Doing", "Done")

  • BoardColumn contains multiple Cards

  • Card optionally has an assignee (User)

  • Every meaningful action on a Board generates an ActivityLog entry, which is broadcast live to /topic/board/{id} via WebSocket

  • Board ↔ User is a genuine @ManyToMany, backed by an explicit board_members join table.

  • All @ManyToOne/@ManyToMany associations are unidirectional and LAZY by design — avoids circular serialization and the MultipleBagFetchException class of bugs.

  • Authorization is enforced explicitly in the service layer (assertIsOwner / assertIsMember), not hidden behind SpEL expressions in annotations — every permission check is a plain, readable if.

WebSocket authentication

WebSocket connections don't pass through the standard servlet filter chain the way REST requests do. Authentication happens instead via a ChannelInterceptor that validates the JWT on the STOMP CONNECT frame and attaches the authenticated principal to the session — every subsequent SUBSCRIBE/SEND on that connection is already authenticated, without re-validating the token per message.

HTTP status semantics

A custom AuthenticationEntryPoint ensures missing/invalid tokens return 401 Unauthorized, while AccessDeniedException (thrown from ownership/membership checks) returns 403 Forbidden — handled centrally in a @RestControllerAdvice.

Kafka & Event-Driven Architecture

Board activity (card created, moved, etc.) is published to Kafka and consumed asynchronously to broadcast over WebSocket — decoupling the write path from real-time delivery.

  • Fixed consumer group ID — prevents the consumer from being treated as "new" on every restart, which would otherwise cause it to re-read the entire topic history
  • Idempotency guard — each event carries a unique eventId; the consumer tracks processed IDs and skips duplicates, preventing the same activity from being broadcast twice on reprocessing
  • Retry with exponential backoff — transient failures are retried automatically (3 attempts, 1s/2s/4s backoff) via @RetryableTopic
  • Dead Letter Topic (DLT) — after exhausting retries, the message is routed to a dedicated -dlt topic instead of being silently dropped or blocking the consumer indefinitely (the "poison pill" problem)

A unit test suite covers this behavior directly (see Testing), including a regression test for an ordering bug where marking an event as "processed" before confirming successful delivery silently defeated the retry mechanism.

Security Hardening

Beyond baseline JWT/RBAC, the following was implemented and validated with real attack payloads — not just configured on faith:

  • Kafka authentication & authorization — broker requires SASL/PLAIN; the app's Kafka user runs with least-privilege ACLs (Write/Read/Describe/Create scoped to the topic/consumer-group prefix, plus IdempotentWrite) instead of a super-user
  • Stored XSS prevention — the WebSocket test client rendered user-controlled data (card titles, activity feed) via innerHTML, allowing a malicious card title to execute JavaScript for any viewer. Fixed with textContent/DOM APIs; verified with both a benign and an active payload
  • Content-Security-Policy — restricts script execution to allow-listed sources, as defense-in-depth
  • Rate limiting on login — 5 attempts/minute via Resilience4j, 429 past threshold
  • Dependency scanning — GitHub Dependabot enabled

Running locally

docker compose up -d --build

The API starts on http://localhost:8083. Swagger UI is available at http://localhost:8083/swagger-ui/index.html.

Try it with curl

# Register
curl -X POST http://localhost:8083/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name":"Gui","email":"gui@example.com","password":"123456"}'

# Create a board (use the token from above)
curl -X POST http://localhost:8083/boards \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{"title":"Sprint 1","description":"First sprint board"}'

Try the live board

Open test-client/websocket-test-client.html directly in a browser (no server needed — it talks to localhost:8083). Log in, load a board, and drag cards between columns. Open a second tab logged in as a different member of the same board to see updates arrive in real time.

API overview

Method Endpoint Description
POST /auth/register Create an account, returns JWT
POST /auth/login Authenticate, returns JWT
POST /boards Create a board
GET /boards List boards you own or belong to
GET /boards/{id} Get a board (members only)
PUT /boards/{id} Update a board (owner only)
DELETE /boards/{id} Delete a board (owner only)
POST /boards/{id}/members/{userId} Add a member (owner only)
DELETE /boards/{id}/members/{userId} Remove a member (owner only)
POST /boards/{id}/columns Create a column (members)
GET /boards/{id}/columns List columns (members)
PUT /columns/{id} Rename a column (members)
DELETE /columns/{id} Delete a column (members)
POST /columns/{id}/cards Create a card (members)
GET /columns/{id}/cards List cards in a column (members)
PUT /cards/{id} Update a card (members)
PATCH /cards/{id}/move Move a card between columns (members)
DELETE /cards/{id} Delete a card (members)

Full request/response schemas available via Swagger UI.

Roadmap

  • Deploy to AWS (EC2 + Docker Compose)
  • Rate limiting on authentication
  • Replace the in-memory STOMP broker (enableSimpleBroker) with a Kafka-backed relay, enabling horizontal scaling across multiple API instances
  • Managed database (AWS RDS) and managed Kafka, replacing self-hosted Docker Compose
  • Observability stack (Actuator + Prometheus + Grafana)

Architecture evolution

This project is deliberately built in two layers: domain logic (entities, services, authorization rules) and infrastructure (where it runs, how events are distributed). The domain logic is considered stable — the roadmap above only swaps out infrastructure underneath it, without rewriting business logic.

Concern Today Planned
Real-time message distribution In-memory STOMP broker (enableSimpleBroker), single instance Kafka-backed relay (enableStompBrokerRelay), supports multiple API instances
Event streaming Kafka (Docker Compose), SASL + least-privilege ACLs Managed Kafka (e.g. MSK)
Database PostgreSQL via Docker Compose AWS RDS (managed PostgreSQL)
Application hosting AWS EC2 + Docker Compose AWS ECS
Secrets Environment variables (Docker Compose) AWS Secrets Manager

The trigger for each change is a real scaling limitation, not novelty for its own sake:

  • Kafka becomes necessary the moment the API runs as more than one instance — an in-memory broker can't relay a message from instance A to a client connected on instance B.
  • AWS becomes necessary for the same reasons any production deployment needs managed infrastructure: uptime, backups, and horizontal scaling that a local machine or a single low-cost host can't provide reliably.

This mirrors the same practical, incremental approach used in finance-api — infrastructure is added when a concrete need justifies it, not preemptively.

Testing

Unit tests cover the Kafka consumer's failure-handling behavior directly, rather than relying on manual verification:

  • Duplicate events (same eventId) are processed exactly once
  • Simulated failures trigger the expected exception path
  • A regression test locks in a fix for an ordering bug where marking an event "processed" before confirming delivery silently broke retry

Run with mvn test.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages