I recently launched Dressora, an AI clothes changer that swaps outfits onto a single photo for virtual try-on. The product side is fun, but the parts I actually sweated over were the boring backend bits: orchestrating multiple AI providers, handling long-running generation jobs, and building a credit system that never double-charges or loses money. Here's what I learned.
AI providers change pricing, rate limits, and quality constantly. Hardcoding one is a trap. I put everything behind a small factory:
const provider = getProvider("evolink");
const task = await provider.createTask({ prompt, aspectRatio });
Each provider implements the same interface (createTask
, handleCallback
, status mapping). Swapping or adding a provider is a new file, not a refactor. When one provider had an outage, switching the default was a one-line env change.
AI generation takes 10s–minutes. Blocking a request is a non-starter. The flow:
generate()
— create a DB record, handleCallback()
— download the result, re-upload to R2, mark complete, The frontend just polls a lightweight status endpoint. The webhook is the source of truth.
A gotcha: always re-upload the provider's output to your own storage. Provider URLs expire. Down and pushing to R2 on completion saved me from dead links later.
Money + concurrency + async failures = the scariest combination. The pattern that worked: freeze → settle / release.
freeze(credits)
— move credits to a "held" statesettle()
— actually consume themrelease()
— give them back
freeze -> hold created, balance reserved
settle -> hold consumed (success)
release -> hold returned (failure)
This way a failed generation never costs the user, and a user can't fire 10 concurrent jobs with credits for one. I also did FIFO consumption across credit packages so credits with the nearest expiry get used first — fairer for users and simpler for accounting.
If you want to see the end result, it's live at aiclotheschanger.me. Happy to answer questions about the architecture in the comments.