Skip to content

feat(convex): webhook-driven sandbox state sync - #83

Open
mislavivanda wants to merge 4 commits into
mainfrom
feat/convex-webhooks
Open

mislavivanda wants to merge 4 commits into
mainfrom
feat/convex-webhooks

Conversation

@mislavivanda

@mislavivanda mislavivanda commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by cubic

Adds webhook-driven sandbox state sync so records update in real time when Daytona changes sandboxes on its own (auto-stop, auto-archive, auto-delete), instead of lagging until refreshSandbox is called. The feature is opt-in and off by default.

  • Routes Daytona's sandbox.state.updated deliveries to the component's webhook route (mounted via httpPrefix) and applies them to tracked sandbox records.
  • Verifies every delivery's HMAC signature using the Svix/Standard Webhooks scheme, rejects timestamps older than 5 minutes to block replays, fails closed when DAYTONA_WEBHOOK_SECRET isn't passed down, and returns 400 on signed-but-malformed payloads instead of crashing.
  • Dedupes duplicate and out-of-order deliveries, and discards late events, via a new remoteUpdatedAt timestamp — which API-observed writes also stamp, so a webhook can't overwrite newer state; events for untracked sandboxes and non-state events are acknowledged and ignored.
  • Setup: set DAYTONA_WEBHOOK_SECRET on your deployment, pass it down in the app config, and point a Daytona webhook endpoint (in the same organization as your DAYTONA_API_KEY) at https://<deployment>.convex.site/daytona/webhook.

Written for commit b545691. Summary will update on new commits.

View guided diff Turn on auto-fix

Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/convex/src/component/webhooks.ts">

<violation number="1" location="packages/convex/src/component/webhooks.ts:140">
P2: The staleness guard only compares against `remoteUpdatedAt`, which is written exclusively by this mutation — every other writer of the row (`upsertSandbox`, `start`, `stop`, `refresh`, `remove`) updates `state` without touching it. A retried or out-of-order delivery arriving within the 5-minute tolerance can therefore be applied even when it is older than state that was already pulled from the API (e.g. a late `stopped` event with `eventTime` 10:28 overwrites a `start` action that observed `started` at 10:30, because the last applied webhook time is older than 10:28). Stamp `remoteUpdatedAt` (e.g. `Date.now()`, or the API's `updatedAt` when available) on every successful API-observed write in `sandboxes.ts` so the discard rule also covers pull-based sync, or the docstring's "older ones are discarded" guarantee only holds for webhook-vs-webhook ordering.</violation>
</file>

This PR changes authentication, authorization, or input validation. Ultrareviews find 2.4x more serious bugs than standard reviews. Comment @cubic-dev-ai ultrareview to run one.

Fix all with cubic | Re-trigger cubic

Comment thread packages/convex/src/component/http.ts
if (!sandbox) return "ignored-unknown";
if (
sandbox.remoteUpdatedAt !== undefined &&
args.eventTime <= sandbox.remoteUpdatedAt

@cubic-dev-ai cubic-dev-ai Bot Oct 2, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The staleness guard only compares against remoteUpdatedAt, which is written exclusively by this mutation — every other writer of the row (upsertSandbox, start, stop, refresh, remove) updates state without touching it. A retried or out-of-order delivery arriving within the 5-minute tolerance can therefore be applied even when it is older than state that was already pulled from the API (e.g. a late stopped event with eventTime 10:28 overwrites a start action that observed started at 10:30, because the last applied webhook time is older than 10:28). Stamp remoteUpdatedAt (e.g. Date.now(), or the API's updatedAt when available) on every successful API-observed write in sandboxes.ts so the discard rule also covers pull-based sync, or the docstring's "older ones are discarded" guarantee only holds for webhook-vs-webhook ordering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/convex/src/component/webhooks.ts, line 140:

<comment>The staleness guard only compares against `remoteUpdatedAt`, which is written exclusively by this mutation — every other writer of the row (`upsertSandbox`, `start`, `stop`, `refresh`, `remove`) updates `state` without touching it. A retried or out-of-order delivery arriving within the 5-minute tolerance can therefore be applied even when it is older than state that was already pulled from the API (e.g. a late `stopped` event with `eventTime` 10:28 overwrites a `start` action that observed `started` at 10:30, because the last applied webhook time is older than 10:28). Stamp `remoteUpdatedAt` (e.g. `Date.now()`, or the API's `updatedAt` when available) on every successful API-observed write in `sandboxes.ts` so the discard rule also covers pull-based sync, or the docstring's "older ones are discarded" guarantee only holds for webhook-vs-webhook ordering.</comment>

<file context>
@@ -0,0 +1,151 @@
+    if (!sandbox) return "ignored-unknown";
+    if (
+      sandbox.remoteUpdatedAt !== undefined &&
+      args.eventTime <= sandbox.remoteUpdatedAt
+    ) {
+      return "ignored-stale";
</file context>
Fix with cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in 2c319dd. Every API-observed state write (upsertSandbox, which backs create/start/stop/refresh/remove, plus setSandboxError when it records a state) now stamps remoteUpdatedAt, so the discard rule covers pull-based sync too: a late event older than state already observed via the API is ignored. New test: seed via the API path, deliver an event timestamped 30s earlier, assert ignored-stale. The stamp uses Convex's clock rather than Daytona's (API-observed writes don't carry Daytona's timestamp through every path), so the tradeoff is that an event emitted within clock skew of an observation may be discarded; the next real state change re-syncs it.

Comment thread packages/convex/scripts/live.mjs Outdated
…writes

- validate the parsed payload is an object and that id, newState and the
  timestamp are non-empty strings; signed-but-malformed deliveries get 400
  instead of crashing the action or failing argument validation
- every API-observed state write (upsertSandbox, setSandboxError) stamps
  remoteUpdatedAt, so a late webhook event older than state already
  pulled from the API is discarded rather than overwriting it
- live script asserts CONVEX_SITE_URL is present instead of failing
  with an opaque URL parse error

Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 1 new issue found across 4 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/convex/src/component/sandboxes.ts">

<violation number="1" location="packages/convex/src/component/sandboxes.ts:173">
P3: This stores local API-observation times in `remoteUpdatedAt`, but `schema.ts` describes the field only as Daytona’s timestamp for the last applied webhook. Update that field comment to document both sources so readers do not assume the ordering watermark is always provider-sourced.

(Based on your team's feedback about tracking API-observed sandbox state.)</violation>
</file>

Comment thread packages/convex/src/component/sandboxes.ts
@mislavivanda

Copy link
Copy Markdown
Collaborator Author

Verified end-to-end with real Daytona webhook deliveries (in addition to unit tests and the live suite):

  • Cloud Convex dev deployment with the component mounted at https://<deployment>.convex.site/daytona/webhook; endpoint created in the Daytona dashboard subscribed to sandbox.state.updated; its signing secret (whsec_…, shown on the endpoint's details page) set as DAYTONA_WEBHOOK_SECRET.
  • Created a sandbox through the component (record: started), then stopped it from the Daytona dashboard, with no component calls in between. The record flipped to stopped from the delivery alone, and remoteUpdatedAt advanced to Daytona's event updatedAt (2026-10-08T21:26:05.070Z), confirming signature verification against a real dashboard-issued secret and the ordering guard on real payloads.
  • Before the secret was set, the public route refused deliveries (fail-closed); afterwards, unsigned requests got 401.

Also confirmed along the way: webhook events are per-organization, so the endpoint must live in the same org as the API key the component uses (worth knowing when debugging "no events arrive").

…et is shown

Both confirmed during the real-delivery test: the endpoint must live in
the API key's organization, and the signing secret is on the endpoint's
details page in the Webhooks table.

Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>
Signed-off-by: Mislav Ivanda <mislavivanda454@gmail.com>

This branch is waiting to be deployed

1 waiting (outdated) deployment
integration-tests — ae6b35f6 Waiting Oct 2, 2026 by mislavivanda via convex #150
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant