Back to Blog

Building Wishare: From Frustration to Full-Stack Rails App

January 14, 20255 min read
wisharerailshotwireindie-hacking

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:

  1. Direct fetch with rotating user agents
  2. Fallback to API services for stubborn sites
  3. 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:

  1. Missing Solid Cache table
  2. ActionCable rate limiting
  3. 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 app
  • wishare-ios/ - Swift wrapper with Hotwire Native
  • wishare-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:

  1. Rails 8 is production-ready - Solid Cache, Solid Queue, and Hotwire make it a complete stack
  2. Real-time is table stakes - Users expect live updates; WebSockets aren't optional anymore
  3. URL extraction is a solved problem until it isn't - Every site has different anti-scraping measures
  4. Design systems save time - The upfront investment pays off within weeks
  5. 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.