Company Events
A microservices-based platform for managing company events and collecting employee feedback, secured with Keycloak SSO and deployed with Docker & Kubernetes.

Overview
Managing internal company events sounds deceptively simple - until you add authentication, multiple user roles, room scheduling on top. Company Events was built to solve exactly that problem: a single, cohesive platform where admins can create and manage events, assign rooms, and employees can browse - all behind a secure, centralized login.
The project was also a deliberate deep-dive into production-grade tooling. The goal was not just "make it work," but to build something that resembles how real enterprise systems are structured: microservices with clear boundaries, containerized infrastructure, and an external identity provider instead of rolling yet another custom auth system.
Architecture & Tech Stack
The system is composed of three main moving parts, each with a clear responsibility:
Backend - Two NestJS Microservices
The backend is deliberately split into two independent services:
company-eventsservice - owns events and rooms (CRUD, scheduling logic, admin operations)feedbackservice - handles post-event feedback submission and retrieval
Both services connect to their own PostgreSQL databases and expose REST APIs consumed by the frontend. Having separate services meant practicing real service boundary decisions - and living with the consequences when they needed to talk to each other.
Frontend - Angular SPA
The frontend is a single-page application built with Angular and styled with Tailwind CSS. It's split into functional views:
home-view- landing / dashboardevents-view- browsing and registering for eventsadmin-view- admin panel for event and room managementabout-view- project info
A custom AuthInterceptor attaches JWT tokens to every outgoing request, and a route AuthGuard protects views that require a logged-in user or a specific role.
Infrastructure - nginx, Docker, Kubernetes
nginx acts as the single entry point, routing traffic to the right backend service or serving the Angular app. Everything is containerized with Docker (multi-stage Dockerfiles for lean production images) and orchestrated with Docker Compose for local development. The deployment target is Kubernetes - the first real encounter with K8s in this project.
Key Features
- Keycloak SSO - centralized authentication and authorization using an industry-standard identity provider; no passwords stored in the app's own database
- Role-based access control - admin and regular user roles enforced at both the route (Angular guard) and API (NestJS guard) level
- Event management - full CRUD for events with room assignment handled by the dedicated service
- Feedback module - a standalone service for collecting and viewing post-event feedback, fully decoupled from the events service
- Health checks - each service exposes a
/healthendpoint; Docker Compose uses them as readiness probes before dependent services start - Containerized dev environment -
docker-compose upspins up the entire stack (two backends, two databases, Keycloak + its own DB, nginx, frontend) with hot-reload for backend services
The Challenge & Solution
Keycloak + nginx: The Issuer Mismatch Problem
The most painful debugging session in the project. The symptom was straightforward: users could log in, receive a JWT from Keycloak, but every API call came back with 401 Unauthorized. The token looked valid - jsonwebtoken.decode() showed all the right claims. But it kept failing validation.
The root cause was an issuer (iss) mismatch. Keycloak embeds the URL it was reached at into the token's iss claim. The Angular frontend accessed Keycloak via the public nginx URL (e.g. http://localhost/auth). Meanwhile, the NestJS service was trying to validate the token by contacting Keycloak at its internal Docker network address (e.g. http://keycloak:8080/auth). Two different URLs -> two different issuers -> validation failure.
The fix involved making nginx consistently expose Keycloak under a single canonical URL and ensuring the NestJS Keycloak adapter was configured with the same URL that the frontend used - the public-facing one, passed in via environment variable. A small config change, but it required deeply understanding how Keycloak's token introspection and issuer validation actually work.
This is also why the custom KeycloakAuthGuard has explicit logging for both the expected and actual issuer - that console.log was the breakpoint that finally pinpointed the problem.
Kubernetes - From Zero to Somewhat Confident
K8s was a first. Going from "it works in Docker Compose" to "it works in a cluster" involved a steep learning curve around Deployments, Services, ConfigMaps, and Secrets.
Lessons Learned
Externalize everything from day one. Database credentials, Keycloak realm names, URLs - all of it lives in environment variables from the start.
Understand your tools, don't just configure them. Keycloak has a lot of moving parts (realms, clients, mappers, token settings). Spending time actually reading the docs saved hours of debugging down the line.
nginx is more than a web server. Using nginx as a reverse proxy, handling URL rewriting, and managing how internal and external URLs map to each other is a genuinely useful, transferable skill - especially once Kubernetes adds its own networking layer on top.