Voor mijn eigen brandweerpost heb ik een applicatie gebouwd waarmee diensten en oefeningen worden ingepland, en waarmee leden via Telegram op de hoogte blijven. Wat begon als “even een rooster maken” is uitgegroeid tot een systeem met een verrassend genuanceerd indelingsalgoritme erachter — en het is inmiddels aan zijn tweede leven bezig.
Hoe de indeling werkt
Rollen worden gegroepeerd in door de beheerder zelf gedefinieerde rolgroepen (kolommen op het overzicht, zoals “Uitzetploeg” of “Speelploeg”), elk met een eigen vulvolgorde. Per rol is in te stellen of hij verplicht gevuld moet worden, of er een specifieke opleiding voor nodig is, en of hij altijd handmatig wordt toegewezen (bijvoorbeeld de oefenleider) — andere rollen kunnen dan pas automatisch vullen zodra die handmatige rollen zijn ingevuld, zodat de rest van de indeling logisch aansluit op wie de leiding heeft.
Een groep kan ook als “voorkeurgroep” worden gemarkeerd: rollen daarbinnen zijn dan óf de leuke rol óf de minder leuke rol. De app onthoudt per persoon hoe vaak diegene op rij de niet-voorkeursrol heeft gekregen, en geeft die persoon dan voorrang op de voorkeursrol. Bij de eerlijkheid zelf wordt op twee niveaus gekeken: eerst hoe vaak iemand in de hele roosterperiode een rol heeft vervuld die dezelfde opleiding vereist als de open rol (brede eerlijkheid), en pas daarna, als tiebreaker, hoe vaak diegene precies déze rol al deed. Wat overblijft wordt willekeurig beslist.
Ook late wijzigingen zijn ondervangen: meldt iemand zich binnen twee dagen voor een oefening af, dan schuift het systeem automatisch iemand uit de overflow-rol door naar de vrijgekomen plek, met dezelfde eerlijkheidslogica. Meldt iemand zich juist weer aan, dan wordt gekeken of er nog een passende open rol is, en anders belandt diegene in de overflow. Per rooster is bovendien in te stellen wanneer een oefening automatisch “definitief” wordt (bijvoorbeeld twee dagen van tevoren om 18:00) — een achtergrondservice bewaakt dat moment en sluit de indeling dan vanzelf.
De Telegram-bot
Leden hebben geen aparte app of account nodig — alles loopt via Telegram. De bot laat je aan- en afmelden voor oefeningen, toont de indeling van de week, geeft je eigen statistieken (of die van een postgenoot), laat zien wie er bij jouw post horen, en begeleidt nieuwe leden door een aanmeldproces. Alles werkt met knoppen in de chat, en een beheerder kan zelfs vanuit een chatbericht rollen toewijzen of oefendoelen aan- en uitvinken. Nieuwe aanmeldingen komen in een wachtrij totdat een beheerder ze koppelt aan een bestaand persoon.
De beheer-omgeving
Daarnaast is er een admin-omgeving voor posten, personen, opleidingen, roosters, rolvervulling en oefendoelen-statistieken, met Telegram-koppelingen beheren, aanmeldingen goed- of afkeuren en een testbericht-functie om een koppeling te checken.
Versie 1 versus versie 2
Wat dit project extra leuk maakt om te laten zien: dit is eigenlijk al de tweede versie. De eerste opzet (najaar 2025) had een vaste, hardgecodeerde lijst van dertien rollen die niet per post aan te passen was, en het indelingsalgoritme kwam neer op: heeft iemand de juiste opleiding, en zo ja, wie heeft deze specifieke rol tot nu toe het minst vaak gedaan — daarna gewoon willekeurig. Geen rolgroepen, geen voorkeurslogica, geen brede eerlijkheid over opleidingen heen, geen opvang van late af- of aanmeldingen.
Daarna is het project een half jaar blijven liggen: van januari tot eind juli 2026 gebeurde er niets. Toen ik het weer oppakte, is in een paar dagen tijd vrijwel alles herbouwd: de Telegram-bot kwam er in één keer bij, en kort daarna volgden razendsnel achter elkaar de configureerbare rolgroepen, de voorkeurslogica, opleiding-brede eerlijkheid, automatische herindeling bij afmeldingen, automatisch definitief maken van oefeningen, statistieken per rol en oefendoel, en de hele beheer-omgeving eromheen. Wat in versie 1 een simpel schema was, is in versie 2 een systeem geworden dat continu leert van wie wat al heeft gedaan.
Techniek
De applicatie bestaat uit een gelaagde .NET 8-backend (Domain, Application, Persistence, Infrastructure, WebApi) met PostgreSQL via Entity Framework Core, en een Next.js/React-frontend met shadcn/ui.
De infrastructuur staat volledig als code in Bicep, en het leukste daaraan is dat er nergens een wachtwoord voorkomt:
- Azure App Service (Linux, gedeeld App Service Plan) host zowel de backend (.NET 8) als de frontend (Node 20, via pm2), elk met een system-assigned managed identity.
- Azure Database for PostgreSQL Flexible Server draait met wachtwoord-authenticatie volledig uitgeschakeld — alleen inloggen via Entra ID is mogelijk. Een user-assigned managed identity is ingesteld als Postgres AAD-beheerder, en een deployment-script gebruikt die identity om de managed identity van de backend als niet-admin databaserol toe te voegen. De backend logt vervolgens in met een Entra-token dat elke paar minuten ververst wordt via DefaultAzureCredential — er bestaat dus letterlijk geen databasewachtwoord om te lekken.
- Azure Key Vault (RBAC-geautoriseerd, geen access policies) bewaart de paar secrets die nog geen Entra-tegenhanger hebben: het Auth0 client secret, de Auth0 sessie-cookie secret, en het Telegram bot-token. Beide Web Apps lezen die via Key Vault-references in hun app settings, zodat de waarden nooit los in configuratie belanden.
- Azure DevOps Pipelines deployt alles, met een service connection op basis van workload identity federation (OIDC) in plaats van een service principal-secret — ook de CI/CD-keten zelf werkt dus zonder rondslingerende sleutels.
- Alles is geparametriseerd op projectnaam en omgevingsnaam, zodat dezelfde Bicep zonder aanpassing naar een andere omgeving of subscription kan worden uitgerold.
Authenticatie en autorisatie lopen op dit moment nog via Auth0 (JWT-tokens, rolclaims), maar dat is de volgende stap: omdat de rest van de infrastructuur al helemaal om Entra ID heen is gebouwd (managed identities, Postgres AAD-auth, OIDC-pipelines), ligt het voor de hand om Auth0 te vervangen door Microsoft Entra ID en zo de laatste losse identity-provider uit de stack te halen.
