Building Wishare: From Frustration to Full-Stack Rails App
The Problem That Started It All
Every birthday and Christmas, the same chaos: "What do you want?" texts flying around, duplicate gifts, awkward returns. Existing wishlist apps either required everyone to create accounts, had clunky UIs, or felt abandoned.
I wanted something simple: share a wishlist link, let people see what you want, mark items as "getting this" to avoid duplicates. How hard could it be?
Turns out, building it right took four months and taught me more about Rails 8 than any tutorial.
Day One: The Foundation
September 7, 2025. I ran rails new wishare and made my first commit: "Initial Rails setup with PostgreSQL and Tailwind CSS".
The stack was intentional:
- Rails 8 - I wanted to use the latest features, especially Solid Cache and Solid Queue
- PostgreSQL - No surprises here
- Tailwind CSS - Fast styling without fighting CSS frameworks
- Hotwire - Rails' answer to React, and I wanted to learn it properly
The Authentication Rabbit Hole
My second commit was Configure Devise authentication. What I thought would be a quick setup became a week of decisions:
- Should I use Devise or build auth from scratch?
- How do I handle Google OAuth without making email-only users feel second-class?
- What about the invite flow—users need to access wishlists before signing up
I went with Devise + Google OAuth. The invite system was trickier: I needed to let users receive invitations, view wishlists, and only require signup when they wanted to create their own lists.
# The connection model that ties users together
class Connection < ApplicationRecord
belongs_to :user
belongs_to :connected_user, class_name: 'User'
enum :status, { pending: 0, accepted: 1, blocked: 2 }
end
The URL Metadata Challenge
Here's where things got interesting. A wishlist app needs to show nice previews when you paste an Amazon or Etsy link. Sounds simple—just fetch the OpenGraph tags, right?
Wrong.
Brazilian e-commerce sites (my primary market) are aggressive about blocking scrapers. Centauro, Sephora Brasil, Casas Bahia—they all detected my requests and served blank pages or CAPTCHAs.
I built a three-tier extraction system:
- Direct fetch with rotating user agents
- Fallback to API services for stubborn sites
- Global caching to avoid hitting sites repeatedly
class UrlMetadataExtractor
def extract(url)
# Check global cache first (shared across all users)
cached = GlobalMetadataCache.find_by(url: normalized_url(url))
return cached.metadata if cached&.fresh?
# Try extraction strategies in order
metadata = try_direct_fetch(url) ||
try_api_service(url) ||
minimal_metadata(url)
GlobalMetadataCache.upsert(url: normalized_url(url), metadata: metadata)
metadata
end
end
The caching was crucial: if one user adds an Amazon link, every other user benefits from that cached metadata. My database became a shared knowledge base of product information.
Going Real-Time with ActionCable
The breakthrough feature was the activity feed. I wanted users to see, in real-time, when friends added items or updated their wishlists.
ActionCable made this possible, but the implementation had surprises. My first attempt created duplicate subscriptions on mobile browsers—every time the app came back from background, it opened a new WebSocket connection.
class ActivityFeedChannel < ApplicationCable::Channel
def subscribed
# Rate limiting to prevent subscription spam
return reject if rate_limited?
stream_for current_user
end
def self.broadcast_activity(user, activity)
user.friends.each do |friend|
broadcast_to(friend, activity.as_json)
end
end
end
The real lesson came during production debugging. Users reported infinite loading on the dashboard. The Railway logs showed PG::UndefinedTable: solid_cache_entries—Rails 8's new caching system needed a migration I'd missed.
That led to three separate bug fixes:
- Missing Solid Cache table
- ActionCable rate limiting
- Inconsistent data between HTTP fallback and WebSocket feeds
The Design System Evolution
Around commit 50, I realized my UI was inconsistent. Buttons had different styles, colors varied between pages, and dark mode was half-broken.
I stopped feature development and built a proper design system:
# app/components/ui/button_component.rb
class UI::ButtonComponent < ViewComponent::Base
VARIANTS = {
primary: "bg-rose-500 hover:bg-rose-600 text-white",
secondary: "bg-dark-800 hover:bg-dark-700 text-white",
ghost: "bg-transparent hover:bg-dark-800 text-dark-300"
}
def initialize(variant: :primary, size: :md, **attrs)
@variant = variant
@size = size
@attrs = attrs
end
end
This refactor took a week but paid off immediately. New features now looked consistent automatically.
Mobile: Hotwire Native
The final push was mobile apps. Instead of building separate React Native apps, I used Hotwire Native (formerly Turbo Native). The Rails app became the single source of truth, with thin native wrappers for iOS and Android.
The architecture:
wishare-core/- The Rails appwishare-ios/- Swift wrapper with Hotwire Nativewishare-android/- Kotlin wrapper with Hotwire Native
90% of the code is shared. Native features like camera access and push notifications are bridged through JavaScript.
What I Learned
Four months of building Wishare taught me:
- Rails 8 is production-ready - Solid Cache, Solid Queue, and Hotwire make it a complete stack
- Real-time is table stakes - Users expect live updates; WebSockets aren't optional anymore
- URL extraction is a solved problem until it isn't - Every site has different anti-scraping measures
- Design systems save time - The upfront investment pays off within weeks
- Ship incrementally - My first deploy was auth + basic wishlists. Everything else came after users were using it
What's Next
Wishare is live and growing. The roadmap includes:
- Smarter gift suggestions based on wishlist patterns
- Group gifting for expensive items
- Better URL extraction for international sites
The code is private for now, but I'll share more technical deep-dives as I solve interesting problems.
Building something? I'd love to hear about it. Reach out on GitHub or LinkedIn.