Prompt-Led Product | For PMs Building in the AI Era

Prompt-Led Product | For PMs Building in the AI Era

How to Add Image Uploads to a Shared Workspace Without Wrecking Your Database

Most builders discover the costs of wrong implementation three weeks after shipping. Here is the architecture I used for DraftKit that stopped it before it started.

Elena | AI Product Leader's avatar
Elena | AI Product Leader
May 26, 2026
∙ Paid

Check out previous Build Series. Part 1 | Part 2 | Part 3 | Part 4 | Part 5

I held back image uploads in DraftKit.app for months because I was afraid of the hosting costs. The real number turned out to be 0.05% of subscription revenue.

Here is what the cost analysis looked like, why I did the math before writing a single line of code, and the three bugs that hit during implementation anyway.

DraftKit’s Shared Workspace has had image support wired in the backend since launch. The TipTap editor was ready. The toolbar button did not exist because I had deliberately not turned it on. The reason was a real technical concern:

Pasting rich HTML content back into Substack.com had historically dropped any images that came from a third-party domain, which would mean every image a user added in DraftKit would disappear the moment they copied the draft to publish.

Then a high-volume user called Dinah from Code Like A Girl told me what was actually happening because of that decision. She was sending images out-of-band, in a separate folder, labeled by number. She said:

“Knowing that an image goes between these two paragraphs is meaningfully different from a folder called Image 3.”

She was right. The workaround was breaking the whole point of a shared workspace inside the app.

I tested the Substack compatibility concern before I wrote a single line of upload code. Copying HTML content with externally hosted images and pasting into Substack: the images survived. The holding condition no longer existed.

The only question left was whether I could host images in a way that did not create a cost problem I could not control.

So I ran the numbers.


If this is the kind of product decision audit you need before building, I do them. Email me at hello@elenacalvillo.com and let’s look at your build.

Builders Fear Image Costs They Have Not Modeled

The fear of image hosting costs is widespread among indie builders and small product teams. It shows up as features that get indefinitely deferred, or as Pro-tier gates on functionality that every competing product gives away for free. The fear is rational in the abstract.

Yet in practice is because most builders have not done the actual math.

The infrastructure economics for DraftKit break down like this.

  • Supabase charges $0.021 per GB per month for storage and $0.09 per GB per month for egress.

  • With client-side compression targeting a 1 MB output maximum, the average optimized web image lands at around 400 KB.

A heavy user publishing 15 posts a month with 5 images each generates roughly 30 MB of new storage per month and about 160 MB of egress during the active collaboration window.

Annual infrastructure cost for that heavy user: $0.249.

At 1,000 Pro users on a medium-average profile, the annual infrastructure cost is $94. Annual revenue at $14.99/month is $180,000.

Image hosting costs 0.05% of subscription revenue at every scale point I modeled.

The only scenario that changes the economics meaningfully is unoptimized uploads: a user uploading raw camera photos at 15 MB each increases cost approximately 37 times per image.

That is the one real risk, and it is solved by a single client-side library before a file ever reaches the server.

The three decisions that came out of the cost analysis:

✅ Client-side compression to 1 MB maximum is required, not optional.

✅ Images go to all users, free and Pro. Cost is not the constraint.

✅ Risk is managed by a technical guardrail, not by access control.

The Pattern Is the Same Across Every Indie Product

A builder defers a feature because the cost feels unbounded. The users find a workaround. The workaround is worse than the feature. The builder finally runs the numbers and ships it.

Why Your Build Instinct Gets This Wrong

The instinct is to treat image hosting as an open-ended cost risk because storage accumulates over time and egress is variable. Both of those things are true. They are also true of every other data your product stores.

The difference is that builders rarely model the actual numbers before making a deferral decision. They defer on vibes and ship on deadlines.

The access decision for images is also a product positioning question, not a cost question. Every writing tool a newsletter creator compares DraftKit to gives images to everyone.

Gating images behind Pro does not reduce your cost meaningfully. It does reduce the experience of your free tier in a way that keeps users anchored to other tools.

Share

The Approach I Follow

✅ Confirm the compatibility constraint before building anything. The reason DraftKit held images back was a real Substack behavior that had changed. Testing the actual end-to-end workflow before writing upload code took 15 minutes and eliminated the blocking assumption.

✅ Model the cost before making a feature decision. The analysis used real Supabase pricing, real user behavior profiles, and a specific risk scenario (unoptimized uploads). It took less than an hour. The output changed a months-long deferral into a clear decision.

✅ Compress client-side, before the file reaches the server. The browser-image-compression library handles this in a Web Worker so the editor never freezes. Target 1 MB maximum output, 1920px longest edge. Every input path (toolbar, drag-and-drop, paste) goes through the same compression flow.

✅ Encode your authorization model into the storage path. DraftKit uses {request_id}/{timestamp}_{filename}. The first path segment is the workspace ID. The storage RLS policy reads that segment and checks it against the collab_requests table directly. If you are not a participant in that workspace, the upload is rejected at the storage layer.

✅ Test the full flow on production before calling it shipped. Three distinct bugs appeared during implementation that were invisible in the local environment. Each one required a separate fix. The feature was confirmed working by Dinah herself testing all three insertion methods on the live product.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 © 2026 Elena Calvillo | AI Product Leader · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture