Mike Cann's avatar
Mike Cann
a month ago

Convex has one big limitation. Daytona fixes it

So, I don't know about you guys, but these days my agents don't just need to be able to write code, they often need to be able to run code in the cloud as well. And this is actually kind of a problem if your backend is Convex, because Convex can't run Python, or the Rust compiler, or npm install.

Well fortunately the good folks over at Daytona released a Convex component which helps with exactly this, and they asked me if I'd like to do a video on it. So I spent a few days having a play and ended up with three little apps, which I had my agent cook up. Here's one of them, writing, compiling and running some Rust from a Convex app:

0:00 / 0:00

Let's take a look.

The limitation

Don't get me wrong, I do love Convex, but it does unfortunately have some limits. Your Convex functions run JavaScript, and that's about it. There's no shell and no proper disk, so if your agent wants to run some Python or compile some Rust you just can't do it. Try to shell out from an action and Convex won't even let you push it:

0:00 / 0:00

Now that error does point you at "use node", which gets you a Node.js runtime for that action, and that's great for npm packages that need it. It still isn't a computer you can install Python or a compiler on tho.

And to be honest, being able to run code is kind of a big deal for agents these days. If you're writing some sort of Lovable or Bolt kind of application, you need your agent to be able to write the code in the cloud and execute it in the cloud.

So what you really need is a computer that you can hand to the agent so it can do the things it needs to do, and if it trashes it, who cares? You just throw it away and grab another one.

That is basically what a sandbox is, and what Daytona does.

Daytona

So Daytona are a sandbox provider and I think they're one of the leading ones out there right now. Loads of the agent tools you've probably heard of are using them, and you can even run Cursor's cloud agents on Daytona.

I've bumped into them a few times now at events, most recently at the AI Engineer conference in Singapore, and honestly they're just amazing people, super smart, very friendly and they know their stuff. And they are legends enough to build their own Convex component, which is super cool.

Setting it up

Setting it up is pretty simple, or you can just do what I do and point your agent at the component page and let it rip. If you're doing it yourself then you install the package:

1npm install @daytona/convex
2

Then add it to your convex/convex.config.ts. Components can't read your app's env vars, so you declare the key and pass it down:

1// convex/convex.config.ts
2import { defineApp } from "convex/server";
3import { v } from "convex/values";
4import daytona from "@daytona/convex/convex.config.js";
5
6const app = defineApp({
7  env: { DAYTONA_API_KEY: v.string() },
8});
9app.use(daytona, {
10  env: { DAYTONA_API_KEY: app.env.DAYTONA_API_KEY },
11});
12
13export default app;
14

If you haven't set that key yet, Convex is going to yell at you when you push:

1✖ Environment variable DAYTONA_API_KEY is declared as required in convex.config.ts but its value was not set.
2Set it with:
3
4    npx convex env set DAYTONA_API_KEY <value>
5

So grab one from your Daytona dashboard and do what it says. Pretty obvious.

Now every sandbox you create, and every command you run in it, ends up as a row in a Convex table inside of the component. So here's a little action that creates a sandbox and runs some Python in it, and a query that reads back everything that sandbox has run:

1// convex/agent.ts
2import { Daytona } from "@daytona/convex";
3import { v } from "convex/values";
4import { components } from "./_generated/api";
5import { action, query } from "./_generated/server";
6
7const daytona = new Daytona(components.daytona);
8
9export const runPython = action({
10  args: { code: v.string() },
11  handler: async (ctx, { code }) => {
12    const { sandboxId } = await daytona.createSandbox(ctx, {
13      autoStopInterval: 15,
14      autoDeleteInterval: 60,
15    });
16    return await daytona.runCode(ctx, { sandboxId, code, language: "python" });
17  },
18});
19
20export const executions = query({
21  args: { sandboxId: v.string() },
22  handler: async (ctx, args) => daytona.listExecutions(ctx, args),
23});
24

That query is just a normal Convex query, so useQuery gives your UI live updates of your sandbox the same way it does for anything else in your app. Super cool! Here are those tables in the dashboard for one of my apps. It starts on the app's own table, then the dropdown switches over to the daytona component, showing sandboxes and then executions. The document that opens on the right is an npm install with its exit code and all of its logs:

0:00 / 0:00

Actually, you know what? Rather than me just explaining it, let's just have a look at the demos.

Demo 1: a mini Lovable

So the first one I want to show off is the one I talked about before, which is kind of like a Lovable or a Bolt builder that runs on Convex. You describe an app, an LLM writes the code from a Convex action, and then a Daytona sandbox installs it, runs it and gives us back the application.

I tried one of the example prompts, a tip calculator that splits the bill. It creates the sandbox straight away, very quick, then you see the code getting written. This clip picks up from there, so watch the npm install logs scrolling in the terminal on the left, and then the live preview on the right:

0:00 / 0:00

Hey, there we go. Cool. So we can set the bill amount to $100 and the tip percentage. This is obviously very American, in Australia we don't really have tips like this, or at all, thank god 😅 But anyway ye, it works.

Because all of this is just data in Convex you can also refresh the page in the middle of a build and it carries on where it was. Here's another build, and the page reloads about 2 seconds into this clip. The terminal empties, then its logs come straight back:

0:00 / 0:00

So now on Convex you can build yourself a Lovable or a Bolt or whatever you want.

Demo 2: chat with your CSV

The second one I want to show off is running Python for analytical kinds of workloads, using pandas on the server. So we start with some sample data that I had Opus cook up, which is a bunch of rows of store sales, and then we ask it a question, like which month had the most revenue.

It's going to run Python in the Daytona sandbox, using the actual pandas library (the default sandbox comes with it installed), and come back with an answer:

0:00 / 0:00

There we go, pretty cool. By the way, if you're quick you can see one of its steps blow up with a NameError when it scrolls past. It read the error and just had another go, which I think is pretty cool.

You can imagine this working inside of a company where you don't want your users executing code on their own computers, or they can't because maybe it's a thin client, but they still want to do analytical workloads. Pandas is a really good use case for that, so I think this is kind of cool.

Demo 3: Rust

And it's not just Python. With the third one we can even run Rust, because we've got somewhere to run the Rust compiler now. This sandbox is built from the official Rust Docker image rather than the default one:

1const { sandboxId } = await daytona.createSandbox(ctx, { image: "rust:1-slim" });
2const { executionId } = await daytona.runBackground(ctx, {
3  sandboxId,
4  command: "cargo build 2>&1",
5});
6

runBackground is for commands that might take a while. It returns straight away, and then the Convex scheduler keeps checking back on the command and writes its logs into that same executions row, which is what the UI is showing while it builds.

So that clip from the start was me asking it to please write me a fizz buzz app. It writes the Rust file, runs cargo build, runs the program and gives us the output. One, two, fizz, four, buzz, fizz. Cool. I can't even remember FizzBuzz, but I think it's one of those common interview questions.

If it doesn't compile then the errors go back to the LLM and it has another go, up to 4 attempts. FizzBuzz went through first time for me, so here's a run where it didn't. This one has strict mode on, which turns warnings into errors. Watch the cargo build panel on the right go red on attempt 1, and then green on attempt 2:

0:00 / 0:00

Then I wondered what would happen if I asked it for the first 50,000 primes. I figured I was probably going to break it, so watch the program output at the bottom:

0:00 / 0:00

Whoa. Ah, I think it stopped, as the run says exit 101. In the video I guessed at some sort of limit on message sizes, but the code shows it's our own doing. This app is a public demo, so to protect it my agent capped each program's output at 10,000 bytes. It got cut off a bit over a thousand primes in, and I think the program then panicked when it couldn't write any more.

Anyway, still very very cool that you can run the Rust compiler on Convex now.

What does it cost?

So you're probably wondering at this point, what does all this actually cost? Well, Daytona bill you per second, and a default sandbox (1 vCPU and 1 GiB of memory) works out at about 7 cents an hour. So very, very little. They also very generously give you $200 of free credit when you sign up, and you don't even need to put in your credit card.

The demos above each take about a minute to run, if that, so that's about a tenth of a cent. The Rust one deletes its sandbox as soon as it's finished, and the other two keep theirs around so you can carry on using it, until it stops after 10 idle minutes. So call it a cent or so.

One gotcha tho is that a sandbox keeps costing you while it's running, so make sure you send your sandboxes to sleep. That's what autoStopInterval and autoDeleteInterval are for in that action earlier, and they're both in minutes. Daytona stops an idle sandbox after 15 minutes by default, but a background command doesn't count as activity, so for really long jobs the component docs say to create the sandbox with autoStopInterval: 0. Then it's on you to stop it.

Oh and I will just point out that the LLM stuff in these demos goes through the Convex AI Gateway, so you don't even need an OpenAI or Anthropic key, and it just shows up on your Convex bill. You do need to be on a paid Convex plan for that one. I have an entire video on it if you're interested to find out more.

Good to know

Now before you go off and actually build stuff, a couple of quick things that are good to know.

The component doesn't have a way to ask for a particular GPU when you create a sandbox right now, so for GPU sandboxes I'd still use the Daytona SDK in a Node action.

Convex also doesn't get told by default when Daytona stops one of your sandboxes, so the row can say started when it isn't. You can call refreshSandbox to find out, and since I made the video there's also an opt-in webhook that keeps the table in sync for you. So just be aware of that if you go and stop something on the Daytona dashboard.

And obviously the usual things. If you're going to use this in production make sure you rate limit it and authenticate it, etc, etc.

Conclusion

So just to recap: Convex can't run Python, Rust or npm install, a Daytona sandbox is a throwaway computer that can, and the component turns every sandbox and every command into a row you can query live. It's about 7 cents an hour for a default sandbox, with $200 free to get you started.

What I like most is that it still feels very Convex'y, as you're just writing actions and queries, and all the messy stuff happens over in a box that you can throw away.

All three apps are open source if you want to have a look: the App Builder, Chat with your CSV and the Rust Fixer. These are just three use cases tho, I can think of an almost infinite number.

Until next time, cheerio! 👋

All gas, no breakages

Convex is the reactive backend platform that keeps up with you and your agents. Database, functions, workflow, sync, search, file storage, and more. All TypeScript, zero glue.

Get started