Skip to content

[convex] Feedback from building a few example apps on 1.1.0 #74

Description

@mikecann

Hey @mislavivanda, thanks again for turning #69 around so fast, runBackground is super nice!

For the video I had my agents build three little example apps on top of the component (a mini Lovable, a "chat with your CSV" Python one, and a Rust one that keeps fixing compiler errors until it builds). They all work really well, but a few things came up along the way, roughly in order of how much they bit us:

1. The API key gets stored in the scheduled function args

runBackground and pollExecution pass config (including apiKey) to ctx.scheduler.runAfter, so the Daytona key gets written in plain text to the component's _scheduled_functions table on every poll, and Convex keeps those records for 7 days after they run.

It also means that if you rotate the key while a background command is running, the polls keep using the old one, fail 3 times and the execution gets marked as failed. The cleanup deleteSession uses the old key too, so the command actually keeps running in the sandbox.

Not sure what the nicest fix is given components can't read the app's env vars, but maybe the client could pass a function handle (e.g. to an internal query that returns the config) instead of the key itself, so only the handle ends up in the scheduled args?

2. No completion callback for runBackground

The reactive execution row is great for the UI, but the backend has no way to know when the command has finished. All three apps ended up writing their own scheduled "check the row and reschedule" loop on top of the component's poller. Something like an onComplete: internal.myModule.buildFinished function handle (similar to Workpool) would remove that second layer.

3. Stale sandbox state

Same as option 3 on #69: once Daytona auto-stops a sandbox the record still says started, which made "how many sandboxes are live" a guess in our rate limiting.

4. Smaller ones

  • deleteSandbox throws if Daytona already auto-deleted the sandbox (404), and the record never gets marked destroyed. stopSandbox and refreshSandbox already handle the 404, so it would be nice if delete did too.
  • listSandboxes only takes userKey and limit (max 500, no pagination), so there's no filtering by state or label. There's also no way to delete old destroyed sandbox or execution rows, so they pile up forever.
  • readFile / writeFile are UTF-8 strings only, so we had to save matplotlib charts as SVG. A bytes variant (v.bytes()) would help for images and other binaries.
  • The poll backoff (1s doubling up to 10s) isn't configurable, so a 0.3s cargo build always shows up about 1.2s later, and long jobs only update their logs every 10s.
  • There's no way to cancel a running background command (we delete the whole sandbox instead). The poller already calls deleteSession when it gives up, so maybe exposing that as a cancelExecution wouldn't be too bad?

The example repos are public now if they're useful for testing:

Cheers!

Activity

  1. mislavivanda commented on Oct 2, 2026

    @mislavivanda
    Collaborator

    Roadmap update now that #75 is released as 2.0.0 — here's how we're sequencing the rest of this issue:

    Next PR (in final review now): background-execution QoL

    • Item 2 — onComplete: runBackground accepts a mutation function handle (Workpool-style) plus an onCompleteContext passthrough; the component's poller invokes it on every terminal state (completed, failed, and the new cancelled), so backends no longer need a second polling loop on top of ours.
    • Item 4 — cancelExecution: atomic claim (racing the poller's finish throws instead of overwriting), kills the session so the command actually stops, fires onComplete with cancelled.
    • Item 4 — deleteSandbox 404 now marks the record destroyed, matching stop/refresh.
    • Item 4 — configurable polling: minPollMs / maxPollMs on runBackground (clamped 250ms–120s), so a fast cargo build can resolve in ~0.5s and long jobs can poll lazily.
    • Item 4 — row hygiene: purgeExecutions / purgeSandboxes (batched, terminal rows only) plus a state filter and cursor-paginated listSandboxesPaginated — your "how many sandboxes are live" rate-limit check becomes listSandboxes(ctx, { state: 'started' }).

    After that, as its own PR: Item 3 — webhook-driven state sync. The component will ship the HTTP handler + signature verification (mounted via registerRoutes, secret passed down through component env like the API key), and we're investigating registering the webhook programmatically via Daytona's webhooks API so setup stays dashboard-free.

    Deferred pending design: binary readFile/writeFile. It'll land as additive *Bytes variants (v.bytes()), with Daytona's pre-signed URLs as the story for payloads beyond Convex's function size limits.

    And yes please on the example repos — three real consumers make a great regression suite. Thanks again Mike, this issue has been the most productive feedback loop of the launch.

  2. mikecann commented on Oct 3, 2026

    @mikecann
    ContributorAuthor

    Awesome thanks @mislavivanda, that roadmap sounds great, onComplete and cancelExecution especially will let me rip out a bunch of polling code from these. Here are the three example repos:

    They are still on the pre-2.0.0 version so they might need a little nudge to upgrade, let me know if you hit anything weird. Cheers!

  3. mislavivanda commented on Oct 9, 2026

    @mislavivanda
    Collaborator

    Hey @mikecann, all of the items from the issue are now implemented and published in the newest 2.1.0 version. I think the opt-in option for state sync with Daytona webhooks will be really useful.

    Thank you for your great feedback and suggestions which resulted in significant improvements of this component and will ultimately leave to better experience & usability for Convex users.

    Closing the issue and feel free to lift new FR in the future if you notice anything else that might be useful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions