Schichtpläne werden in vielen Unternehmen noch per Excel oder WhatsApp koordiniert – mit allen damit verbundenen Problemen: Überschneidungen werden übersehen, Änderungen kommen nicht bei allen an, und eine Übersicht über offene Schichten gibt es nicht. Als ein Bekannter im Eventcatering-Bereich genau diese Probleme beschrieb, war das der Startschuss für den Schichtplaner.

Das Besondere an diesem Projekt: Es sind technisch gesehen drei Anwendungen in einer – eine Flask-REST-API im Backend und zwei separate React-Frontends, die sich dieselbe API teilen.

Warum Flask statt Django?

Für reine API-Backends ohne Template-Rendering ist Flask oft die bessere Wahl: weniger Overhead, klare Trennung zwischen API und Frontend, einfachere Konfiguration für JSON-only-Endpoints. Flask-Migrate (basierend auf Alembic) übernimmt die Datenbank-Migrations – bei jedem Deployment wird flask db upgrade ausgeführt, um Schema-Änderungen sicher auf die Produktionsdatenbank anzuwenden.

JWT-Authentifizierung mit Rollenmodell

Die API nutzt JWT-Token zur Authentifizierung. Ein Token enthält neben der User-ID auch die Rolle (admin oder staff). Beide Frontends nutzen dieselbe API, bekommen aber je nach Rolle unterschiedliche Daten und Berechtigungen:

from functools import wraps
from flask_jwt_extended import verify_jwt_in_request, get_jwt

def require_role(role):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            verify_jwt_in_request()
            claims = get_jwt()
            if claims.get("role") != role:
                return {"msg": "Forbidden"}, 403
            return fn(*args, **kwargs)
        return wrapper
    return decorator

@app.route("/api/shifts/", methods=["POST"])
@require_role("admin")
def create_shift():
    ...

Zwei Frontends – ein API-Endpunkt

Der Admin benutzt das Admin-Dashboard: Schichten anlegen, Mitarbeiter verwalten, Besetzung kontrollieren. Die Servicekräfte öffnen den Marktplatz: offene Schichten sehen, sich eintragen, Absagen melden.

Beide Frontends sind mit React und Vite gebaut. Der Build-Output sind statische HTML/JS/CSS-Dateien, die Nginx direkt serviert. Für API-Calls zeigen beide auf dieselbe Flask-API, aber mit verschiedenen JWT-Rollen.

cd frontend    && npm ci && npm run build   # → frontend/dist/
cd marketplace && npm ci && npm run build   # → marketplace/dist/

Nginx-Konfiguration auf NixOS

Ein Gunicorn-Socket bedient beide Nginx-Vhosts. Nginx entscheidet anhand des Host-Headers, welches statische Frontend serviert wird, und leitet API-Anfragen an denselben Socket weiter:

services.nginx.virtualHosts."admin.example.com" = {
  root = "/var/www/meinprojekt/frontend/dist";
  locations."/" = { tryFiles = "$uri $uri/ /index.html"; };
  locations."/api/" = {
    proxyPass = "http://unix:/run/meinprojekt/gunicorn.sock";
  };
};

services.nginx.virtualHosts."jobs.example.com" = {
  root = "/var/www/meinprojekt/marketplace/dist";
  locations."/" = { tryFiles = "$uri $uri/ /index.html"; };
  locations."/api/" = {
    proxyPass = "http://unix:/run/meinprojekt/gunicorn.sock";
  };
};

Das tryFiles "$uri $uri/ /index.html" ist entscheidend für das SPA-Routing: Wenn React-Router eine Route wie /shifts/42 rendert und der Nutzer die Seite neu lädt, würde Nginx ohne diese Direktive eine 404 zurückgeben.

Deploy mit flask db upgrade

Das Deploy-Script muss drei Teile aktualisieren:

cd /var/www/meinprojekt
git pull origin main

# Backend + Migrations
pip install -r backend/requirements.txt
FLASK_APP=wsgi.py flask db upgrade

# Frontends neu bauen
cd frontend    && npm ci && npm run build
cd marketplace && npm ci && npm run build

systemctl restart meinprojekt

Ein GitHub Webhook triggert dieses Script automatisch bei jedem Push – der gleiche Mechanismus wie in meinem Webhook-Deploy-Artikel beschrieben.

Fazit

Das Hybrid-Setup aus Flask-API und zwei React-Frontends hat sich bewährt. Die klare Trennung zwischen API und UI macht es einfach, Admin-Interface und Mitarbeiter-App unabhängig voneinander weiterzuentwickeln. Fragen zur Architektur gerne über das Kontaktformular.