Building RapVideoGo with TanStack Start and Cloudflare: Beyond the AI API Call The developer behind RapVideoGo, a web app that generates personalized rap videos from reference photos and text prompts, detailed how the product handles the failure cases between its interface and an external AI video provider. Built on TanStack Start and Cloudflare with a D1 database, the app uses atomic batched credit deduction and task creation, database guards against concurrent credit reads, idempotency keys and saved requests to avoid duplicate paid provider jobs, and a claim step that prevents repeated failure notifications from restoring credits twice. An AI video product has a deceptively simple interface: upload a photo, describe an idea, and click Generate. Behind that button, the application needs to answer several questions. What happens if the user submits twice? What if the provider accepts a request but the connection drops? When should credits be deducted, and how should they be restored after a failure? I’m the creator of RapVideoGo https://rapvideogo.com/ , a web application for making personalized rap videos from reference photos and text prompts. This post covers some of the implementation decisions behind it, particularly the parts that sit between the interface and the generation provider. The application uses: I built on an existing SaaS foundation for accounts, payments, and credits. That let me focus product development on the creation workflow and its failure cases. The video model runs through an external provider. The application’s responsibility is to turn a user’s creative input into a valid request, track its progress, and keep the surrounding account state consistent. The basic task lifecycle is: pending → processing → success ↘ failed A task record stores the owner, generation settings, reference image URLs, prompt, provider information, and credit cost. This record gives the application a durable identity for the work. The browser does not have to be the only place that remembers a generation is running. Repeated requests also need more than a check that an ID already exists. In RapVideoGo, the existing task must match the requesting user, product module, and request fingerprint. Reusing an ID with different input produces a conflict. That distinction matters: retrying the same request should recover existing work, while changing the request should not silently reuse an unrelated task. One consistency problem is easy to describe: Deduct credits Create task If the second operation fails, the user has spent credits without getting a task. Reversing the order creates a different partial state: a task without its corresponding charge. The implementation plans credit consumption first, then inserts the task and applies the credit operations through one atomic database operation. On D1, the project’s database helper uses a batch of statements. Cloudflare documents that a failed statement in a batch causes the sequence to roll back. Reference: Cloudflare D1 batch documentation https://developers.cloudflare.com/d1/worker-api/d1-database/ batch . Reading a balance before writing is not enough, however. Two requests can read the same available credits. The credit operations therefore include database guards. If another request consumes credits that the current operation expected to use, the guard prevents the stale plan from completing. Task creation can then re-plan within a bounded retry loop. The provider request happens separately. A database batch cannot make an external API call transactional, so provider submission still needs its own recovery logic. A timeout does not always mean the provider rejected the job. There is a meaningful difference between these cases: Automatically submitting again in the second case could create duplicate paid work. The implementation distinguishes these situations. Where the provider supports an idempotency key, recovery uses the same task identity and saved request. Other paths require waiting for confirmation or expiration rather than blindly creating another job. Task updates are conditional, too. A completion update checks the expected state and provider task ID, helping prevent an older response from overwriting a newer state. Failure handling also needs repeat protection. The credit restoration path claims the original consumption record before restoring credits. A repeated failure notification should not restore the same charge twice. This does not make every external failure disappear. It gives the application explicit rules for handling uncertainty. Another problem lives closer to the interface. Imagine a duet with two reference slots: @1 performs the opening line. @2 responds. If the user uploads the second slot first, upload completion order cannot become performer order. The application preserves explicit slot numbers and derives the active reference list from those slots. A simplified version of the helper looks like this: function activePhotos