Architektur-Übersicht
SchoolSync ist eine Multi-Tenant SaaS-Plattform für Schulverwaltung, gebaut auf Next.js 15 mit App Router.
Monorepo-Struktur
school-sync/
├── webapp/ # Next.js 15 Applikation (Kern)
├── docs-developer/ # Docusaurus — Technische Dokumentation (Port 3001)
├── docs-handbook/ # Docusaurus — Endbenutzer-Handbuch (Port 3002)
└── .claude/rules/ # Claude Code Rule-Dateien
Paketmanager: webapp/ → pnpm | docs-developer/ + docs-handbook/ → npm
Tech Stack
| Schicht | Technologie |
|---|---|
| Framework | Next.js 15 App Router, React 19, Turbopack |
| Datenbank | PostgreSQL (Neon serverless) + Prisma ORM |
| Auth | NextAuth 5.0 beta — Credentials + GitHub OAuth |
| Payments | Stripe (Subscriptions, Webhooks, Trial) |
| UI | Radix UI Primitives + shadcn/ui + Tailwind CSS |
| Sprache | TypeScript (strict mode) |
| Tests | Vitest (Unit/Integration) + Playwright (E2E) |
| Deployment | Vercel (Preview via develop, Production via main) |
Multi-Tenant-Architektur
Alle Schul-Entitäten sind über tenant_id hart isoliert. Kein Cross-Tenant-Zugriff ist auf DB-Ebene möglich — alle DB-Funktionen in lib/*/db.ts holen die tenantId aus der laufenden Session.
Tenant (Schule)
├── Users (ADMIN | MEMBER)
│ ├── Custom-Roles → Permissions (resource:action)
│ ├── job_role_id → JobRole (UserJobRole: TEACHER, DIRECTOR, ...)
│ └── is_owner (exakt einer pro Tenant)
├── Students, SchoolClasses, Subjects, ...
└── Stripe Subscription (Modell `Subscription`)
Tenant-Onboarding
Neue Tenants entstehen ausschließlich über den Registrierungs-Wizard:
- Regulär: Setup-Intent → Stripe Elements →
onCreateTenantAndUserServer Action - Trial:
create-trial-checkout-session→ Stripe Hosted Checkout → Webhook erstellt Tenant + Owner-User
Hauptbereiche der Applikation
Route-Gruppen (webapp/app/)
(backend)/ Gesicherter Admin-Bereich (Schutz über proxy.ts)
dashboard/ Hauptdashboard
students/ Schülerverwaltung (inkl. Guardians, TrustedPersons)
school-classes/ Klassenverwaltung
school-years/ Schuljahre
subjects/ Fächer
grade-levels/ Klassenstufen
lesson-times/ Unterrichtszeiten
timetable/ Stundenplan
klassenbuch/ Digitales Klassenbuch
vertretungsplan/ Vertretungsplan
lehrkraefte/ Lehrkräfteverwaltung
users/ Benutzerverwaltung
job-roles/ Job-Rollen
einstellungen/ Rollen & Rechte, Konfigurationen
(user)/ Benutzerprofil + Account-Einstellungen
(stripe)/ Stripe-bezogene Seiten (Trial, Upgrade)
(portal)/ Schüler-/Eltern-Portal (separater Login-Bereich)
(public)/ Öffentliche Seiten (Landing Page, Registrierung)
Berechtigungssystem
Granulares RBAC auf resource:action-Basis:
ADMIN→ bypasses alle ChecksMEMBER→ benötigt explizit zugewiesene Custom-Rolesis_owner→ Tenant-Inhaber-Schutz (kann nicht gelöscht/degradiert werden)
Implementierung: lib/permissions.ts — hasPermission(), getResourcePermissions(), checkPermissions()
Details: Berechtigungssystem
Datenfluss
HTTP Request
│
▼
proxy.ts (Next.js-Middleware, in Next 15.5 als proxy.ts benannt)
Session vorhanden? → Nein → /login
Status aktiv? → Nein → /login
│
▼
Layout (layout.tsx)
auth() — Session aus JWT lesen (kein DB-Hit)
│
▼
Page (page.tsx) — Server Component
auth() — Session
hasPermission() — DB-Lookup, gecacht via React cache()
Kein Zugriff? → redirect('/dashboard?no_access=1')
│
▼
DB-Funktion (lib/*/db.ts)
prisma.*.findMany({ where: { tenant_id: session.user.tenantId } })
│
▼
Response / Render
Mutation-Pfad (Server Actions)
Client (Form Submit / Button)
│
▼
Server Action (lib/*/actions.ts)
auth() — Session validieren
hasPermission() — Permission prüfen (gecacht)
Zod — Input validieren
│
▼
DB-Funktion (lib/*/db.ts)
Business-Logic + Owner-Schutz
prisma.$transaction (bei zusammenhängenden Writes)
│
▼
revalidatePath() — ISR-Cache invalidieren
Datenbankschema (Überblick)
SaaS-Infrastruktur
| Modell | Zweck |
|---|---|
Tenant | Organisation/Schule |
User | Auth, Rollen, Stripe-Daten |
Permission / Role / UserCustomRole | Granulares RBAC |
Account / Session | NextAuth Session-Management |
VerificationToken | E-Mail-Verifikation |
Schul-Entitäten (alle per tenant_id isoliert)
| Modell | Zweck |
|---|---|
Student | Schüler + Guardian + TrustedPerson |
SchoolClass | Klassen (verknüpft mit GradeLevel, SchoolYear) |
SchoolYear | Schuljahre mit Start-/Enddatum |
Subject | Unterrichtsfächer |
GradeLevel | Klassenstufen |
LessonTime | Unterrichtszeiten für Stundenpläne |
Timetable | Stundenplan-Einträge |
JobRole | Job-Rollen (Typisierung via enum UserJobRole) |
Hinweis: Es gibt kein eigenes
Teacher-Modell. Lehrkräfte sindUser-Datensätze mit gesetztemjob_role_id(verknüpft mitJobRole, typisiert überenum UserJobRolemit Werten wieTEACHER,DIRECTOR,SECRETARY). Fächer-Zuordnungen und Vertretungsbezüge (TeacherAbsence.teacher_id,Substitution) referenzieren ebenfallsUser.
Umgebungsmanagement
| Vercel-Umgebung | Git-Branch | Zweck |
|---|---|---|
| Production | main | Live-System |
| Preview | develop | Staging / QA |
| Development | lokal | webapp/.env |
Datenbankverbindung und GitHub OAuth werden per lib/environment-handling.ts umgebungsspezifisch aufgelöst (Preview nutzt die DEVELOP_*-Varianten). Stripe-Keys sind hingegen nicht umgebungsspezifisch: getStripeSecretKey(), getStripeWebhookSecret() und getStripeTrialProductPriceId() geben in allen Umgebungen dieselben Variablen zurück (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_TRIAL_PROCUCT_PRICE_ID). Test- vs. Live-Modus wird daher über den Wert der jeweils gesetzten Variable gesteuert, nicht über getrennte Variablennamen.