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.
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
- The bare minimum you need to know about Convex — 10:30
- What is Convex & Why Should You Care? — 17:38
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.
query— reads, no writes, no network. Deterministic, so it is cacheable and re-runnable, which is why it can be reactive.mutation— reads and writes, no network. Runs as one transaction: all of it lands or none of it does. Because it is deterministic it can be safely retried.action— can call the network. Therefore it cannot be a transaction, cannot be retried automatically, and is not reactive. It reaches the database only by calling queries and mutations.
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:
- It's a hosted platform. There is an open-source backend, but the default path is Convex's cloud. That is a vendor relationship. Decide with open eyes.
- No SQL. No
JOIN, no arbitrary ad-hoc query tool for your analysts. You write TypeScript, and you plan your access patterns as indexes up front (Day 2). - The reactive model has a cost shape. A query that reads a huge slice of a hot table re-runs on every write to that slice. Sloppy queries are more expensive here than in a system that only runs them when asked.
- Actions are the ragged edge. Everything elegant about Convex is about determinism. The parts that talk to Stripe or OpenAI don't get to be elegant, and Day 4 is largely about containing that.
#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
fetchmakes 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 devThen 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
- State the rule that determines whether something is a query, a mutation, or an action.
- Why is a mutation automatically retryable when an action is not?
- What does
useQueryreturn before data arrives, and why can't that collide with a real result? - Name one thing you give up by choosing Convex.
- 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.