◆ Convex Stack Bootcamp
Day 1 of 7

Day 1 — The mental model

The one idea. In a normal stack your UI asks for data and you build machinery to find out when to ask again. In Convex you declare what data a component needs, and the backend takes responsibility for pushing you a new answer whenever it changes.

Time: ~60 min · Code written: almost none · You ship: a running deployment and a one-page written model you can defend.

Today is deliberately not a coding day. Nearly everyone who struggles with Convex is fighting the model, not the syntax.


#1. The problem Convex is actually solving

The story. You build a dashboard. useEffect + fetch, ships Monday. Tuesday: the numbers are stale, so you add a 5-second setInterval. Wednesday: your database bill triples, because 400 idle tabs are asking "anything new?" 12 times a minute and the answer is no 99.6% of the time. Thursday: you rip it out for WebSockets, and now you own a socket server, a reconnect strategy, a subscription registry, and a cache-invalidation problem. Friday: someone asks why the dashboard is 4,000 lines.

None of that was the feature. All of it was answering "has this changed?"

Convex's claim is that this question should be the database's job, because the database is the only component that actually knows the answer. It tracks which queries read which data, and when a mutation writes, it re-runs exactly the affected queries and pushes new results to exactly the clients subscribed to them.

That is the entire pitch. Everything else — TypeScript functions, the schema, the dashboard — is supporting cast.

#2. Watch, skeptically

While watching, write down the answer to: what does Convex say you no longer have to build? A vendor video will tell you what it's good at. Your job is to extract the actual claim so you can test it on Day 3 and judge it on Day 7.

#3. The trichotomy

Three function types. One rule generates all their properties:

Queries and mutations are deterministic transactions. Actions are not.

The story. Think of queries and mutations as work done inside a sealed room. Because nothing enters or leaves except through the database, the system can replay that work, cache it, and roll it back. An action is a phone call to the outside world. You cannot un-ring a phone. So the moment you make a network call, transactionality is gone — and Convex makes you say so in the function type rather than finding out at 3am.

The practical decision procedure, all bootcamp long:

Does this need the network? No → mutation. Yes → action that delegates its writes to an internal mutation.

#4. Why undefined means "loading"

useQuery returns undefined while the first result is in flight. There is no isLoading flag. And a query that returns undefined is translated to null on the client.

That translation is not a quirk — it is what makes the design work. Because a real result can never be undefined, undefined unambiguously means "no answer yet." One value, one meaning.

#5. What you give up

Honest accounting, because a bootcamp that only sells is a brochure:

#Quick check 1

A function needs to charge a card via Stripe and record the payment in your database. Which function type, and why can't it be a mutation?

Answer

An action — it makes a network call, and mutations cannot. It should do the Stripe call itself, then persist the result via ctx.runMutation(internal.payments.record, ...). Note what you lose: the charge and the database write are not one atomic unit. If the write fails after the charge succeeds, you have taken money and recorded nothing. That is not a Convex flaw — it is inherent to spanning two systems — but Convex makes the seam visible instead of letting you pretend it isn't there.

#Quick check 2

Why can't a query call fetch(), when technically the JavaScript runtime could?

Answer

Because queries must be deterministic. Convex caches query results and re-runs queries to compute updates; both depend on the same inputs producing the same output. A fetch makes the result depend on the outside world, so caching would serve stale data and re-running could produce a different answer. The restriction is what buys reactivity.


#Build task

No app yet. Instead:

npm create convex@latest
cd <your-project>
npx convex dev

Then open the Convex dashboard, find Data and Logs, and leave both open. Add a row by hand in the dashboard. Notice there is no table until you define one.

#Checkpoint — answer without scrolling up

  1. State the rule that determines whether something is a query, a mutation, or an action.
  2. Why is a mutation automatically retryable when an action is not?
  3. What does useQuery return before data arrives, and why can't that collide with a real result?
  4. Name one thing you give up by choosing Convex.
  5. In one sentence: what work does Convex claim to remove from your codebase?

Two or more misses → re-read §3 and §5 before Day 2.

#Cheat strip

query    : read,  no net, deterministic, cached, reactive
mutation : read+write, no net, ONE transaction, retryable
action   : net allowed, no transaction, not reactive, not retried
           → reaches DB only via ctx.runQuery / ctx.runMutation

useQuery(...) === undefined  ⇔  still loading
decision: needs network? no → mutation · yes → action + internal mutation

#Tomorrow

Day 2 defines a schema and meets the single most consequential performance decision in the stack: .withIndex() versus .filter(). You will write the slow version on purpose.

Stored in this browser only.