How I Built an AI Interior Design App

Koushith

How I Built an AI Interior Design App?

Recently i moved into a new apartment and it looked like a hospital waiting room. White walls, beige carpet, furniture from three different IKEA trips that somehow don't match. I kept meaning to fix it but couldn't visualize what "better" actually looked like.

ChatGPT suggested "add some plants and a statement rug." Cool, thanks. That's like telling someone who can't cook to "just add flavor."

I wanted to see what my space could become. So I built a thing.

I wanted the flow to be very simple:

Upload a photo of your room. Pick a style. Get back three redesigned versions that keep your walls, windows, and doors exactly where they are. No AI hallucinating extra doorways or removing your ceiling fan.

Built the whole thing in 3 weeks using Claude Code for rapid development. Not a 3-month side project. Three weeks of focused building.

The Architecture

Click to expand

Vercel's serverless functions timeout at 10 seconds. AI generation takes 30+. Non-negotiable.

Zero egress fees. S3 would bankrupt me with this many images thats why i went with Cloudflare R2.

The schema changed every week during development. SQL migrations would've killed momentum. mongo db is a good fit for this.

User Flow

Click to expand

Image Processing Pipeline

I thought the hard part would be AI prompting. Nope. It's dealing with whatever garbage users upload.

Someone sends you a 25MB HEIC file from their iPhone 15 Pro Max. Gemini doesn't know what to do with that. Your bandwidth bill cries. The request times out.

The fix:


1. Detect format from data URL

2. If HEIC/HEIF → convert to JPEG using heic-convert

3. Resize to max 4096x4096 (Gemini's limit)

4. Compress with mozjpeg at 92% quality

5. Result: 15MB → 400KB

Storage costs dropped 90%. Timeouts stopped.

The Three-Stage AI Pipeline

I don't just throw images at Gemini and hope.

Stage 1: Validation

Ask Gemini 2.0 Flash: "Is this actually a room?" If confidence < 60%, reject it. No point burning API credits on someone's photo of their dog.

Stage 2: Analysis

Before generating anything, understand the space:

  • Room type and dimensions

  • Structural elements (doors, windows, beams)

  • Current style assessment

  • Three recommended styles that would work

This context feeds into generation and lets me explain why certain styles were recommended.

Stage 3: Generation

Gemini 2.5 Flash Image Preview does the actual work. The prompt structure matters:


Transform this [room_type] into [style] style.



CRITICAL: Keep ALL doors, windows, walls EXACTLY where they are.

DO NOT move, add, or remove structural elements.

Maintain 60-90cm walking space between furniture.



Style: [name]

Colors: [palette]

Materials: [list]

Key elements: [list]



[custom user instructions if any]

Three variations generated in parallel with Promise.all(). If one fails, the others still complete.

21 Design Styles

Each style isn't just a name. It's a full configuration:


{

name: 'Japandi',

description: 'Japanese minimalism meets Scandinavian comfort',

colorPalette: ['#F5F2ED', '#D4C4B0', '#8B7355', '#2F2F2F', '#FFFFFF'],

keyElements: ['Low furniture', 'Natural textures', 'Minimal decor', 'Zen elements'],

materials: ['Bamboo', 'Rice paper', 'Natural linen', 'Light oak'],

lighting: 'Soft, diffused lighting with paper lanterns',

mood: 'Tranquil, balanced, and mindfully curated'

}

The AI uses all of this context. The style selection logic:

  • User picked a style? Use that first, add 2 complementary styles

  • Reshuffle mode? Random selection from all 21

  • No preference? Use the AI-recommended styles from the analysis step

Styles include: Modern Contemporary, Scandinavian, Industrial Chic, Luxury Modern, Mid-Century Modern, Japandi, Coastal Modern, Bohemian Luxe, Minimalist, Traditional Classic, Rustic Farmhouse, Art Deco, Mediterranean, Contemporary Eclectic, French Country, Urban Modern, Transitional, Tropical Paradise, Hollywood Glam, Nordic Minimalism, and a custom option.

Uploading to R2 Without the AWS SDK

Cloudflare R2 is S3-compatible but I didn't want the entire AWS SDK for something this simple. Implemented AWS Signature V4 signing manually:


1. Build canonical request (method, path, headers, payload hash)

2. Create string to sign (algorithm, timestamp, scope, request hash)

3. Generate signing key (HMAC chain: secret → date → region → service)

4. Sign the string

5. PUT request with Authorization header

Each image gets uploaded twice: WebP (primary) and PNG (fallback). Both in parallel. Public URLs go straight to Cloudflare's CDN with 1-year cache headers.

The Credit System

Three tiers:

  • Free: 5 generations per day (resets at midnight)

  • Starter: 25 one-time credits ($9.99), never expire

  • Pro: 60 per month with rollover ($19.99/month)

The tricky part: which credits get used first? If someone has purchased credits AND a Pro subscription, what happens?


Priority order:

1. Bonus credits (admin gifts, promo codes) - use first, they're free

2. Monthly subscription allowance - expires end of month, use before they vanish

3. Purchased credits - paid real money, preserve as long as possible

Key insight: deduct credits after successful generation, not before. Otherwise you're charging people for failed requests.

Promo Codes

Built a simple promo system for marketing and customer support:


PromoCode {

code: "LAUNCH50"

credits: 10

maxRedemptions: 100

currentRedemptions: 0

expiresAt: Date

redeemedBy: [userId, ...] // prevent double redemption

}

When someone redeems a code, credits go into promoRedeemedCredits bucket. Tracked separately so I can see which codes are actually driving usage.

Plan Upgrades

What happens when a Starter user upgrades to Pro? Their purchased credits don't disappear. They become backup credits that kick in after the monthly allowance runs out. Users keep what they paid for.

Regional Pricing

Not everyone pays in USD. The system detects region through:


1. CloudFlare headers (fast, usually accurate)

2. Client-provided timezone

3. IP geolocation APIs as fallback (ipapi.co, ip-api.com, ipinfo.io)

India gets INR pricing (₹599 for Starter, ₹999/month for Pro). US gets USD ($9.99, $19.99). DodoPayments handles the currency conversion.

Security check: validate that the payment amount matches expected regional price within 5% tolerance. Prevents VPN abuse.

Payment Flow

Click to expand

Soft Delete and Storage Management

I can't keep every generated image forever. But deleting immediately makes users angry.

Solution: soft delete with 3-day grace period.


User clicks delete → isDeleted = true, deletedAt = now

Cron job runs every 6 hours:

- Find images where isDeleted = true AND deletedAt < 3 days ago

- Delete from R2

- Remove from database



Exception: "liked" images skip deletion entirely

This nudges users to curate their favorites while keeping storage costs sane.

Likes and Favorites

The "like" button does more than feel good. It's tied directly to storage policy.


GeneratedImage {

isLiked: false // user clicked heart

isPinned: false // saved to moodboard

isDeleted: false // soft deleted

expiresAt: Date // null if liked or pinned

}

Liked images:

  • Never auto-delete

  • Show up in "Favorites" tab on dashboard

  • Can be added to moodboards

Unliked images:

  • Get expiresAt set to 3 days from creation

  • Cron job cleans them up

  • Warning shown: "Like to keep forever"

This turns storage management into a feature. Users engage more because there's a reason to curate.

Moodboards

Users can organize designs into collections. Good for planning a full home renovation or comparing styles.


Moodboard {

userId

name: "Living Room Ideas"

description: optional

isPublic: false // shareable link if true

items: [MoodboardItem, ...]

}



MoodboardItem {

moodboardId

imageId // ref to GeneratedImage

position: { x, y } // for drag-drop layout

notes: optional

}

When an image is added to a moodboard, isPinned gets set to true. Pinned images never expire, even if not liked.

Shareable moodboards have a public URL. Good for sharing with partners, designers, or contractors.

Admin Panel

I needed visibility without diving into MongoDB Compass every time something broke.

Built a protected /admin route with:

User Lookup

  • Search by email

  • View their generated images

  • See plan, credits, usage stats

  • Gift credits manually (for bug compensation)

Analytics Dashboard

  • Generations today/week/month

  • Error rate (goal: < 5%)

  • New signups

  • Revenue (from DodoPayments webhooks)

  • Most popular styles

Error Monitoring

  • Real-time error log (auto-refreshes every 30s)

  • Filter by error code (403, 429, 500)

  • Color-coded severity

  • Direct links to user profiles for debugging

Content Moderation

  • View all generated images

  • Flag/delete inappropriate content

  • Ban users if needed

Promo Code Management

  • Create new codes

  • Set redemption limits

  • Track which codes are performing

The admin check is simple:


user.isAdmin === true // set manually in DB for now

No fancy role system. Just a boolean. If I need more granularity later, I'll add it.

Error Handling

Gemini returns 503s and 429s under load. Instead of failing immediately:


for attempt 1 to 3:

try:

return gemini.generate(prompt, image)

catch error:

if error is 503 or 429:

wait (2^attempt) seconds # 1s, 2s, 4s

continue

else:

throw error

User-facing errors get translated:

  • SAFETY → "Image flagged by content filter"

  • quota → "High demand, try again in a few minutes"

  • anything else → "Something went wrong. Credits not charged."

Custom Furniture Integration

Users can upload photos of furniture they own (or want to buy) and have it incorporated into the design.


content = [

{ text: main_prompt },

{ image: room_photo }

]



for each furniture_photo:

content.append({ image: furniture_photo })

content[0].text += "Incorporate furniture from image N into the design"



gemini.generate(content)

No fine-tuning required. Gemini's multimodal context window handles reference images directly.

Webhook Handling

Payment webhooks can be retried. DodoPayments might send the same event twice.


on webhook received:

event_id = webhook.id



if Payment.exists(webhook_events contains event_id):

return "already processed"



process payment

Payment.webhook_events.push(event_id)

Idempotency prevents double-crediting users.

Console Easter Eggs

Because why not.

Open dev tools and you'll find:

  • ASCII art cat welcome message

  • roomcraft.meow() plays an actual meow sound (base64 encoded audio)

  • roomcraft.designTip() returns real interior design advice like "Use the 60-30-10 color rule" or "Place mirrors opposite windows to bounce light"

  • roomcraft.showStyles() lists all 21 design styles

  • roomcraft.surprise() random fun facts

The Konami code (up up down down left right left right B A) triggers:

  • Rainbow hue-rotation effect on the whole page

  • 50 confetti particles falling with rotation animation

  • Console message: "You're awesome!"

There's also a hidden promo code (CURIOUS_CAT) for people curious enough to check the console. Rewards exploration.

What I'd Do Differently

Start with rate limiting. I added it after a surge of requests blew through my API quota. Should've been there from day one.

Test with real iPhone photos earlier. HEIC handling was an afterthought that became critical infrastructure.

Build the admin panel sooner. I was digging through MongoDB Compass for way too long. A simple dashboard to view generations, gift credits, and check error rates would've saved hours.

The Numbers

Generation timeline breakdown:

StageDuration
Upload & format conversion1-2s
Room validation (Gemini)2-3s
Room analysis3-4s
Generate 3 designs (parallel)15-20s
Upload to R23-5s
Save to MongoDB1-2s
Total25-36s

After a month of running:

  • Success rate: 94% (up from 71% before retry logic)

  • Storage per user: ~12MB (down from ~180MB before optimization)

  • Most popular style: Modern Contemporary (32%)

  • R2 vs S3 savings: ~$500/month on egress fees

The app works. People use it. I can finally see what my apartment could look like with furniture that matches.


Built by Koushith Amin. The code is messier than this post suggests. used llms for grammar, typos etc 🤖