How I Built an AI Interior Design App
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
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
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
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
expiresAtset 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:
| Stage | Duration |
|---|---|
| Upload & format conversion | 1-2s |
| Room validation (Gemini) | 2-3s |
| Room analysis | 3-4s |
| Generate 3 designs (parallel) | 15-20s |
| Upload to R2 | 3-5s |
| Save to MongoDB | 1-2s |
| Total | 25-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 🤖