2026 ist der Anspruch an Backend-Entwicklung deutlich gestiegen: Real-Time ist kein Nice-to-have mehr, sondern Standard. Nutzer erwarten, dass Daten ohne Page-Reload aktualisiert werden — ob im kollaborativen Dokument, im Live-Dashboard oder im Chat. Gleichzeitig soll das Team keine Infrastruktur verwalten, keine WebSocket-Server aufsetzen und keine Caching-Schichten pflegen.

Convex löst genau dieses Problem: ein vollständig typsicheres TypeScript-Backend, das Datenbank, Real-Time-Layer, Serverless-Functions, Cron-Jobs und File Storage in einer einzigen Plattform vereint. Und Claude Code als KI-Coding-Assistent macht den Einstieg so schnell, dass du in unter einer Stunde ein produktionsreifes Backend hast — inklusive Real-Time, Auth und File-Uploads.

Real-Time by Default
Jede Query ist automatisch eine Live-Subscription
🔒
End-to-End Typsicherheit
Schema → Backend → Frontend ohne Bruch
🚀
Zero Server-Management
Deploy mit einem Befehl, kein DevOps nötig
🤖
Claude Code Integration
KI generiert validen Convex-Code in Sekunden
Wer profitiert am meisten? Solo-Entwickler und kleine Teams, die schnell skalierbare Fullstack-Apps bauen wollen — ohne sich um Infrastruktur, WebSocket-Server oder manuelle Cache-Invalidierung kümmern zu müssen.

1. Convex Grundlagen & Projektstruktur

Der Einstieg in Convex beginnt mit einem einzigen Terminal-Befehl. Convex initialisiert das Projekt, erstellt den convex/-Ordner und verbindet dich mit dem Cloud-Backend. Claude Code kann den gesamten Prozess begleiten und Schema-Definitionen, Funktionen und Client-Code in einem Rutsch generieren.

Setup Projekt initialisieren

Convex installieren und mit dem Cloud-Dashboard verbinden:

Terminal
# Neues Projekt mit Vite + React + TypeScript npm create vite@latest meine-app -- --template react-ts cd meine-app && npm install # Convex installieren und initialisieren npm install convex npx convex dev # Claude Code starten — Convex-Schema generieren lassen claude

Nach npx convex dev erstellt Convex automatisch den convex/-Ordner mit generierten TypeScript-Typen. Hier liegt das Herzstück jeder Convex-App: die Schema-Datei.

Schema-Definition mit defineSchema

Das Schema ist der einzige Ort, an dem du die Datenbankstruktur definierst. Convex generiert daraus vollständige TypeScript-Typen — kein separates ORM, kein manuelles Typing.

convex/schema.ts
import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ // Aufgaben-Tabelle mit Indexes für effiziente Abfragen tasks: defineTable({ title: v.string(), description: v.optional(v.string()), completed: v.boolean(), priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")), dueDate: v.optional(v.number()), // Unix-Timestamp userId: v.string(), teamId: v.optional(v.id("teams")), }) .index("by_user", ["userId"]) .index("by_team", ["teamId"]) .index("by_user_completed", ["userId", "completed"]) .searchIndex("search_title", { searchField: "title" }), // Teams-Tabelle teams: defineTable({ name: v.string(), slug: v.string(), ownerId: v.string(), plan: v.union(v.literal("free"), v.literal("pro"), v.literal("enterprise")), createdAt: v.number(), }) .index("by_slug", ["slug"]) .index("by_owner", ["ownerId"]), // Kommentare mit verschachtelten Referenzen comments: defineTable({ taskId: v.id("tasks"), authorId: v.string(), body: v.string(), edited: v.optional(v.boolean()), createdAt: v.number(), }) .index("by_task", ["taskId"]) .index("by_author", ["authorId"]), });
Claude Code Prompt-Tipp: "Erstelle ein Convex-Schema für eine Projektmanagement-App mit Tasks, Teams und Kommentaren. Füge sinnvolle Indexes für häufige Abfragen hinzu." Claude Code generiert typischerweise ein vollständiges, production-ready Schema in einem einzigen Schritt.

Die Typen v.string(), v.number(), v.boolean(), v.id() und v.optional() decken alle gängigen Felder ab. Indexes werden direkt an die Tabellendefinition gekettet — Convex erstellt sie automatisch in der Cloud-Datenbank.

2. Queries & Real-Time Subscriptions

Das Besondere an Convex: Jede Query ist automatisch eine Real-Time Subscription. Du schreibst normale Datenbankabfragen mit query(), und Convex sorgt dafür, dass der React-Hook useQuery() auf dem Client automatisch neu rendert, sobald sich relevante Daten ändern — ohne WebSocket-Konfiguration, ohne Event-Listener, ohne Polling.

Query Real-Time Datenbankabfrage
convex/tasks.ts
import { query } from "./_generated/server"; import { v } from "convex/values"; // Alle offenen Tasks eines Users — automatisch Real-Time export const getOpenTasks = query({ args: { userId: v.string(), limit: v.optional(v.number()), }, handler: async (ctx, args) => { const tasks = await ctx.db .query("tasks") .withIndex("by_user_completed", (q) => q.eq("userId", args.userId).eq("completed", false) ) .order("desc") .take(args.limit ?? 50); return tasks; }, }); // Volltextsuche über Task-Titel export const searchTasks = query({ args: { searchQuery: v.string(), userId: v.string(), }, handler: async (ctx, args) => { if (args.searchQuery.length < 2) return []; return ctx.db .query("tasks") .withSearchIndex("search_title", (q) => q.search("title", args.searchQuery) .eq("userId", args.userId) ) .take(20); }, }); // Einzelner Task mit Kommentaren (Join-Muster) export const getTaskWithComments = query({ args: { taskId: v.id("tasks") }, handler: async (ctx, args) => { const task = await ctx.db.get(args.taskId); if (!task) return null; const comments = await ctx.db .query("comments") .withIndex("by_task", (q) => q.eq("taskId", args.taskId)) .order("asc") .collect(); return { ...task, comments }; }, });

useQuery im React-Frontend

Im Frontend ersetzt useQuery() jeden manuellen useEffect-Fetch. Das Ergebnis ist immer aktuell — Convex invalidiert den Cache automatisch, wenn sich Daten in der Datenbank ändern.

src/components/TaskList.tsx
import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api"; export function TaskList({ userId }: { userId: string }) { // Kein useEffect, kein useState für Daten — Convex managed alles const tasks = useQuery(api.tasks.getOpenTasks, { userId, limit: 20 }); // undefined = noch nicht geladen, null = nicht gefunden if (tasks === undefined) return <Skeleton />; if (tasks.length === 0) return <EmptyState />; return ( <ul> {tasks.map((task) => ( <TaskItem key={task._id} task={task} /> ))} </ul> ); } // Suche: automatisches Re-Rendering bei Tippen export function TaskSearch({ userId }: { userId: string }) { const [query, setQuery] = useState(""); const results = useQuery(api.tasks.searchTasks, { searchQuery: query, userId }); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul>{results?.map(t => <li key={t._id}>{t.title}</li>)}</ul> </> ); }
Real-Time ohne Boilerplate: Wenn ein anderer User einen Task als erledigt markiert, aktualisiert sich TaskList auf allen Clients sofort — ohne WebSocket-Setup, ohne Redux, ohne manuellen Re-Fetch.

3. Mutations & Datenbankoperationen

Mutations sind die Schreiboperationen in Convex. Sie laufen transaktional auf dem Server und haben Zugriff auf alle Datenbankoperationen: insert, patch, replace und delete. Claude Code kann komplexe Mutations inklusive Validierungslogik und Fehlerbehandlung in einem Schritt generieren.

Mutation CRUD-Operationen
convex/tasks.ts (Mutations)
import { mutation } from "./_generated/server"; import { v } from "convex/values"; // Task erstellen — mit server-seitiger Validierung export const createTask = mutation({ args: { title: v.string(), description: v.optional(v.string()), priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")), dueDate: v.optional(v.number()), userId: v.string(), teamId: v.optional(v.id("teams")), }, handler: async (ctx, args) => { // Validierung: Titel darf nicht leer sein if (args.title.trim().length === 0) { throw new Error("Titel darf nicht leer sein"); } const taskId = await ctx.db.insert("tasks", { title: args.title.trim(), description: args.description, completed: false, priority: args.priority, dueDate: args.dueDate, userId: args.userId, teamId: args.teamId, }); return taskId; }, }); // Task aktualisieren (patch = nur geänderte Felder) export const updateTask = mutation({ args: { taskId: v.id("tasks"), title: v.optional(v.string()), description: v.optional(v.string()), priority: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high"))), completed: v.optional(v.boolean()), dueDate: v.optional(v.number()), }, handler: async (ctx, args) => { const { taskId, ...updates } = args; // Prüfen ob Task existiert const existing = await ctx.db.get(taskId); if (!existing) throw new Error("Task nicht gefunden"); // Nur übergebene Felder aktualisieren const patch: Partial<typeof existing> = {}; if (updates.title !== undefined) patch.title = updates.title.trim(); if (updates.description !== undefined) patch.description = updates.description; if (updates.priority !== undefined) patch.priority = updates.priority; if (updates.completed !== undefined) patch.completed = updates.completed; if (updates.dueDate !== undefined) patch.dueDate = updates.dueDate; await ctx.db.patch(taskId, patch); return taskId; }, }); // Task löschen (mit Kommentaren) export const deleteTask = mutation({ args: { taskId: v.id("tasks") }, handler: async (ctx, args) => { // Cascade: erst alle Kommentare löschen const comments = await ctx.db .query("comments") .withIndex("by_task", (q) => q.eq("taskId", args.taskId)) .collect(); for (const comment of comments) { await ctx.db.delete(comment._id); } await ctx.db.delete(args.taskId); }, });

Optimistische Updates im Frontend

Mit useMutation() kannst du optimistische Updates definieren — das UI aktualisiert sich sofort, bevor die Server-Antwort zurückkommt. Bei Fehler rollt Convex automatisch zurück.

src/components/TaskItem.tsx
import { useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; export function TaskItem({ task }) { const updateTask = useMutation(api.tasks.updateTask) .withOptimisticUpdate((localStore, args) => { // Sofortiges UI-Update — kein Warten auf Server const current = localStore.getQuery(api.tasks.getOpenTasks, ...); if (current !== undefined) { localStore.setQuery(api.tasks.getOpenTasks, ..., current.map(t => t._id === args.taskId ? { ...t, ...args } : t) ); } }); const toggleComplete = () => updateTask({ taskId: task._id, completed: !task.completed }); return ( <li> <input type="checkbox" checked={task.completed} onChange={toggleComplete} /> <span>{task.title}</span> </li> ); }

4. Actions & Externe APIs

Während Queries und Mutations direkt auf die Convex-Datenbank zugreifen, sind Actions für alles gedacht, was externe Dienste einbindet: HTTP-Requests, OpenAI-Calls, Stripe-Zahlungen, E-Mail-Versand. Actions können Queries und Mutations aufrufen, aber keinen direkten Datenbankzugriff nutzen — das hält die Trennung sauber.

Action OpenAI Integration + Scheduling
convex/ai.ts
import { action } from "./_generated/server"; import { v } from "convex/values"; import { api } from "./_generated/api"; import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // Task-Beschreibung mit KI generieren export const generateTaskDescription = action({ args: { taskTitle: v.string(), taskId: v.id("tasks"), }, handler: async (ctx, args) => { // Externes API aufrufen (geht in Actions, nicht in Queries/Mutations) const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: `Schreibe eine kurze, präzise Beschreibung für folgende Aufgabe: "${args.taskTitle}". Max. 2 Sätze, Deutsch.`, }], }); const description = completion.choices[0].message.content ?? ""; // Mutation über runMutation aufrufen (nicht direkt ctx.db) await ctx.runMutation(api.tasks.updateTask, { taskId: args.taskId, description: description, }); return { description }; }, }); // Stripe-Zahlung initiieren export const createCheckoutSession = action({ args: { teamId: v.id("teams"), plan: v.union(v.literal("pro"), v.literal("enterprise")), userId: v.string(), }, handler: async (ctx, args) => { const team = await ctx.runQuery(api.teams.getById, { teamId: args.teamId }); if (!team) throw new Error("Team nicht gefunden"); const priceId = args.plan === "pro" ? process.env.STRIPE_PRO_PRICE_ID : process.env.STRIPE_ENT_PRICE_ID; const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: [{ price: priceId, quantity: 1 }], mode: "subscription", success_url: `https://app.example.com/success?session={CHECKOUT_SESSION_ID}`, cancel_url: `https://app.example.com/pricing`, metadata: { teamId: args.teamId, userId: args.userId }, }); return { url: session.url }; }, });
Wichtig: Environment-Variablen in Actions werden über das Convex Dashboard gesetzt (npx convex env set OPENAI_API_KEY sk-...), nicht über .env-Dateien. Claude Code generiert dafür automatisch die korrekten process.env.*-Zugriffe.

5. Crons & Scheduler

Convex bietet zwei Arten von zeitgesteuerten Aufgaben: Crons für wiederkehrende Jobs (täglich, stündlich, nach Zeitplan) und den Scheduler für einmalige, verzögerte Ausführungen. Beide sind vollständig in TypeScript definiert — keine externe Cron-Service nötig, kein zusätzlicher Deployment-Schritt.

Cron Wiederkehrende Jobs
convex/crons.ts
import { cronJobs } from "convex/server"; import { api } from "./_generated/api"; const crons = cronJobs(); // Jeden Tag um 08:00 UTC: Fälligkeits-E-Mails senden crons.daily( "daily-due-date-reminders", { hourUTC: 8, minuteUTC: 0 }, api.notifications.sendDueDateReminders ); // Alle 15 Minuten: Inaktive Sitzungen bereinigen crons.interval( "cleanup-inactive-sessions", { minutes: 15 }, api.sessions.cleanupInactive ); // Jeden Montag um 09:00 UTC: Wochen-Report generieren crons.weekly( "weekly-team-report", { dayOfWeek: "monday", hourUTC: 9, minuteUTC: 0 }, api.reports.generateWeeklyTeamReport ); // Benutzerdefinierter Cron: Stündlich zu Minute 30 crons.cron( "hourly-stats-update", "30 * * * *", api.analytics.updateHourlyStats ); export default crons;

ctx.scheduler für verzögerte Ausführung

Der Scheduler ermöglicht es, Jobs einmalig zu einem bestimmten Zeitpunkt oder nach einer Verzögerung auszuführen — ideal für Reminder, Retry-Logik oder mehrstufige Workflows.

convex/notifications.ts
import { mutation, internalMutation } from "./_generated/server"; import { v } from "convex/values"; import { api, internal } from "./_generated/api"; // Reminder 24h nach Task-Erstellung planen export const scheduleReminder = mutation({ args: { taskId: v.id("tasks"), userId: v.string(), }, handler: async (ctx, args) => { // Job in 24 Stunden ausführen await ctx.scheduler.runAfter( 24 * 60 * 60 * 1000, // 24h in Millisekunden internal.notifications.sendReminder, { taskId: args.taskId, userId: args.userId } ); }, }); // Zu einem bestimmten Zeitpunkt ausführen export const scheduleAtDeadline = mutation({ args: { taskId: v.id("tasks"), deadlineMs: v.number(), }, handler: async (ctx, args) => { // 1 Stunde vor der Deadline erinnern const reminderTime = args.deadlineMs - 60 * 60 * 1000; if (reminderTime > Date.now()) { await ctx.scheduler.runAt( reminderTime, internal.notifications.sendDeadlineReminder, { taskId: args.taskId } ); } }, }); // Interner Job — nicht von außen aufrufbar export const sendDueDateReminders = internalMutation({ handler: async (ctx) => { const tomorrow = Date.now() + 24 * 60 * 60 * 1000; const today = Date.now(); const dueSoonTasks = await ctx.db .query("tasks") .filter((q) => q.and( q.eq(q.field("completed"), false), q.gte(q.field("dueDate"), today), q.lte(q.field("dueDate"), tomorrow) ) ) .collect(); // E-Mail-Versand für jeden fälligen Task planen for (const task of dueSoonTasks) { await ctx.scheduler.runAfter(0, internal.email.sendDueReminder, { taskId: task._id, userId: task.userId, }); } return { processed: dueSoonTasks.length }; }, });
Claude Code Best Practice: Verwende internalMutation und internalAction für Jobs, die nur vom Scheduler oder anderen Server-Functions aufgerufen werden sollen — nicht vom Client. Claude Code erkennt dieses Muster automatisch und generiert den richtigen Funktionstyp.

6. Auth & File Storage

Convex integriert sich nahtlos mit Auth-Anbietern wie Clerk und Auth0. Die User-Identity ist in jeder Funktion über ctx.auth.getUserIdentity() verfügbar — kein manuelles Token-Parsing, keine JWT-Validierung im Code. File Storage ist ebenfalls eingebaut: Upload, Speicherung und Abruf per URL ohne externen S3-Bucket.

Auth Clerk Integration
convex/auth.config.ts
export default { providers: [{ domain: "https://clerk.example.com", applicationID: "convex", }], };
convex/tasks.ts (mit Auth)
export const getMyTasks = query({ handler: async (ctx) => { // User-Identity aus Clerk-JWT extrahieren const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Nicht authentifiziert"); return ctx.db .query("tasks") .withIndex("by_user", (q) => q.eq("userId", identity.subject)) .order("desc") .collect(); }, }); export const createMyTask = mutation({ args: { title: v.string(), priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Nicht authentifiziert"); return ctx.db.insert("tasks", { title: args.title, completed: false, priority: args.priority, userId: identity.subject, // Clerk User-ID }); }, });

File Storage: Upload & Abruf

Convex File Storage ermöglicht den direkten Upload von Dateien aus dem Browser — ohne S3-Konfiguration, ohne eigenen Upload-Server. Der Ablauf ist dreistufig: Upload-URL generieren, Datei hochladen, Storage-ID in der Datenbank speichern.

Storage File Upload Pattern
convex/files.ts
import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; // Schritt 1: Upload-URL generieren (läuft serverseitig) export const generateUploadUrl = mutation({ handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Nicht authentifiziert"); return await ctx.storage.generateUploadUrl(); }, }); // Schritt 3: Storage-ID nach erfolgreichem Upload speichern export const saveAttachment = mutation({ args: { taskId: v.id("tasks"), storageId: v.id("_storage"), filename: v.string(), mimeType: v.string(), sizeBytes: v.number(), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Nicht authentifiziert"); return ctx.db.insert("attachments", { taskId: args.taskId, storageId: args.storageId, filename: args.filename, mimeType: args.mimeType, sizeBytes: args.sizeBytes, uploadedBy: identity.subject, uploadedAt: Date.now(), }); }, }); // Datei-URL abrufen (temporär, signiert) export const getAttachmentUrl = query({ args: { storageId: v.id("_storage") }, handler: async (ctx, args) => { return ctx.storage.getUrl(args.storageId); }, });
src/components/FileUpload.tsx
import { useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; export function FileUpload({ taskId }) { const generateUploadUrl = useMutation(api.files.generateUploadUrl); const saveAttachment = useMutation(api.files.saveAttachment); const handleUpload = async (file: File) => { // Schritt 1: Upload-URL vom Server holen const uploadUrl = await generateUploadUrl(); // Schritt 2: Direkt zu Convex Storage hochladen const result = await fetch(uploadUrl, { method: "POST", headers: { "Content-Type": file.type }, body: file, }); const { storageId } = await result.json(); // Schritt 3: Metadata in Datenbank speichern await saveAttachment({ taskId, storageId, filename: file.name, mimeType: file.type, sizeBytes: file.size, }); }; return ( <input type="file" onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])} /> ); }

Convex vs. traditionelle Backend-Optionen

Feature Convex Express + PostgreSQL Supabase
Real-Time by Default ✓ Automatisch ✗ Manuell (Socket.io) ~ Nur Postgres Replication
TypeScript End-to-End ✓ Schema → Client ✗ Manuelle Types ~ Generierte Types
Server-Management ✓ Keins nötig ✗ VPS/Container ✓ Managed
Cron-Jobs ✓ Eingebaut ✗ Externe Lösung ~ pg_cron Extension
File Storage ✓ Eingebaut ✗ S3 nötig ✓ Eingebaut
Optimistische Updates ✓ Nativ ✗ Manuelle Implementierung ✗ Manuelle Implementierung
Claude Code Kompatibilität ✓ Exzellent ~ Gut ~ Gut

Fazit: Convex + Claude Code = Full-Stack in Stunden

Convex verändert, wie TypeScript-Backends gebaut werden. Die Kombination aus automatischem Real-Time, End-to-End-Typsicherheit, eingebautem Scheduler und File Storage eliminiert ganze Kategorien von Boilerplate-Code. Was früher eine Woche Infrastruktur-Setup bedeutete — WebSocket-Server, Redis-Cache, S3-Integration, Cron-Service — ist mit Convex in einem einzigen npx convex dev erledigt.

Claude Code multipliziert diesen Vorteil: Schema-Definition, Query-Optimierung, Mutations mit Validierungslogik, komplexe Action-Chains — Claude Code generiert production-ready Convex-Code, den du direkt deployen kannst. Kein Stack Overflow, kein Copy-Paste aus alten Tutorials. Stattdessen: beschreiben, generieren, testen, deployen.

Next Steps: Starte mit npx convex dev und dem offiziellen Convex Quick-Start. Nutze Claude Code mit dem Prompt: "Erstelle ein vollständiges Convex-Backend für [deine App] mit Schema, Queries, Mutations und Auth." — du wirst überrascht sein, wie vollständig der generierte Code ist.
Convex Claude Code TypeScript Real-Time Backend Serverless React WebSockets File Storage Cron Jobs
SM
SpockyMagicAI Team
Agentic AI & Developer Tools | agentic-movers.com

Convex-Backend mit KI-Assistenz bauen?

Starte jetzt deinen kostenlosen Trial und lass Claude Code dein nächstes Real-Time Backend mit Convex aufbauen — Schema, Queries, Auth und Crons inklusive.

Kostenlosen Trial starten →