|
| 1 | +# Convex × Trigger.dev: prompt-driven image editing |
| 2 | + |
| 3 | +A small demo by Trigger.dev: choose one of ten bundled photographs, describe a modification, and get a private, downloadable edited image. |
| 4 | + |
| 5 | +**Public app:** https://ardent-rabbit-965.convex.site |
| 6 | + |
| 7 | +## Current state |
| 8 | + |
| 9 | +Backend, frontend, and Trigger tasks are deployed, and **image editing is enabled**. All Trigger.dev dependencies are pinned to `4.8.3` from npmjs, including `@trigger.dev/convex` and `@trigger.dev/react-hooks`. |
| 10 | + |
| 11 | +A live Realtime verification completed the prepare → model edit → parallel exports workflow in **26.2 seconds**, streamed real phase metadata, requested authoritative Convex synchronization on completion, and verified all three private AVIF downloads. Unauthorized token issuance, cross-session refresh, cross-run token reads, and unauthenticated image access were rejected. The readable task payload contained no worker credential. A separate browser run verified the React subscription, terminal refresh, and final image delivery. |
| 12 | + |
| 13 | +`AI_GATEWAY_API_KEY` belongs only in Trigger environment variables, never Convex, Vite, source files, or the browser. |
| 14 | + |
| 15 | +## Page flow |
| 16 | + |
| 17 | +The page has three vertical steps: choose an image, describe the edit, and view/download the result. Only one source photo is shown; a native dialog offers the other photographs. Export settings, internal task details, and budget counters are deliberately not exposed as controls. |
| 18 | + |
| 19 | +A fresh visit starts with an empty prompt and result. Old results appear only after an explicit submission or history selection, never merely because their inputs match. The current draft, selected job, and submission request ID are saved for this browser session. Refreshing restores the exact photo, prompt, and job; an owned active job is also recovered from Convex when local editor state is missing. The request ID finds accepted work even if the browser missed its submission response. Recovery does not automatically enqueue another generation. |
| 20 | + |
| 21 | +Restored results are labeled, and **New edit** clears the prompt/result while retaining the selected photo. Every disabled Submit state explains its reason. Selecting a previous edit restores its inputs. The page caches all generated variants privately, while showing just the primary image. |
| 22 | + |
| 23 | +During work, one progress bar and three labels, **Prepare → Edit → Finish**, show actual workflow progress. There are no elapsed-time estimates or artificial progress increments. Convex still controls job completion and when verified results become visible. |
| 24 | + |
| 25 | +## Actual Trigger workflow |
| 26 | + |
| 27 | +The parent task `convex-edit-image` orchestrates: |
| 28 | + |
| 29 | +1. **Prepare** (`convex-image-prepare`): load the selected bundled JPEG, strip metadata, and downsize to at most 1024 pixels per side. |
| 30 | +2. **Edit** (`convex-image-model-edit`): send the photo and prompt to `meta/muse-image-1.0` through Vercel AI Gateway. Request exactly one image, with no automatic paid retries. Normalize the result to a bounded PNG. |
| 31 | +3. **Export** (`convex-image-export`): fan out three child tasks concurrently, creating 1024 × 768, 800 × 600, and 400 × 400 AVIF versions using Sharp. |
| 32 | +4. **Finish**: the parent collects successful exports. The component's terminal callback verifies the files, publishes safe metadata, and releases the admission lock. |
| 33 | + |
| 34 | +Child tasks have deterministic job-specific idempotency keys. Every task has one attempt. A failed export does not rerun the image model automatically. There are no fake timers or artificial delays. |
| 35 | + |
| 36 | +Convex transactionally admits the request and enqueues the Trigger component operation. For the selected active job, the browser uses `useRealtimeRun` with an ownership-authorized, exact-run token. Parent metadata reports each real phase; export children set idempotent completion flags. A terminal Realtime event invokes the ownership-checked `jobs.refresh` mutation, which calls the component’s `trigger.refresh()` API. It never forwards browser-provided status or output. |
| 37 | + |
| 38 | +The component still polls Trigger every 15–60 seconds as recovery when browsers close or Realtime is unavailable. Registered artifacts also update Convex reactively. Progress is held at or below 95 until the completion callback verifies all files. |
| 39 | + |
| 40 | +## Privacy and retention |
| 41 | + |
| 42 | +- Each browser creates a cryptographically random 256-bit session capability. The server stores only its hash and scopes every job query to it. |
| 43 | +- No shared gallery, public result query, or cross-user prompt cache. |
| 44 | +- Images are served through an authenticated HTTP action, not public Convex storage URLs. Downloads require the browser's capability in an Authorization header. Tokens never go in image URLs. |
| 45 | +- Trigger workers derive a job-scoped HMAC capability from `DEMO_ARTIFACT_SECRET`, configured only in Trigger and Convex server environments. No worker capability or shared secret appears in a task payload, metadata, or output. Browsers cannot upload artifacts or read intermediate files. |
| 46 | +- Browser Realtime tokens authorize only the owned parent run, expire within ten minutes or job expiry, and remain in memory. Realtime omits payload/output columns, but this is not REST protection: the task payload must still be credential-free. Legacy jobs created before this migration cannot receive Realtime tokens. |
| 47 | +- Task payloads and outputs contain references/metadata, not image bytes. Provider errors are sanitized to avoid recording image bodies in task errors. Prompt/task metadata is still available to trusted operators in private dashboards; this is not end-to-end encryption. |
| 48 | +- The browser fetches final images as Blobs, previews them with object URLs, and keeps up to 20 jobs in session-scoped IndexedDB. Server URLs are not persisted in the local cache. |
| 49 | +- Normal server files and job records expire one hour after admission. Intermediate files are removed on completion, and failed jobs have their registered files removed immediately. An hourly indexed sweep collects unregistered files left by process interruption, normally within two hours of upload. |
| 50 | +- Root Convex storage is dedicated to this demo's artifacts. Static hosting uses isolated component storage. Scope the collector before introducing unrelated root-storage files. |
| 51 | + |
| 52 | +Losing the browser session key loses access to pending remote jobs. Clearing local history does not erase the session key or immediately erase unexpired remote jobs. Browser storage can be evicted; downloads are the reliable way to retain files permanently. If IndexedDB is unavailable, current-page download links still work. |
| 53 | + |
| 54 | +The image provider receives the selected bundled image and prompt. There are no visitor uploads or arbitrary input URLs. |
| 55 | + |
| 56 | +## Spending and abuse limits |
| 57 | + |
| 58 | +- Fixed model, exactly one generated image, and prompts capped at 500 characters. |
| 59 | +- **500 lifetime admitted edits and 50 per UTC day**, enforced atomically before enqueueing. Counters persist across deployments and browser sessions. |
| 60 | +- At the catalog's current $0.01/image rate, 500 generations is approximately **$5 in model charges**, excluding compute, storage, and traffic. These are admission limits, not a guarantee against future provider price changes. |
| 61 | +- One outstanding job globally, a shared 20-second cooldown, five-minute task queue TTLs, and bounded execution durations. |
| 62 | +- No automatic retries at either the Trigger task or AI SDK level. Failed jobs still consume their reserved slot, including definite pre-model failures. No automatic refunds on ambiguous failures. |
| 63 | +- Fixed image inputs, pixel limits, bounded streamed artifact uploads, and no user-selected model/provider/size/count. |
| 64 | +- `DEMO_ENABLED` is an operator kill switch. The manual deployment procedure pauses new work until deployment and hosting checks finish. |
| 65 | +- Optional Turnstile is implemented. Set both `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY` in Convex to require a successful challenge with the deployment's hostname. A partially configured pair fails closed. Without these keys, spending caps still apply but CAPTCHA is not active. |
| 66 | + |
| 67 | +These measures bound admitted image-generation work. They are not DDoS protection for public Convex endpoints. Use additional traffic controls before broad promotion. |
| 68 | + |
| 69 | +## Setup and local development |
| 70 | + |
| 71 | +This is a standalone, private pnpm workspace under `packages/convex/example`. Its dependencies, tests, assets, and build output are excluded from the `@trigger.dev/convex` package build and npm tarball. Run its commands from this directory, not the monorepo root. |
| 72 | + |
| 73 | +```sh |
| 74 | +corepack pnpm install --frozen-lockfile |
| 75 | +corepack pnpm dev |
| 76 | +corepack pnpm dev:convex |
| 77 | +``` |
| 78 | + |
| 79 | +Without `VITE_CONVEX_URL`, the page is an honest read-only preview. |
| 80 | + |
| 81 | +Current development deployment: `chris-arderne:tiny-worlds:dev/demo`, https://ardent-rabbit-965.convex.cloud. The historical Convex project name is unchanged. [Convex dashboard](https://dashboard.convex.dev/d/ardent-rabbit-965). |
| 82 | + |
| 83 | +Trigger project: `convex-image-processing`, reference `proj_wcwcgcodbajkuocrovpq`. CLI profile **test**, environment **prod on test cloud**, not production cloud. [Trigger dashboard](https://test-cloud.trigger.dev/projects/v3/proj_wcwcgcodbajkuocrovpq). |
| 84 | + |
| 85 | +Use Node.js 24 and pnpm 10.33.2. Authenticate the Convex CLI for the selected deployment and the Trigger CLI's `test` profile for test cloud. Ensure `.env.local` contains the selected deployment's `VITE_CONVEX_URL` and `VITE_CONVEX_SITE_URL` (see `.env.example`). Add `AI_GATEWAY_API_KEY` to the Trigger environment above before deploying tasks. |
| 86 | + |
| 87 | +`configure` safely resolves the project's Trigger credentials from the CLI profile, creates or reuses the server-only artifact secret, and injects Convex configuration over stdin without printing secret values. Run it before the first task deployment so worker authentication is configured. Omit `--enable` to keep new work paused. For a different Trigger project, deliberately update the config and operator helper. |
| 88 | + |
| 89 | +Follow the manual deployment steps below. The helper and `deploy:trigger` command target the existing test-cloud project; for another project, update `trigger.config.ts` and `scripts/cloud.ts` first. |
| 90 | + |
| 91 | +Disable new work with: |
| 92 | + |
| 93 | +```sh |
| 94 | +corepack pnpm exec convex env set DEMO_ENABLED false |
| 95 | +``` |
| 96 | + |
| 97 | +If component synchronization pauses, restore credentials and invoke the authenticated internal `jobs:resume` mutation with the job ID. Visitors cannot invoke it. |
| 98 | + |
| 99 | +## Manual deployment |
| 100 | + |
| 101 | +There is no demo CI or automatic deployment. Run these commands from `packages/convex/example`; no GitHub environments or repository secrets are required. They update the existing demo's selected **development** Convex deployment and its hosted frontend. |
| 102 | + |
| 103 | +Install dependencies, check the example, pause new edits, and configure server credentials before deploying: |
| 104 | + |
| 105 | +```sh |
| 106 | +corepack pnpm install --frozen-lockfile |
| 107 | +corepack pnpm test |
| 108 | +node scripts/cloud.ts configure |
| 109 | +corepack pnpm exec convex dev --once --typecheck enable |
| 110 | +corepack pnpm deploy:trigger --local-bundle |
| 111 | +node scripts/cloud.ts status |
| 112 | +``` |
| 113 | + |
| 114 | +Confirm the Trigger status is `DEPLOYED` before continuing. Then build/upload the frontend and check its public assets: |
| 115 | + |
| 116 | +```sh |
| 117 | +corepack pnpm upload:site |
| 118 | +node scripts/verify-hosting.ts |
| 119 | +``` |
| 120 | + |
| 121 | +`upload:site` runs the frontend build. Once deployment and hosting checks pass, explicitly enable new edits and check readiness without creating a generation: |
| 122 | + |
| 123 | +```sh |
| 124 | +node scripts/cloud.ts configure --enable |
| 125 | +node scripts/verify.ts |
| 126 | +``` |
| 127 | + |
| 128 | +Optionally run one paid end-to-end verification: |
| 129 | + |
| 130 | +```sh |
| 131 | +node scripts/verify.ts --launch --realtime |
| 132 | +``` |
| 133 | + |
| 134 | +`--realtime` checks scoped token access, consumes live run updates, and requests a Convex refresh on the terminal frame. `--launch` explicitly spends one generation. The stable operator session/request is reused while the server job exists. If Turnstile is configured, use the browser for live generation; the CLI does not bypass the challenge. |
| 135 | + |
| 136 | +`verify.ts` without `--launch` checks readiness and hosted samples without creating a model job. With an existing verification job it checks that job and authenticated downloads. Tests cover ownership of token issuance and refresh, job-scoped worker authentication, Realtime progress identity/clamping, cross-session isolation, authenticated file delivery/expiry, orphan cleanup, limits/deduplication, invalid result handling, native AVIF decoding, and bounded model-call configuration. |
| 137 | + |
| 138 | +Live browser recovery QA submitted a Coast edit with Enter, refreshed mid-generation, left the page entirely for 35 seconds, and returned to the same job and verified result. Draft restoration, Shift+Enter, New edit/reset persistence, and explicit history selection were also checked without additional paid work. |
| 139 | + |
| 140 | +Convex does not currently support renaming the generated `*.convex.site` subdomain ([feature request](https://ship.convex.dev/requests/js7ch5fteyhskr871g4w9ce5n58e1zh7)). An owned [custom domain](https://docs.convex.dev/production/custom-domains) requires Convex Pro and DNS configuration. Changing origins also creates a separate browser session/history unless a migration is provided. |
| 141 | + |
| 142 | +`upload:site` updates the selected development frontend. `deploy:site` targets the default production deployment and deploys backend plus frontend. Do not confuse them. |
| 143 | + |
| 144 | +The ten frozen JPEGs in `public/samples` come from Unsplash; source URLs are in `shared/catalog.ts`. See the [Unsplash license](https://unsplash.com/license). `node scripts/download-samples.ts` refreshes and bounds them to 1600 pixels per side; `--resize-only` normalizes existing local files without fetching again. |
| 145 | + |
| 146 | +The example source lives alongside the component in `packages/convex/example` and syncs with the public repository. It is not included in published package bundles. |
0 commit comments