Zum Hauptinhalt springen

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​

SchichtTechnologie
FrameworkNext.js 15 App Router, React 19, Turbopack
DatenbankPostgreSQL (Neon serverless) + Prisma ORM
AuthNextAuth 5.0 beta — Credentials + GitHub OAuth
PaymentsStripe (Subscriptions, Webhooks, Trial)
UIRadix UI Primitives + shadcn/ui + Tailwind CSS
SpracheTypeScript (strict mode)
TestsVitest (Unit/Integration) + Playwright (E2E)
DeploymentVercel (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 → onCreateTenantAndUser Server 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 Checks
  • MEMBER → benötigt explizit zugewiesene Custom-Roles
  • is_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​

ModellZweck
TenantOrganisation/Schule
UserAuth, Rollen, Stripe-Daten
Permission / Role / UserCustomRoleGranulares RBAC
Account / SessionNextAuth Session-Management
VerificationTokenE-Mail-Verifikation

Schul-Entitäten (alle per tenant_id isoliert)​

ModellZweck
StudentSchüler + Guardian + TrustedPerson
SchoolClassKlassen (verknüpft mit GradeLevel, SchoolYear)
SchoolYearSchuljahre mit Start-/Enddatum
SubjectUnterrichtsfächer
GradeLevelKlassenstufen
LessonTimeUnterrichtszeiten für Stundenpläne
TimetableStundenplan-Einträge
JobRoleJob-Rollen (Typisierung via enum UserJobRole)

Hinweis: Es gibt kein eigenes Teacher-Modell. Lehrkräfte sind User-Datensätze mit gesetztem job_role_id (verknüpft mit JobRole, typisiert über enum UserJobRole mit Werten wie TEACHER, DIRECTOR, SECRETARY). Fächer-Zuordnungen und Vertretungsbezüge (TeacherAbsence.teacher_id, Substitution) referenzieren ebenfalls User.

Umgebungsmanagement​

Vercel-UmgebungGit-BranchZweck
ProductionmainLive-System
PreviewdevelopStaging / QA
Developmentlokalwebapp/.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.