Files
lemonspace_app/convex/CLAUDE.md
Matthias Meister 456b910532 feat(docs): update LemonSpace manifest and PRD for v2.0 release
- Updated version from v1.5 to v2.0 in both the LemonSpace Manifest and PRD documents.
- Expanded Phase 1 scope to include video and asset nodes, and integrated non-destructive image editing capabilities.
- Enhanced node taxonomy to reflect 6 categories with 27 node types.
- Added details on offline sync features and optimistic updates in the documentation.
- Improved clarity and structure of the product vision and problem statement sections.
2026-04-06 22:27:21 +02:00

12 KiB

convex/ — Backend-Schicht

Convex ist das vollständige Backend von LemonSpace: Datenbank, Realtime-Subscriptions, File Storage und Background-Job-Scheduler in einem. Kein separates API-Layer notwendig.


Dateien im Überblick

Datei Zweck
schema.ts Einzige Wahrheitsquelle für alle Tabellen und Typen
node_type_validator.ts Node-Typen Validator (Phase 1, Phase 2, Phase 3, Adjustment Presets)
ai.ts KI-Bildgenerierungs-Pipeline
credits.ts Credit-System: Balance, Reservation+Commit, Tier-Config
nodes.ts CRUD für Canvas-Nodes
edges.ts CRUD für Canvas-Edges
canvases.ts CRUD für Canvases
openrouter.ts OpenRouter-HTTP-Client + Modell-Config (Backend)
auth.ts Better Auth Integration
helpers.ts requireAuth() + optionalAuth() für Auth-Checks
batch_validation_utils.ts Validierung von Batch-Node-Operationen
canvas-connection-policy.ts Verbindungspolitiken zwischen Nodes (Validierung)
polar.ts Polar.sh Webhook-Handler (Subscriptions)
pexels.ts Pexels Stock-Bilder API
freepik.ts Freepik Asset-Browser API
storage.ts Convex File Storage Helpers + gebündelte Canvas-URL-Auflösung
export.ts Canvas-Export-Logik
http.ts HTTP-Endpunkte (Webhooks)

Schema (schema.ts)

Alle Node-Typen werden über Validators definiert: phase1NodeTypeValidator, nodeTypeValidator (Phase 1+), adjustmentNodeTypeValidator, und adjustmentPresetNodeTypeValidator.

Phase-Struktur

  • Phase 1 Nodes: Aktiv implementierte Nodes (PHASE1_CANVAS_NODE_TYPES)
  • Phase 2/3 Nodes: Vordeklariert, aber implemented: false — UI filtert nach Phase
  • Adjustment Presets: Spezielle Presets für Curves, Color Adjust, Light Adjust, Detail Adjust

Node Data Shapes

Node-Typ Felder Bemerkung
image storageId, url, mimeType, width, height Bild-Upload oder URL
text content Markdown-Text
prompt content, model, modelTier KI-Generierungsanweisung
ai-image storageId, prompt, model, modelTier, parameters, generationTimeMs, creditCost Generiertes KI-Bild
text-node content Generierter KI-Text
compare leftNodeId, rightNodeId, sliderPosition Vergleichs-Node
frame label, exportWidth, exportHeight, backgroundColor Artboard
group label, collapsed Container-Node
note content, color Anmerkung
curves Presets (Kurven) Nicht im UI als Node-Typ, sondern als Presets
color-adjust Presets (Farbkorrektur)
light-adjust Presets (Helligkeit)
detail-adjust Presets (Details)

Tabellen

canvases — Canvas pro User. Index: by_owner, by_owner_updated.

nodes — Alle Nodes eines Canvas. Felder: type, positionX/Y, width, height, status (idle|analyzing|clarifying|executing|done|error), statusMessage, retryCount, data (v.any()), parentId, zIndex. Index: by_canvas, by_canvas_type, by_parent.

data ist v.any() — Typ-Safety läuft über den type-Discriminator + Zod im Frontend. Die Node-Data-Shapes sind in schema.ts dokumentiert.

edges — Verbindungen zwischen Nodes. Index: by_canvas, by_source, by_target.

mutationRequests — Idempotenz-Layer für Client-Replay (clientRequestId) bei offline/Retry-Sync. Felder: userId, mutation, clientRequestId, optionale Ziel-IDs (canvasId, nodeId, edgeId). Index: by_user_mutation_request.

adjustmentPresets — Benutzerspezifische Presets für Adjustment-Nodes. Index: by_userId, by_userId_nodeType.

creditBalances — Pro User: balance, reserved, monthlyAllocation. available = balance - reserved (computed, nicht gespeichert).

creditTransactions — Jede Credit-Bewegung. Types: subscription | topup | usage | reservation | refund. Status: committed | reserved | released | failed.

subscriptions — Aktive Subscription. Tier: free | starter | pro | max | business.

dailyUsage — Täglicher Zähler pro User für Abuse-Prevention. Key: userId + date (ISO).


AI-Pipeline (ai.ts)

generateImage (action, public)
  → checkAbuseLimits (internalMutation)
  → reserve (mutation, wenn INTERNAL_CREDITS_ENABLED=true)
  → markNodeExecuting (internalMutation)
  → scheduler.runAfter(0, processImageGeneration)   ← Background-Job

processImageGeneration (internalAction)
  → generateAndStoreImage (internalAction)
      → generateImageWithAutoRetry (lokal, max 2 Retries)
          → generateImageViaOpenRouter
      → ctx.storage.store(blob)
  → finalizeImageSuccess (internalMutation)
      → commitInternal (credits)
  [Fehler] → releaseInternal (credits)
           → finalizeImageFailure (internalMutation)
  [finally] → decrementConcurrency

Wichtig: generateImage gibt { queued: true } zurück, sobald der Background-Job geplant ist. Der Node wechselt sofort auf status: "executing". Der eigentliche API-Call läuft asynchron.

Retry-Logik: Max. 2 Retries (MAX_IMAGE_RETRIES = 2). Backoff: min(1500ms, 400ms * retryCount). Retryable: provider (5xx, 408, 429), timeout, transient. Nicht retryable: credits, policy, unbekannte 4xx.

env-Flags:

  • INTERNAL_CREDITS_ENABLED=true — Aktiviert Reservation+Commit. Bei false wird nur Abuse-Prevention geprüft.
  • OPENROUTER_API_KEY — Pflicht.

Credit-System (credits.ts)

Tier-Konfiguration (TIER_CONFIG)

Tier Credits/Monat Daily Cap Concurrency Premium-Modelle
free 50 10 1 nein
starter 400 50 2 ja
pro 3.300 200 2 ja
max 6.700 500 2 ja

1 Credit = €0,01 (Euro-Cent-Einheit durchgängig in DB und UI).

Reservation+Commit-Flow

  1. reserve() — Prüft available >= estimatedCost, Daily Cap, Concurrency. Erhöht reserved + dailyUsage. Gibt transactionId zurück.
  2. Job läuft...
  3. commitInternal() — Zieht actualCost von balance ab, gibt estimatedCost aus reserved frei. Schreibt type: "usage".
  4. Bei Fehler: releaseInternal() — Gibt estimatedCost aus reserved frei. generationCount bleibt erhöht (Versuch zählt).

env-Flag: ALLOW_TEST_CREDIT_GRANT=true — Aktiviert grantTestCredits Mutation (nur Dev/Staging).


Auth (helpers.ts)

requireAuth(ctx) // → { userId: string }
optionalAuth(ctx) // → { userId: string } | null

Wirft bei unauthentifiziertem Zugriff. Wird von allen Queries und Mutations genutzt, die User-Daten berühren.

Better-Auth Setup (auth.ts)

  • Auth-Library läuft in Convex (createAuth) und wird via authComponent.registerRoutes(http, createAuth) in http.ts registriert.
  • Login-Modi:
    • emailAndPassword (mit requireEmailVerification: true)
    • magicLink Plugin (better-auth/plugins)
  • Magic-Link-Konfiguration:
    • disableSignUp: true (Magic Link nur für bestehende Accounts)
    • expiresIn: 600 (10 Minuten)
    • Versand über Resend (sendMagicLink)
  • Wichtig für Multi-Domain-Setup (SITE_URL + APP_URL):
    • Verify-Links aus Better Auth werden vor dem Versand auf die App-Origin umgeschrieben (toAuthAppUrl(...)), damit Session-Cookies auf der korrekten Origin gesetzt werden.
    • Ohne dieses Umschreiben kann Login per Magic Link zwar erfolgreich sein, aber das Dashboard in einem permanenten Loading-State hängen (fehlende Session auf App-Origin).

Auth-Race-Härtung

  • canvases.get nutzt optionalen Auth-Check und gibt bei fehlender Session null zurück (statt Throw), damit SSR/Client-Hydration bei kurzem Token-Race nicht in 404 kippt.
  • canvases.list gibt bei fehlender Session eine leere Liste zurück (statt Throw), damit Dashboard-Subscriptions beim Logout keinen Error-Spam erzeugen.
  • credits.getBalance gibt bei fehlender Session einen Default-Stand (0-Werte) zurück (statt Throw), damit UI-Widgets nicht mit Unauthenticated fehlschlagen.
  • credits.getSubscription fällt bei fehlender Session auf Free/Active zurück (statt Throw), damit Tier-UI stabil bleibt.
  • credits.getRecentTransactions gibt bei fehlender Session [] zurück (statt Throw), damit Aktivitätslisten beim Logout sauber leeren.
  • credits.getUsageStats gibt bei fehlender Session 0-Statistiken zurück (statt Throw), damit Verbrauchsanzeigen ohne Fehler ausrendern.

Idempotente Canvas-Mutations

  • nodes.create, nodes.createWithEdgeSplit, nodes.createWithEdgeFromSource, nodes.createWithEdgeToTarget sind über clientRequestId idempotent.
  • edges.create ist über clientRequestId idempotent.
  • nodes.splitEdgeAtExistingNode ist über clientRequestId idempotent (Replay wird als No-op behandelt).
  • nodes.batchRemove ist idempotent tolerant: wenn alle angefragten Nodes bereits entfernt sind, wird die Mutation als No-op beendet.

Nodes & Edges (nodes.ts / edges.ts)

Nodes (nodes.ts)

Validierung:

  • getValidatedBatchNodesOrThrow() — Validiert Batch von Nodes mit validateBatchNodesForUserOrThrow()
  • Verwendet canvas-connection-policy.ts für Verbindungsberechtigungen

Mutationen:

  • create, update, delete — Standard CRUD
  • createWithEdgeSplit, createWithEdgeFromSource, createWithEdgeToTarget — Erstellen mit Edge-Verbindung
  • batchRemove, batchRemoveNodes — Batch-Entfernung
  • splitEdgeAtExistingNode — Split einer Edge am existierenden Node

Optimistische IDs:

  • Nodes erhalten temporäre IDs mit optimistic_-Prefix für optimistic updates

Bridge-Edges:

  • Bei Node-Löschung werden computeBridgeCreatesForDeletedNodes in canvas-utils.ts verwendet, um Kanten neu zu verbinden

Edges (edges.ts)

Validierung:

  • assertConnectionPolicy() — Prüft, ob Source-Node Output erlaubt und Target-Node Input erlaubt
  • assertTargetAllowsIncomingEdge() — Performance-optimierte Prüfung auf eingehende Edges
  • getCanvasConnectionValidationMessage() — Fehlermeldung bei ungültigen Verbindungen

Validierungsregeln (aus canvas-connection-policy.ts):

  • Source-Typ muss Output-Ports haben
  • Target-Typ muss Input-Ports haben
  • Keine self-loops (Edge von Node zu sich selbst)
  • Quelle: image, text, note, group, compare, frame → Source-Ports
  • Ziel: ai-image, compare → Target-Ports
  • Curves- und Adjustment-Node-Presets: Nur Presets nutzen, keine direkten Edges

Storage (storage.ts)

  • generateUploadUrl bleibt eine normale Mutation für Upload-Start im Client.
  • batchGetUrlsForCanvas ist absichtlich keine reaktive Query mehr, sondern eine Mutation. Der Canvas ruft sie gezielt an, wenn sich das aktuelle Set von storageIds geändert hat.
  • Eingabe: canvasId + client-seitig ermittelte storageIds.
  • Server-seitig werden die angefragten IDs gegen die aktuellen Nodes des Canvas verifiziert, bevor ctx.storage.getUrl(...) aufgerufen wird.
  • Ziel der Änderung: weniger Query-Fanout und weniger Canvas-weite Requery-Last bei jedem Node-/Edge-Update.

Konventionen

  • internalMutation / internalAction — Nur von anderen Convex-Funktionen aufrufbar, nicht direkt vom Client.
  • mutation / query / action — Öffentlich, direkt vom Frontend nutzbar.
  • Alle User-Daten sind über userId (Better Auth User ID, kein Convex-eigenes User-Dokument) indiziert.
  • Schema-Migrationen: npx convex dev erkennt Breaking Changes automatisch. Bei v.any()-Feldern gibt es keine automatische Migration — Datensätze müssen manuell/scriptmäßig migriert werden.