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.
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.
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.
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.
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.
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.
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.
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.
- Queries + useQuery: Real-Time ohne WebSocket-Konfiguration
- Mutations: Transaktionale Schreiboperationen mit optimistischen Updates
- Actions: Externe API-Calls (OpenAI, Stripe) sauber getrennt von DB-Logic
- Crons + Scheduler: Zeitgesteuerte Jobs in TypeScript, kein extra Service
- Auth + File Storage: Clerk/Auth0-Integration und Upload ohne S3
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 →