---
title: "Convex: Your Reactive Database"
description: "Model and query data in Convex with end-to-end type safety and live updates, and use indexes and soft deletes the way real products do"
type: "lesson"
locale: "en"
course: "The Modern App Stack - Auth, Data and Payments"
number: "3.3"
canonical: "https://agenticschool.dev/courses/modern-app-stack/convex-your-reactive-database"
datePublished: "2026-06-12"
dateModified: "2026-06-12"
---

# Convex: Your Reactive Database

- Course: The Modern App Stack - Auth, Data and Payments
- Lesson: 3.3
- Duration: 30 min
- Level: fortgeschritten
- Status: published
- Canonical URL: https://agenticschool.dev/courses/modern-app-stack/convex-your-reactive-database
- Locale: en

> Model and query data in Convex with end-to-end type safety and live updates, and use indexes and soft deletes the way real products do

## Summary

Convex is a reactive backend: you define a schema, write queries and mutations as TypeScript functions, and your UI updates live whenever data changes. This lesson explains what a database really is versus an Excel sheet, why reactivity matters, the schema-queries-mutations-actions model, the end-to-end type safety that kills whole classes of bugs, when to add an index, and the soft-delete pattern that lets you recover deleted data instead of losing it forever.

## What you learn

- What a database is versus a spreadsheet, and why a reactive database changes how you build
- Schema, queries, mutations and actions, with end-to-end type safety from database to UI
- Indexes for fast reads, and soft delete versus hard delete with a real recovery pattern

## Summary

Your users can log in. Now they need data that persists: their profile, their projects, their saved work. That is the database layer, and Convex is a particularly good fit for the builders this course is for, because it does something most databases do not - it is reactive. When data changes, every part of your UI that depends on that data updates by itself, with no manual refresh code. On top of that, your types flow end to end, from the database into your React components, so a whole category of bugs simply cannot happen. This lesson teaches the model: what a database is, the four kinds of function you write, indexes, and the delete patterns that separate a toy from a real product.

## What you will learn

You will learn what a database actually is and why it beats a spreadsheet for an app, what reactivity buys you, how to define a schema and write queries, mutations and actions, how end-to-end type safety prevents bugs before they happen, when and why to add an index, and the difference between hard and soft deletes with a recovery pattern you can copy.

## Prerequisites

A working app with Clerk auth from the previous lesson, since you will want data tied to logged-in users. The architecture lesson, so you remember the database is the third layer. And the Fundamentals page on what a database is if the idea of a schema or a table is brand new. Comfort with basic TypeScript helps, because in Convex your database logic is just TypeScript functions.

## The problem

Beginners reach for whatever they know: a spreadsheet, a CSV file, or a pile of JSON saved to disk. That works until two users touch the same data, or you need to find one record among ten thousand quickly, or you change the shape of your data and everything silently breaks. Then they bolt a traditional database onto the side and spend days writing glue code: fetch on load, refetch after every change, handle loading states, keep the UI in sync, and chase stale-data bugs where the screen shows old numbers. Most of that glue is pure waste. A reactive, typed database deletes the glue and the bug class with it.

## A database is not a spreadsheet

An Excel sheet or a CSV is fine for a list you read by eye. A database is built for a different job: many users reading and writing at once, finding specific records fast even among millions, enforcing the shape of your data so a bad row cannot sneak in, and never losing or corrupting data when two things happen at the same time. A spreadsheet has no idea who else is editing it and no way to guarantee that a "price" column is always a number. A database does both. The mental upgrade is this: a spreadsheet is a document you look at; a database is a service your app talks to, that guarantees the data stays correct and consistent no matter how many users or how much load. The moment your data is shared between users or needs to be queried quickly, you have outgrown the spreadsheet.

- Spreadsheet/CSV: one editor at a time, no guarantees on data shape, slow to search at scale, easy to corrupt.
- Database: many concurrent users, enforced data shape (the schema), fast lookups via indexes, safe under load.
- Convex adds reactivity on top: changes push to every connected client automatically.

## Why reactivity matters

In a normal setup you write a lot of code to keep the screen in sync with the database: fetch when the page loads, refetch after you change something, poll for updates from other users, manage loading and error states by hand. Every one of those is a chance for a bug where the UI shows stale data. Convex flips this. A query is a live subscription: you write it once, and whenever any data it reads changes - whether you changed it or another user did - Convex pushes the new result to your component and it re-renders. You delete the refetch logic, the polling, and the entire stale-data bug class. For a collaborative or real-time product (a dashboard, a chat, a shared list) this is transformative, and even for a simple app it removes a pile of tedious, error-prone plumbing.

## Schema, queries, mutations and actions

Convex gives you four building blocks. The schema declares the shape of your data - your tables and their fields and types - so the database can enforce it. Queries read data and are reactive. Mutations change data (insert, update, delete) and run in a transaction so they either fully succeed or fully fail. Actions are for talking to the outside world (calling an external API, sending an email) where you need to do something that is not a pure database read or write. Here is a real schema plus a query and a mutation, the everyday motions you will write constantly.

```typescript
// convex/schema.ts - the shape of your data, enforced by the database
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'

export default defineSchema({
  projects: defineTable({
    ownerId: v.string(), // the Clerk user id who owns this project
    name: v.string(),
    archivedAt: v.optional(v.number()), // null/absent = active; a timestamp = soft-deleted
  })
    // index so 'find this user's projects' is a fast lookup, not a full scan
    .index('by_owner', ['ownerId']),
})
```
A schema with a typed table and an index. archivedAt is the field that powers soft delete.

```typescript
// convex/projects.ts - a query (read, reactive) and a mutation (write, transactional)
import { query, mutation } from './_generated/server'
import { v } from 'convex/values'

// Reactive: any client subscribed to this re-renders when the data changes.
export const listActive = query({
  args: { ownerId: v.string() },
  handler: async (ctx, { ownerId }) => {
    const rows = await ctx.db
      .query('projects')
      .withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
      .collect()
    // Soft-deleted rows have archivedAt set - hide them from the active list.
    return rows.filter((row) => row.archivedAt === undefined)
  },
})

export const create = mutation({
  args: { ownerId: v.string(), name: v.string() },
  handler: async (ctx, { ownerId, name }) => {
    return await ctx.db.insert('projects', { ownerId, name })
  },
})
```
Queries read and are live; mutations write inside a transaction. Both are plain TypeScript.

## End-to-end type safety

This is the quiet superpower. Because your schema is TypeScript and Convex generates types from it, the type of your data flows all the way from the database into your React components. If you rename a field in the schema, every query, mutation and component that used the old name turns red in your editor before you ever run the app. You cannot accidentally read a field that does not exist, pass the wrong argument to a mutation, or assume a string is a number. Whole classes of "it crashed in production because the data was not the shape I assumed" bugs become impossible because your editor and the typechecker catch them while you type. For someone building with an agent, this is huge: the agent gets the same type signals and writes correct code far more often, and your typecheck quality gate catches the rest before it ships.

## Indexes: making reads fast

When you ask the database "give me all projects owned by this user", it has two ways to answer. Without an index, it scans every row and checks each one - fine for ten rows, painfully slow for a million. With an index on ownerId, it jumps straight to the matching rows, like using the index at the back of a book instead of reading every page. The rule is simple: any field you regularly filter or sort by deserves an index. You saw it in the schema above - the by_owner index is what makes listActive fast no matter how many total projects exist. Adding indexes early costs almost nothing; discovering you needed them when your app slows to a crawl under real data costs a stressful debugging session.

- No index: the database checks every row (a full scan). Fine for tiny tables, slow at scale.
- With an index: it jumps straight to matching rows. Fast even with millions of rows.
- Index the fields you filter or sort by often - ownerId, status, createdAt are common.
- Define the index in the schema; use it in queries with .withIndex(...).

## Soft delete versus hard delete, with recovery

A hard delete removes a row permanently - it is gone, and so is any chance of getting it back. A soft delete keeps the row but marks it as deleted, usually with a timestamp like the archivedAt field in the schema above. Real products almost always prefer soft deletes for user-facing data, because users delete things by accident all the time, and "sorry, that is gone forever" is a terrible experience and sometimes a compliance problem. With a soft delete you can offer a trash or an undo, recover the data, and keep history and audit trails intact. You only hard-delete later, on a schedule, for data that has been soft-deleted long enough and that you are legally allowed to purge. Here is the pattern: deleting sets the timestamp, restoring clears it, and a background job can hard-delete truly old rows.

```typescript
// Soft delete: mark it, don't destroy it. Restoring is then trivial.
export const softDelete = mutation({
  args: { id: v.id('projects') },
  handler: async (ctx, { id }) => {
    await ctx.db.patch(id, { archivedAt: Date.now() })
  },
})

export const restore = mutation({
  args: { id: v.id('projects') },
  handler: async (ctx, { id }) => {
    await ctx.db.patch(id, { archivedAt: undefined })
  },
})

// Hard delete: only for data soft-deleted long ago, usually run by a scheduled job.
export const purge = mutation({
  args: { id: v.id('projects') },
  handler: async (ctx, { id }) => {
    await ctx.db.delete(id)
  },
})
```
Soft delete sets a flag and stays recoverable; hard delete is final and reserved for old, purgeable data.

## Typical mistakes

The frequent ones: writing manual refetch and polling code when Convex queries are already reactive, so you are fighting the tool; forgetting indexes and watching the app crawl once real data arrives; hard-deleting user data with no recovery, then getting the support ticket you cannot fix; trying to call an external API inside a query or mutation instead of an action; and loosening your schema (everything optional, everything a string) until the type safety that was protecting you evaporates. Lean into the schema and the reactivity rather than working around them.

## Business ROI

A reactive, typed database is a productivity multiplier and a quality multiplier at once. You write dramatically less plumbing, so features ship faster. End-to-end types catch bugs before they reach a customer, so you ship with fewer fires. Indexes keep the app fast as you grow, which protects both conversion and your reputation. And the soft-delete pattern turns a category of catastrophic support incidents - "I deleted everything by accident" - into a one-click restore. For a founder, the time saved on plumbing and the bugs that never happen are worth far more than the modest cost of the service.

## Checklist

You are ready to move on when each of these is true in a real app you built, not just understood in theory.

- You can explain why a database beats a spreadsheet once data is shared or large.
- You have a schema with at least one table, an index, and working query and mutation functions.
- You can see types flow from the schema into your components and catch a rename error in the editor.
- You implemented soft delete with a working restore, and you know when a hard delete is appropriate.

## Resources

The Convex docs are excellent and current - keep the schema, queries, mutations and indexes pages bookmarked. The Fundamentals page on what a database is grounds the concepts if any felt shaky. You now have auth and data; the next lesson locks down the secrets that connect every service you have added, including how to encrypt API keys your users entrust to you.

## Your task

Add a Convex table for something your app needs (a list of items, projects or notes tied to the logged-in user), with an index on the owner. Write a reactive query and a create mutation, then implement soft delete and restore. Delete an item, confirm it disappears from the list, then restore it and watch it reappear without a page refresh. That single exercise proves you understand reactivity, indexes and recoverable deletes all at once.

## Next lesson

Your app now has users and data, which means it has a growing pile of secret keys connecting them. The next lesson is the disciplined version of secret handling: env files, .gitignore, what to do if you ever did push a secret, Convex deploy keys, and encrypting user API keys instead of storing them in plain text.

## Transcript

This lesson is a written, text-first guide. Convex is a reactive backend: you define a schema, write queries and mutations as TypeScript functions, and your UI updates live whenever data changes. This lesson explains what a database really is versus an Excel sheet, why reactivity matters, the schema-queries-mutations-actions model, the end-to-end type safety that kills whole classes of bugs, when to add an index, and the soft-delete pattern that lets you recover deleted data instead of losing it forever. You will model and query data in convex with end-to-end type safety and live updates, and use indexes and soft deletes the way real products do. Work through the sections in order, try the task at the end in a real project, and move on once it works for you. There is no video required - everything you need is in the written steps above.
