Zum Inhalt springen
Zurück zu ausgewählten Projekten
Live-Demo · Stripe-Testmodus
KROMA-Logo

KROMA

Ein Coffeeshop mit Vorbestellung, von Anfang bis Ende gebaut — ein Live-Storefront mit Stripe-Checkout und ein echter Backoffice-Bereich: Küchenboard, Servicetag und Kennzahlen.

Hero-Bild des KROMA-Storefronts

Ein fiktiver Specialty-Coffeeshop, gebaut als vollständiges Vorbestellungssystem und nicht als Mockup. Gäste stöbern in einer live gepflegten Tageskarte, passen ein Getränk an, bezahlen über Stripe und verfolgen die Bestellung; das Team bearbeitet dieselbe Bestellung auf einem Küchenboard, führt einen Servicetag, der mit einer Kassenzählung endet, und liest die Kennzahlen des Shops — während Preise, Bestände, Zahlungsstatus und Berechtigungen in Postgres entschieden werden und nicht dem Browser vertraut wird.

// Datenschicht

Postgres-first-Logik

Preise, Bestände, Berechtigungen und Berichte liegen in SQL-Funktionen, die durch Row Level Security abgesichert sind.

// Zahlungen

Erst zahlen, dann bestellen

Eine Kartenbestellung wird erst angelegt, wenn Stripe die Zahlung bestätigt; Fehlschläge werden erstattet.

// Team-Bereich

Realtime-Küchenboard

Supabase Realtime auf den Bestellungen, mit einem 30-Sekunden-Polling als Rückfallebene.

// Verifikation

Auf zwei Ebenen getestet

node:test für die App-Logik, dazu SQL-Tests, die in einer zurückgerollten Transaktion laufen.

// überblick

Was KROMA ist

KROMA ist ein fiktiver Specialty-Coffeeshop, gebaut als vollständige, produktionsnahe Webanwendung. Ein Gast stöbert in einer live gepflegten Tageskarte, passt ein Getränk an, bezahlt per Karte über Stripe oder an der Bar und verfolgt die Bestellung; das Team bearbeitet dieselbe Bestellung auf einem Küchenboard und verwaltet Bestand, Menü, Erstattungen und die Kassenzählung am Tagesende in derselben App. Es ist ein Portfolio-Projekt — der Shop ist fiktiv, und Zahlungen laufen im Stripe-Testmodus —, aber der Ablauf dahinter ist echt.

Jede Bestellung wird in Postgres bepreist, bestandsgeprüft und berechtigungsgeprüft, sodass Storefront, Küchenboard und Tagesbericht denselben Bestelldatensatz lesen.

// 01. Strategischer Kontext

Warum KROMA so gebaut ist

01 // Problem

Ein Bezahlen-Button ist noch kein Shop

Vorbestell-Demos enden oft bei einem Bestellen-Button: eine statische Karte und nichts dahinter. Was ein funktionierendes System von einem Mockup unterscheidet, ist alles, was diese Demos auslassen — Bestand, der mitten am Vormittag ausgeht, ein Zahlungsstatus, der serverseitig geprüft werden muss, und eine zweite Oberfläche für die Menschen, die die Bestellung tatsächlich zubereiten.

Kerndynamik: Zwei Oberflächen, eine Bestellung
02 // Lösung

Ein Bestelldatensatz, zwei Oberflächen

KROMA ist als zweiseitiges System gebaut. Die Bestellseite der Gäste und das Küchenboard des Teams lesen und schalten denselben Bestelldatensatz weiter. Postgres-Funktionen berechnen den Preis jeder Bestellung, buchen den Bestand ab und entscheiden, wer was darf; die Next.js-App ruft sie auf.

Kerndynamik: Regeln leben direkt bei den Daten
03 // Ergebnis

Ein Shop, der seinen Tag selbst organisiert

Ein täglicher Vercel Cron-Job eröffnet den Servicetag, und das Team schließt ihn ab, indem es die Kasse gegen einen generierten Bericht zählt. Die Demo bleibt auch vor Fremden sicher: Kartenzahlungen laufen im Stripe-Testmodus, und die Team-PIN ist ein echtes Zugangsdatum mit Schreibrechten, das nicht veröffentlicht wird.

Kerndynamik: Per Cron eröffneter Tag, gezählter Abschluss
// architektonischer Grundsatz
„Der Browser schlägt vor; Postgres entscheidet. Der Client sendet Artikel-IDs, Mengen und Modifikatornamen — jeder Preis, jede Bestandsänderung und jede Berechtigungsprüfung wird in der Datenbank aufgelöst.“
— Architektonischer Grundsatz
// 02. Produktrundgang

KROMA in Aktion erleben

Ein Rundgang durch den Kunden-Storefront und den Team-Bereich — was jeder Teil kann und wie er sich anfühlt

Kundenseite

Was ein Gast oder Mitglied erlebt, vom Stöbern in der Karte bis zum Abholen der Bestellung

01Storefront

Live-Menü-Storefront

Eine live gepflegte Tageskarte, präsentiert als redaktionelle Liste statt als Kartenraster. Bestand und Preise kommen direkt aus Postgres, sodass eine ausverkaufte Charge innerhalb einer Minute als vergriffen erscheint.

Screenshot des KROMA-Storefronts mit der redaktionellen Menüliste und dem fixierten Vorschaubereich
  • Live-Tageskarte: Artikel, Preise und Bestand pro Artikel werden aus Postgres gelesen und alle 30 Sekunden neu validiert. Liefert die Abfrage nichts, greift der Storefront auf einen lokalen menu.json-Snapshot zurück.
  • WebGL-Hero mit Tiefenkarte: Ein Fragment-Shader verschiebt das Hero-Foto entlang einer Graustufen-Tiefenkarte, sobald sich der Cursor bewegt. Ohne WebGL, bei Ladefehlern oder mit reduzierter Bewegung fällt er auf ein Standbild zurück.
  • Redaktionelle Menüliste: Haarlinien-getrennte Zeilen in voller Breite mit einem fixierten Vorschaubereich auf dem Desktop, der die Fotografie bei Hover oder Tastaturfokus wechselt.
  • Kategoriefilter: Pillen-Navigation, deren aktive Füllung zwischen den Kategorien gleitet, statt zu springen.
  • Artikelanpassung: Ein seitliches Sheet für Modifikatoren und Varianten, wobei sich der Preis bei jeder Auswahl neu berechnet.
  • Bestandszustände: Ein erschöpfter Artikel zeigt „Heute vergriffen“; bei fünf oder weniger steht dort „Nur noch N“.
Next.js 16SupabaseFramer MotionWebGLRevalidierung alle 30 s
02Checkout & Tracking

Bestellen, bezahlen, verfolgen

Bezahlen Sie per Karte über Stripe Checkout oder wählen Sie Zahlung an der Bar und verfolgen Sie die Bestellung auf einer teilbaren Seite. Eine Kartenbestellung entsteht erst, wenn Stripe die Zahlung bestätigt.

  • Gast oder Konto: Bestellen funktioniert ohne Konto. Der Warenkorb eines angemeldeten Kunden wird in Postgres gespeichert, und ein Gast-Warenkorb wird bei der Anmeldung damit zusammengeführt.
  • Stripe Checkout: Kartenzahlungen laufen im Stripe-Testmodus. Die Sitzung wird anhand eines serverseitigen Angebots bepreist und läuft nach 30 Minuten ab.
  • Zahlung an der Bar: Eine Bestellung am Tresen wird als offen angelegt; die Barista erfasst Bargeld oder Karte, wenn sie sie als bezahlt markiert.
  • Ernährungsprüfung: Gespeicherte Ernährungsweisen und Allergene markieren unverträgliche Artikel im Warenkorb bereits vor dem Checkout.
  • Bestellverfolgung: Eine teilbare /order/[token]-Seite zeigt, wie die Bestellung von bezahlt über in Zubereitung zu fertig wechselt, und aktualisiert sich alle 15 Sekunden. Bevor sie die Bar erreicht, kann der Kunde sie stornieren, und eine Online-Zahlung wird erstattet.
  • Fertig-Benachrichtigung: Eine Web Push-Benachrichtigung wird ausgelöst, sobald die Bestellung fertig ist, mit E-Mail als Rückfallebene, wenn Push nicht verfügbar ist.
  • Belege: Eine druckbare Belegseite mit Barcode und MwSt.-Zeile, dazu ein per Resend verschickter E-Mail-Beleg, sofern eine Adresse vorliegt.
Stripe CheckoutStripe WebhooksWeb Push (VAPID)ResendServer Actions
03Konten

Konto & Stempelkarte

Die Anmeldung per E-Mail oder Google schaltet eine Stempelkarte, ein „Das Übliche“ mit einem Tipp, die Bestellhistorie und gespeicherte Ernährungspräferenzen frei.

  • Anmeldung: E-Mail und Passwort mit Bestätigungs-E-Mail und Passwort-Zurücksetzung oder Google OAuth.
  • Stempelkarte: Nur Getränkekategorien sammeln einen Stempel. Ein neues Konto startet mit 2 Stempeln, und eine volle Karte hat 10. Der Stand wird aus der Bestellhistorie abgeleitet statt gespeichert, sodass eine stornierte oder erstattete Bestellung ihre Stempel mitnimmt.
  • Gratisartikel: Eine volle Karte löst beim Checkout eine Einheit eines Artikels ein.
  • Das Übliche: Der Artikel, den der Kunde am häufigsten bestellt hat, mit den Modifikatoren seiner letzten Bestellung, bereit zum erneuten Bestellen.
  • Präferenzen: Ein Anzeigename plus ein separater Barname, der am Pass aufgerufen wird, Ernährungsweisen (vegan, vegetarisch, pescetarisch, glutenfrei) und zu meidende Allergene.
  • Bestellhistorie: Frühere Bestellungen mit Button zum erneuten Bestellen.
Supabase AuthGoogle OAuthRow Level SecurityAbgeleiteter Stand

Teamseite

Das Backoffice unter /dashboard, hinter einem echten Konto und einer 4-stelligen PIN

4-stellige PIN

Zweischichtige Team-Anmeldung

Das iPad hinter der Bar hält eine langlebige Sitzung, damit das Board dargestellt werden kann; eine Person schaltet das Schreibrecht mit einer 4-stelligen PIN frei. Die PIN wird ausschließlich in Postgres mit pgcrypto geprüft, fünf Fehleingaben sperren diese Person für 15 Minuten, und jede Entsperrung wird protokolliert.

Screenshot des KROMA-Team-PIN-Pads auf der dunklen Kitchen-Display-Oberfläche
04Küchendisplay

Bestell-Board

Ein dunkles Board mit hohem Kontrast, gebaut für die Linie. Bestellungen wandern durch vier Spalten — Am Pass, In Zubereitung, Bereit an der Bar, Abgeholt — und die Seite des Kunden folgt mit.

Screenshot des KROMA-Küchenboards mit den Bestellungen in den vier Spalten und ihren Alters-Anzeigen
  • Live-Warteschlange: Supabase Realtime auf der Bestelltabelle lädt das Board neu. Eine Verbindungsanzeige zeigt live, Verbindung wird aufgebaut oder offline, ein 30-Sekunden-Poll überbrückt Lücken, und Schreibzugriffe sind offline deaktiviert.
  • Alters-Anzeige: Jede Bestellung zeigt einen laufenden Timer und wechselt nach 5 Minuten von frisch auf warm und nach 10 auf überfällig.
  • Signalton für neue Bestellungen: Ein Ton erklingt, wenn eine neue Bestellung auf dem Board erscheint, sobald das Gerät per Tipp aktiviert wurde.
  • Weiterschalten & Rückgängig: Ein Tipp schaltet eine Bestellung weiter. Sie eine Spalte zurückzuschieben ist innerhalb von 90 Sekunden jedem erlaubt; danach nur noch Manager oder Inhaber.
  • Notizen, Stornos & Erstattungen: Jeder in der Schicht kann eine Notiz hinzufügen. Manager und Inhaber können stornieren oder erstatten, und die Stripe-Erstattung einer Online-Bestellung wird aus der App heraus ausgelöst.
  • Rabatte & Freiartikel: Manager und Inhaber können einen Prozentsatz, einen Festbetrag oder einen vollständigen Freiartikel anwenden, mit verpflichtender Begründung.
  • Installierbar: Das Board lässt sich als Vollbild-PWA im Querformat installieren.
Supabase RealtimePostgres-RPCsPWAFramer Motion
05Servicetag

Öffnen, verkaufen, schließen

Der Verkauf ist in Tage gegliedert. Jemand öffnet den Shop mit Soll-Bestandszählungen, ein geschlossener Shop lehnt Bestellungen ab, und ein Manager oder Inhaber schließt ihn ab, indem er die Kasse gegen einen generierten Bericht zählt.

  • Service öffnen: Das Öffnen setzt den Bestand jedes Artikels auf seinen Sollwert zurück oder auf die im Öffnen-Bildschirm eingegebenen Zahlen. Jede Rolle darf öffnen, und zwei Geräte, die gleichzeitig auf Öffnen tippen, erzeugen genau eine Öffnung.
  • Geschlossener Shop: Solange kein Tag geöffnet ist, löst eine Bestellung die Meldung „Die Backstube ist geschlossen.“ aus.
  • Bonnummern: Jeder Tag nummeriert seine Bons ab #001.
  • Zahlungsart: Am Tresen erfasst die Barista, ob eine Bestellung bar oder per Karte bezahlt wurde.
  • Kassenzählung: Der Abschluss-Bildschirm nimmt eine Zählung pro Stückelung entgegen und zeigt die Abweichung vom erwarteten Bargeld: Wechselgeld plus Bareinnahmen minus Barerstattungen.
  • Abschlusssperre: Ein Tag lässt sich nicht schließen, solange noch Bestellungen am Pass liegen; die Fehlermeldung listet deren Bonnummern auf.
  • Tagesbericht: Bestellungen, Einnahmen, Netto und MwSt.; Summen für Bar, Karte und Online; rabattierte, stornierte, erstattete und entsorgte Beträge; und was noch im Bestand ist.
  • MwSt.: Die Preise bleiben inklusive MwSt. Die MwSt. wird pro Kategorie herausgerechnet (11 % auf alles, was heute verkauft wird) und als Snapshot auf jede Bestellzeile geschrieben.
Postgres-RPCsServicetageMwSt.Vercel Cron
06Analyse

Zahlen & Hauptbuch

Eine Ansicht des Shops für Manager und Inhaber, in Postgres statt im Browser aggregiert, sodass die Berechtigungsprüfung dort sitzt, wo das Geld summiert wird.

  • Einnahmen: Einnahmen über ein gewähltes Datumsfenster, nach Kategorie filterbar.
  • Hinter der Bar: Pro Person: begonnene, stornierte und erstattete Bestellungen, mit Zeiten aus denselben Bestellzeilen.
  • Hauptbuch: Das Audit-Protokoll, gefiltert und seitenweise: Entsperrungen, Bestellverschiebungen, Rabatte, Schichtgrenzen, Öffnen und Schließen sowie Menüänderungen.
  • Berechtigungsschranke: Jede Aggregation liest die Rolle des Aufrufers in der Datenbank erneut und weist alle unterhalb der Manager-Ebene ab.
Postgres-RPCsAudit-ProtokollRollenbasierte Sperre
Menüverwaltung

Das Live-Menü bearbeiten

Manager und Inhaber bearbeiten die Karte im Dashboard: Artikel umsortieren, das Artikel-Sheet mit seinem Modifikator-Editor öffnen, Kategorien verwalten und Artikel löschen. Jeder Schreibvorgang läuft über eine berechtigungsgeprüfte Funktion, und frühere Bestellungen behalten Namen und Preis ihrer Artikel.

// 03. Architektur & Topologie

Serverlose Full-Stack-Architektur

Eine Next.js-App auf Vercel vor einem Supabase-Projekt, mit über Server-Routen angebundenen Diensten für Stripe, Cron und Push

topology.ascii
    [ Browser · Customer & Staff PWAs ]                           [ Vercel Cron ]
      │                           │                                     │
      │                           │ HTTPS                               │ Bearer secret
      │                           ▼                                   ▼
      │                    ┌────────────────────────────────────────────────────────────────┐
      │                    │                Next.js 16 · React 19  (Vercel)                 │
      │                    ├────────────────────────────────────────────────────────────────┤
      │                    │ • Server Components & Server Actions                           │
      │                    │ • Route Handlers: Stripe webhook · cron · auth callbacks       │
      │                    │ • Signed cookie that unlocks staff writes                      │
      │                    └───────────┬─────────────────────┬───────────────────┬──────────┘
      │                                │ RPC                 │ API               │ Push · email
      │                                ▼                   ▼                  ▼
      │                     ┌──────────────────────┐  ┌───────────────┐  ┌───────────────────┐
      └─ RPC · Realtime ──►│ Supabase             │  │ Stripe        │  │ Notifications     │
                            ├──────────────────────┤  ├───────────────┤  ├───────────────────┤
                            │ • Postgres + RLS     │  │ • Checkout    │  │ • Web Push (VAPID)│
                            │ • Security-definer   │  │ • Refunds     │  │ • Resend email    │
                            │   RPCs               │  │ • Webhooks    │  └───────────────────┘
                            │ • Auth               │  └───────────────┘
                            │ • Realtime           │
                            └──────────────────────┘

Matrix der technischen Entscheidungen & Begründungen

Wo jede Verantwortung liegt und warum sie dort liegt

Schicht & UmfangTechnologiewahlZentrale Fähigkeiten & Begründung
Frontend
App-Framework
Next.js 16 (App Router) + React 19

Server Components, Server Actions, eine einzige Client-Grenze

Der Storefront ist eine Server Component, die das Menü lädt und alle 30 Sekunden neu validiert; eine einzige Client-Grenze hält Filter- und Warenkorbzustand. Mutationen sind Server Actions, und Route Handler decken den Stripe-Webhook, den Cron-Job und die Auth-Callbacks ab.
Persistenz
Datenbank & Logik
Supabase Postgres

Geschäftsregeln als Security-Definer-RPCs, RLS auf Kundendaten

Preisberechnung, Bestand, Bonnummern, Berechtigungen, Erstattungen und Berichte liegen in SQL-Funktionen, die als versionierte Migrationen verwaltet werden, sodass Storefront, Board und Cron-Job alle durch dieselben Regeln gehen.
Auth
Identität
Supabase Auth + staff-Tabelle

E-Mail/Google für Kunden; Stationssitzung plus PIN für das Team

Kunden melden sich mit Supabase Auth an. Das Team arbeitet über eine Station, die lesen darf, während eine PIN pro Person Schreibzugriffe freischaltet, sodass jede Änderung jemandem zugeordnet ist.
Commerce
Zahlungen
Stripe Checkout + Webhooks

Erst bezahlen, dann existiert die Bestellung

Die Checkout-Sitzung wird anhand eines Datenbankangebots bepreist. Die Bestellung wird erst nach der Zahlung angelegt, aus dem Webhook oder der Rückkehrseite, und erstattet, falls sie nicht aufgegeben werden kann.
Echtzeit
Live-Updates
Supabase Realtime

Schlank abonnieren, umfassend abrufen

Das Küchenboard lauscht auf Änderungen an der Bestelltabelle und lädt über eine einzige RPC neu, mit einem 30-Sekunden-Poll als Rückfallebene. Die Bestellseiten der Kunden pollen alle 15 Sekunden.
Betrieb
Geplante Jobs
Vercel Cron

Eine authentifizierte tägliche Route um 03:00 UTC

Der Job gibt abgelaufene Online-Bestellungen frei, öffnet den Servicetag über dieselbe RPC, die auch eine Barista nutzt, und löscht veraltete Push-Abonnements.
Messaging
Benachrichtigungen
Web Push (VAPID) + Resend

Zuerst Push, E-Mail als Rückfallebene

Fertig-Benachrichtigungen gehen über einen minimalen Service Worker als Web Push raus; wird kein Push zugestellt, wird eine E-Mail gesendet, sofern eine Adresse existiert. Belege werden per Resend verschickt.
Design
Oberfläche
Tailwind CSS v4 + Framer Motion + WebGL

Haarlinien, Mono-Beschriftungen, Rückfälle für reduzierte Bewegung

Struktur entsteht durch Ein-Pixel-Linien statt durch Karten oder Schatten. Motion-Tokens sind gemeinsam genutzt, und jede Ambient-Animation fällt bei reduzierter Bewegung auf einen ruhigen, nutzbaren Zustand zurück.
// 04. Unter der Haube

Gebaut, damit Geld, Bestand und Berechtigungen stimmen

Vier Entscheidungen, die das System zusammenhalten

// 01 • Zahlungen & Bestellungen

Bezahlen, bevor die Bestellung existiert

Eine Kartenbestellung wird nur aus einer bereits bezahlten Stripe-Sitzung angelegt, sodass eine unbezahlte Kartenbestellung nie in die Bestelltabelle gelangt.

  • In der Datenbank bepreist: Das Angebot vor der Zahlung und das Einfügen nach der Zahlung nutzen denselben Zeilen-Resolver. Der Client sendet Artikel-IDs, Mengen und Modifikatornamen; jeder Preis wird aus menu_items gelesen.
  • Webhook und Rückkehrseite: checkout.session.completed und die Bestätigungsweiterleitung rufen beide denselben Handler auf, der auf stripe_session_id idempotent ist.
  • Erstattung bei Fehlschlag: Kann eine bezahlte Sitzung nicht zur Bestellung werden, wird sie automatisch mit einem Idempotenzschlüssel pro Sitzung erstattet.
  • Signatur geprüft: Der Webhook weist jede Anfrage ohne gültige Stripe-Signatur ab.
  • Stripe
  • Webhooks
  • Idempotenz
  • Postgres RPC
// 02 • Daten & Sicherheit

Postgres als maßgebliche Instanz

Row Level Security und Security-Definer-Funktionen legen die Regeln direkt neben die Daten, sodass kein Client-Pfad sie umgehen kann.

  • Eingegrenzte Kundendaten: RLS-Richtlinien beschränken Kunden auf ihre eigenen Warenkörbe, Profile, Favoriten und Bestellungen.
  • Gastbestellungen per Token: Gäste haben keine Richtlinie auf Bestellungen. Ein nicht erratbarer Token öffnet genau eine Bestellung über order_by_token(), das den Token, die Nutzer-ID und die Stripe-Spalten auslässt.
  • Snapshots: Artikelname, Preis, MwSt.-Satz und Stempelberechtigung werden auf jede Bestellzeile kopiert, sodass spätere Menüänderungen die Historie nicht umschreiben.
  • Abgeleitete Stände: Der Stempelkartenstand ist eine Abfrage über die Bestellhistorie, kein gespeicherter Zähler.
  • In SQL getestet: SQL-Tests prüfen das echte Schema innerhalb einer Transaktion, die zurückgerollt wird, während node:test die App-seitige Logik abdeckt.
  • Row Level Security
  • security definer
  • Supabase
  • node:test
// 03 • Team & Berechtigungen

Rollen, PINs & ein Audit-Protokoll

Die Rollen Inhaber, Manager und Team werden von einer einzigen staff_can()-Funktion durchgesetzt, die die RLS-Richtlinien und jede schreibende RPC aufrufen.

  • Eine Berechtigungsfunktion: Das TypeScript-Gegenstück existiert nur, um Buttons auszublenden; die Datenbank ist die Grenze.
  • Akteur neu lesen: Jede schreibende RPC liest den Akteur aus der staff-Tabelle, sodass das Deaktivieren einer Person ihren nächsten Schreibvorgang stoppt, selbst wenn ihr Cookie noch gültig ist.
  • PIN-Sperre: Fünf falsche PINs sperren eine Person für 15 Minuten und schreiben ein staff.locked-Ereignis.
  • Inhaber-Anspruch: Das erste angemeldete Konto, das claim_owner() aufruft, wird Inhaber, und die Tür schließt sich hinter ihm — ohne vorgegebene Zugangsdaten.
  • Audit-Protokoll: Aktionen schreiben in ein staff_events-Protokoll, das Manager und Inhaber lesen können.
  • staff_can()
  • pgcrypto
  • Sperre
  • Audit-Protokoll
// 04 • Betrieb

Ein sich selbst führender Servicetag

Ein täglicher Job und eine race-sichere Öffnung halten den Shop ohne manuellen Schritt am Laufen.

  • Täglicher Cron: vercel.ts plant /api/cron/release-holds für 03:00 UTC, authentifiziert mit einem CRON_SECRET-Bearer-Token.
  • Was er tut: Er gibt abgelaufene Online-Bestellungen frei, öffnet den Tag über open_service (als die am frühesten angelegte aktive Person im Team) und löscht Push-Abonnements, die älter als 24 Stunden sind.
  • Race-sichere Öffnung: Das Öffnen ist ein Insert auf den Primärschlüssel des Tages, sodass gleichzeitige Öffnungen einen einzigen Tag hinterlassen und den Bestand vom Morgen nie zurücksetzen.
  • Serialisierter Bestand: Menüzeilen werden beim Abbuchen des Bestands mit for update gesperrt, sodass zwei Käufer, die nach dem letzten Artikel greifen, sich einreihen.
  • Nummerierte Bons: Bonnummern stammen aus der Zeile des Tages, innerhalb derselben Funktion zur Bestellanlage, die den Bestand abbucht.
  • Vercel Cron
  • open_service
  • Zeilensperren
  • Servicetage
// 05. Herausforderungen & Hürden

Technische Hürden & Lösungen

Wo echtes Geld, echter Bestand und echtes Personal den einfachen Ansatz falsch machten

// 01 • Nebenläufigkeit & Geld

Zwei Personen, ein letztes Brötchen

Das Hindernis

Eine Online-Bestellung hält keinen Bestand mehr, während der Kunde bei Stripe ist, sodass zwei Personen für den letzten Artikel zahlen können. Das Angebot weist einen bereits vergriffenen Artikel ab, kann diese Race Condition aber nicht verhindern.

Die Lösung

Die Funktion zur Bestellanlage sperrt die Menüzeilen mit for update, bevor sie den Bestand abbucht. Das Insert des Verlierers löst einen Fehler aus, und der Zahlungs-Handler erstattet ihm das Geld. Wiederholungen sind sicher: Bestellungen sind pro stripe_session_id eindeutig, Webhook und Rückkehrseite teilen sich einen Handler, und die Erstattung trägt einen Idempotenzschlüssel pro Sitzung.

Wirkung: Eine Zahlung wird höchstens zu einer Bestellung oder wird erstattet. Der bewusst in Kauf genommene Kompromiss: Der Verlierer zahlt und wird erstattet, statt früher gestoppt zu werden.

  • Zeilensperren
  • Idempotenz
  • Auto-Erstattung
  • Stripe
// 02 • Datentransport

Ein Warenkorb, der in die Stripe-Metadaten passt

Das Hindernis

Die Bestellung wird nach der Zahlung von Code angelegt, der nur die Checkout-Sitzung sieht, also muss der Warenkorb mit ihr reisen — und Stripe-Metadatenwerte sind kurz.

Die Lösung

packItems() serialisiert den Warenkorb als JSON und teilt ihn in items_N-Schlüssel zu je 500 Zeichen auf, auf 45 begrenzt, um Platz für Name, Notizen und IDs zu lassen. unpackItems() setzt ihn wieder zusammen, und die Datenbank baut die Bestellung aus den Artikel-IDs neu auf und berechnet die Preise neu, statt den Metadaten zu vertrauen.

Wirkung: Vor der Zahlung existiert keine Entwurfs-Bestellzeile, sodass ein abgebrochener Checkout nichts hinterlässt. Bestellungen sind auf 50 Zeilen begrenzt.

  • Stripe-Metadaten
  • Chunking
  • Server Actions
// 03 • Sicherheit & Identität

Das iPad, das sich nie abmeldet

Das Hindernis

Das Gerät hinter der Bar bleibt den ganzen Tag angemeldet, sodass seine Sitzung nicht sagen kann, wer was getan hat — und ein gestohlenes iPad sollte niemandem etwas erstatten können.

Die Lösung

Eine Station hält die langlebige Sitzung, damit das Board lesen kann, doch eine Datenbank-Constraint verbietet einer Station eine PIN, sodass für sie nie eine Akteurssitzung erzeugt werden kann. Personen schalten Schreibzugriffe mit einer in Postgres geprüften 4-stelligen PIN frei, und jede RPC liest den Akteur neu. Team-Richtlinien, die die staff-Tabelle abfragten, gerieten in eine Rekursion, daher durchbricht ein Security-Definer-Helfer is_staff() den Zyklus.

Wirkung: Schreibvorgänge werden einer Person zugeordnet, eine Deaktivierung wirkt beim nächsten Schreibvorgang, und die Möglichkeit, das Board zu sehen, verleiht nie die Möglichkeit, es zu ändern.

  • PIN + pgcrypto
  • Sperre
  • security definer
  • RLS
// 06. Technologie-Stack

Womit es gebaut ist

Schichtweise Aufschlüsselung der Frameworks, Dienste und Werkzeuge

Frontend & UI
  • Next.js 16
  • React 19
  • Tailwind CSS v4
  • shadcn/ui
  • Base UI
  • Framer Motion
Backend & API
  • Supabase Postgres
  • Stripe Checkout
  • Stripe Webhooks
Echtzeit & WSS
  • Supabase Realtime
Hintergrund-Queues
  • Vercel Cron
Speicher & Medien
  • ImageKit
Auth & Sicherheit
  • Supabase Auth
  • Row Level Security
  • pgcrypto
Kommunikation
  • Web Push (VAPID)
  • Resend
DevOps & Deployment
  • Vercel
  • pnpm
// 07. Lokaler Betrieb & Evaluierung

Live ausprobieren & lokal nachbauen

Der Storefront unter kroma.lenardtunya.com öffnet sich jeden Morgen von selbst, sodass es immer eine Live-Karte zum Bestellen gibt. Kartenzahlungen laufen im Stripe-Testmodus — verwenden Sie 4242 4242 4242 4242, ein beliebiges zukünftiges Ablaufdatum und eine beliebige CVC.

Das Team-Dashboard ist echt, und genau deshalb wird seine PIN nicht veröffentlicht: Sie ist ein Zugangsdatum mit Schreibrechten auf Live-Demodaten. Um es zu sehen, führen Sie das Projekt lokal aus — der Seed bringt eine eigene Wegwerf-PIN mit — oder nehmen Sie Kontakt auf, um eine interaktive Vorführung zu erhalten.

local-setup.sh
# 1. Clone and install
git clone https://github.com/JustTunya/kroma.git
cd kroma
pnpm install

# 2. Start local Postgres + Auth, seeded from supabase/seed.sql
supabase start

# 3. Configure environment variables (see README for each one)
cp .env.local.example .env.local

# 4. Start the app
pnpm dev
# Storefront: http://localhost:3000
# Dashboard:  http://localhost:3000/dashboard

# 5. Unlock the staff dashboard (local seed only)
#    a. Sign up at /auth/sign-up, confirm via the local Inbucket mail UI
#       (`supabase status` prints its URL, usually http://localhost:54324)
#    b. Link that account to the seeded staff row:
#       update staff set user_id = (select id from auth.users
#         where email = 'you@example.com') where display_name = 'Demo Owner';
#    c. Sign in, open /dashboard, enter the seeded PIN 1234, then open the shop

Hinweise zur Evaluierung & Details zur Simulation

  • Simulierter Shop: KROMA ist fiktiv. Kartenzahlungen laufen im Stripe-Testmodus ohne echte Abbuchungen, und die Texte zu Röstung und Beschaffung im Storefront sind Atmosphäre, keine echte Lieferkette.
  • Echtes Web Push, keine SMS: Fertig-Benachrichtigungen sind echte VAPID-Benachrichtigungen per Web Push, mit E-Mail als Rückfallebene. Es gibt keinen SMS- oder Telefonnummernweg.
  • PolyForm-Noncommercial-Lizenz: Der Quellcode ist öffentlich zum Lesen, Studieren und für persönliche oder Bildungszwecke unter PolyForm Noncommercial 1.0.0. Kommerzielle Nutzung — einschließlich des Betriebs als echter Shop — erfordert eine separate Lizenz.

KROMA in Aktion erleben

Öffentlich zugänglich für technische Bewertung, Code-Review und Architekturprüfung.