Jagoda Kuczkowska
Back to projects
2025Solo Fullstack Developer

Company Events

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

Company Events

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-events service - owns events and rooms (CRUD, scheduling logic, admin operations)
  • feedback service - 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 / dashboard
  • events-view - browsing and registering for events
  • admin-view - admin panel for event and room management
  • about-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 /health endpoint; Docker Compose uses them as readiness probes before dependent services start
  • Containerized dev environment - docker-compose up spins 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.

Project Links

Tech Stack

AngularNestJSKeycloakPostgreSQLDockerKubernetesTailwind CSSTypeScriptnginx