<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shaher Shamroukh</title>
    <description>The latest articles on DEV Community by Shaher Shamroukh (@shahershamroukh).</description>
    <link>https://dev.to/shahershamroukh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F508938%2F7b59ffb1-d88a-405f-8dbe-46996a2aa250.png</url>
      <title>DEV Community: Shaher Shamroukh</title>
      <link>https://dev.to/shahershamroukh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shahershamroukh"/>
    <language>en</language>
    <item>
      <title>AWS vs DigitalOcean for SaaS: Why We Chose DigitalOcean for a Production Rails App</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Wed, 20 May 2026 19:34:22 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/aws-vs-digitalocean-for-saas-why-we-chose-digitalocean-for-a-production-rails-app-8oi</link>
      <guid>https://dev.to/shahershamroukh/aws-vs-digitalocean-for-saas-why-we-chose-digitalocean-for-a-production-rails-app-8oi</guid>
      <description>&lt;p&gt;There is a moment every SaaS founder hits early on.&lt;/p&gt;

&lt;p&gt;You open AWS, look at the console, and it feels like you have walked into an airport control tower.&lt;/p&gt;

&lt;p&gt;Everything is powerful. Everything is configurable. Everything is also overwhelming.&lt;/p&gt;

&lt;p&gt;Then you open DigitalOcean and think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This looks too simple to be serious.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We had the same reaction while building RobinReach, a Ruby on Rails SaaS handling social media scheduling, background jobs, API integrations, media processing, and multi-tenant data.&lt;/p&gt;

&lt;p&gt;And we still chose DigitalOcean.&lt;/p&gt;

&lt;p&gt;Not because AWS is worse.&lt;/p&gt;

&lt;p&gt;But because better infrastructure is not the same thing as a better decision for your stage.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Wrong Question Most Engineers Ask
&lt;/h1&gt;

&lt;p&gt;Most infrastructure debates start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which is better, AWS or DigitalOcean?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But in real SaaS building, that is not the right question.&lt;/p&gt;

&lt;p&gt;The real question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which system lets us move faster without increasing operational complexity?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because early-stage SaaS products do not fail due to lack of scalability.&lt;/p&gt;

&lt;p&gt;They fail due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow iteration cycles
&lt;/li&gt;
&lt;li&gt;operational overhead
&lt;/li&gt;
&lt;li&gt;DevOps complexity
&lt;/li&gt;
&lt;li&gt;decision fatigue
&lt;/li&gt;
&lt;li&gt;fragile deployments
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infrastructure is rarely the bottleneck early on.&lt;/p&gt;

&lt;p&gt;Complexity is.&lt;/p&gt;




&lt;h1&gt;
  
  
  What We Actually Needed for RobinReach
&lt;/h1&gt;

&lt;p&gt;RobinReach is not a simple CRUD app.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby on Rails API backend
&lt;/li&gt;
&lt;li&gt;Sidekiq background job processing
&lt;/li&gt;
&lt;li&gt;Redis queues
&lt;/li&gt;
&lt;li&gt;PostgreSQL database
&lt;/li&gt;
&lt;li&gt;Media processing for images and videos
&lt;/li&gt;
&lt;li&gt;OAuth integrations with multiple social platforms
&lt;/li&gt;
&lt;li&gt;Scheduled publishing across timezones
&lt;/li&gt;
&lt;li&gt;Multi-tenant architecture
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, this sounds like AWS territory.&lt;/p&gt;

&lt;p&gt;But in reality, none of this requires AWS level infrastructure on day one.&lt;/p&gt;

&lt;p&gt;What it requires is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable compute
&lt;/li&gt;
&lt;li&gt;reliable background jobs
&lt;/li&gt;
&lt;li&gt;predictable deployments
&lt;/li&gt;
&lt;li&gt;simple observability
&lt;/li&gt;
&lt;li&gt;low operational friction
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  AWS: What Looks Powerful vs What You Actually Feel
&lt;/h1&gt;

&lt;p&gt;AWS is undeniably powerful.&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infinite scaling options
&lt;/li&gt;
&lt;li&gt;global infrastructure
&lt;/li&gt;
&lt;li&gt;deep service ecosystem like S3, ECS, Lambda
&lt;/li&gt;
&lt;li&gt;enterprise grade security
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But early-stage SaaS reality looks very different.&lt;/p&gt;

&lt;p&gt;What you actually feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC design decisions before product market fit
&lt;/li&gt;
&lt;li&gt;IAM complexity from day one
&lt;/li&gt;
&lt;li&gt;service fragmentation
&lt;/li&gt;
&lt;li&gt;networking overhead
&lt;/li&gt;
&lt;li&gt;slower deployment workflows
&lt;/li&gt;
&lt;li&gt;DevOps dependency creeping in early
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS is optimized for scale.&lt;/p&gt;

&lt;p&gt;But early-stage startups are not a scaling problem yet.&lt;/p&gt;

&lt;p&gt;They are a speed problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Hidden Cost of AWS: Cognitive Load
&lt;/h1&gt;

&lt;p&gt;The real cost of AWS is not money.&lt;/p&gt;

&lt;p&gt;It is cognitive overhead.&lt;/p&gt;

&lt;p&gt;Every small decision becomes a system design problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which service should we use here
&lt;/li&gt;
&lt;li&gt;How should these services communicate
&lt;/li&gt;
&lt;li&gt;Why did this IAM role break production
&lt;/li&gt;
&lt;li&gt;Why is deployment slower than expected
&lt;/li&gt;
&lt;li&gt;Why do we need five services for something simple
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Individually, these problems are solvable.&lt;/p&gt;

&lt;p&gt;But together, they create friction.&lt;/p&gt;

&lt;p&gt;And friction slows iteration.&lt;/p&gt;

&lt;p&gt;In SaaS, iteration speed is everything.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why DigitalOcean Worked Better for Us
&lt;/h1&gt;

&lt;p&gt;DigitalOcean did not feel powerful.&lt;/p&gt;

&lt;p&gt;It felt predictable.&lt;/p&gt;

&lt;p&gt;And that predictability matters more than people think.&lt;/p&gt;

&lt;p&gt;With DigitalOcean, we could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploy Rails apps quickly
&lt;/li&gt;
&lt;li&gt;run Sidekiq workers without orchestration complexity
&lt;/li&gt;
&lt;li&gt;manage PostgreSQL without infrastructure overhead
&lt;/li&gt;
&lt;li&gt;scale vertically in a controlled way
&lt;/li&gt;
&lt;li&gt;focus on product instead of infrastructure design
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly:&lt;/p&gt;

&lt;p&gt;We reduced the number of decisions we had to make outside of product work.&lt;/p&gt;

&lt;p&gt;That alone increased our shipping speed.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Real Decision Was Not Technical, It Was Strategic
&lt;/h1&gt;

&lt;p&gt;We did not choose DigitalOcean because it is simpler.&lt;/p&gt;

&lt;p&gt;We chose it because it reduced unnecessary system complexity at our current stage.&lt;/p&gt;

&lt;p&gt;At this stage of RobinReach, the most valuable resource is not infrastructure power.&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;uninterrupted product iteration.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  The Scaling Myth
&lt;/h1&gt;

&lt;p&gt;There is a common belief in engineering culture:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Start with AWS so you can scale later.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But in practice, most SaaS companies do not fail because they chose the wrong cloud provider.&lt;/p&gt;

&lt;p&gt;They fail because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they over engineered too early
&lt;/li&gt;
&lt;li&gt;they slowed down before finding product market fit
&lt;/li&gt;
&lt;li&gt;they optimized systems they did not yet need
&lt;/li&gt;
&lt;li&gt;they lost iteration speed
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scaling is not the first problem.&lt;/p&gt;

&lt;p&gt;Survival is.&lt;/p&gt;




&lt;h1&gt;
  
  
  When AWS Actually Makes Sense
&lt;/h1&gt;

&lt;p&gt;This is not an anti AWS argument.&lt;/p&gt;

&lt;p&gt;AWS becomes the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traffic volume becomes consistently large
&lt;/li&gt;
&lt;li&gt;multi region deployment is required
&lt;/li&gt;
&lt;li&gt;enterprise compliance is mandatory
&lt;/li&gt;
&lt;li&gt;infrastructure specialization is needed
&lt;/li&gt;
&lt;li&gt;DevOps becomes a dedicated function, not a side task
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, AWS is not overkill.&lt;/p&gt;

&lt;p&gt;It is necessary.&lt;/p&gt;

&lt;p&gt;But early on, it can easily become premature complexity.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Hidden Part Nobody Talks About: Billing Complexity
&lt;/h1&gt;

&lt;p&gt;One of the biggest reasons we chose DigitalOcean is not only infrastructure simplicity.&lt;/p&gt;

&lt;p&gt;It is billing clarity.&lt;/p&gt;

&lt;p&gt;Because in early SaaS, the real pain is not cost itself.&lt;/p&gt;

&lt;p&gt;It is unpredictable cost and unclear billing logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  AWS: Powerful, but requires interpretation
&lt;/h2&gt;

&lt;p&gt;AWS billing is not a single number per server.&lt;/p&gt;

&lt;p&gt;It is a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EC2 compute usage
&lt;/li&gt;
&lt;li&gt;EBS storage
&lt;/li&gt;
&lt;li&gt;S3 storage and requests
&lt;/li&gt;
&lt;li&gt;data transfer in and out
&lt;/li&gt;
&lt;li&gt;load balancers
&lt;/li&gt;
&lt;li&gt;NAT gateways
&lt;/li&gt;
&lt;li&gt;logging and monitoring services
&lt;/li&gt;
&lt;li&gt;region based pricing differences
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is not that AWS is expensive.&lt;/p&gt;

&lt;p&gt;The problem is:&lt;/p&gt;

&lt;p&gt;You often only understand your bill after you have already been charged.&lt;/p&gt;

&lt;p&gt;Even small changes in architecture can affect cost in non obvious ways.&lt;/p&gt;




&lt;h2&gt;
  
  
  DigitalOcean: simple, predictable, boring in a good way
&lt;/h2&gt;

&lt;p&gt;DigitalOcean takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed monthly pricing for compute
&lt;/li&gt;
&lt;li&gt;clear pricing for databases
&lt;/li&gt;
&lt;li&gt;predictable bandwidth limits
&lt;/li&gt;
&lt;li&gt;simple storage pricing
&lt;/li&gt;
&lt;li&gt;no hidden service cross billing complexity
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need to decode your bill.&lt;/p&gt;

&lt;p&gt;You already know what it will be.&lt;/p&gt;

&lt;p&gt;That predictability changes how you build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters more than people think
&lt;/h2&gt;

&lt;p&gt;At early stage SaaS, the biggest cost is not infrastructure.&lt;/p&gt;

&lt;p&gt;It is mental bandwidth.&lt;/p&gt;

&lt;p&gt;If you constantly need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explain your cloud bill
&lt;/li&gt;
&lt;li&gt;debug unexpected cost spikes
&lt;/li&gt;
&lt;li&gt;optimize infra before you have scale
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are not building product.&lt;/p&gt;

&lt;p&gt;You are managing infrastructure uncertainty.&lt;/p&gt;




&lt;h2&gt;
  
  
  The simple rule we followed
&lt;/h2&gt;

&lt;p&gt;We asked ourselves:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we predict next month’s infrastructure cost within a reasonable range without analysis?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is no, complexity is too high for our stage.&lt;/p&gt;




&lt;h1&gt;
  
  
  Architecture Snapshot (What We Run on DigitalOcean)
&lt;/h1&gt;

&lt;p&gt;A simplified view of our setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rails application for API and web layer
&lt;/li&gt;
&lt;li&gt;Sidekiq workers for background processing
&lt;/li&gt;
&lt;li&gt;Redis for queues and caching
&lt;/li&gt;
&lt;li&gt;PostgreSQL as the primary datastore
&lt;/li&gt;
&lt;li&gt;Object storage for media assets
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup is intentionally simple.&lt;/p&gt;

&lt;p&gt;Not because we cannot handle complexity.&lt;/p&gt;

&lt;p&gt;But because we are optimizing for speed of iteration.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Real Lesson from Building RobinReach
&lt;/h1&gt;

&lt;p&gt;Infrastructure decisions are not about capability.&lt;/p&gt;

&lt;p&gt;They are about tradeoffs between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;flexibility and simplicity
&lt;/li&gt;
&lt;li&gt;power and cognitive load
&lt;/li&gt;
&lt;li&gt;scalability and iteration speed
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And early-stage SaaS has one consistent truth:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The fastest team to iterate usually wins, not the one with the most complex infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Final Thought
&lt;/h1&gt;

&lt;p&gt;AWS did not feel wrong.&lt;/p&gt;

&lt;p&gt;It just felt like solving problems we did not have yet.&lt;/p&gt;

&lt;p&gt;DigitalOcean did not feel impressive.&lt;/p&gt;

&lt;p&gt;It felt invisible.&lt;/p&gt;

&lt;p&gt;And in early-stage SaaS, invisible infrastructure is often exactly what you want.&lt;/p&gt;

&lt;p&gt;Because the less you think about infrastructure,&lt;/p&gt;

&lt;p&gt;the more you can think about building something people actually use.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>digitalocean</category>
      <category>rails</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Production MCP Server in Ruby on Rails, Lessons from RobinReach</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Thu, 30 Apr 2026 20:05:14 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/building-a-production-mcp-server-in-ruby-on-rails-lessons-from-robinreach-4f4c</link>
      <guid>https://dev.to/shahershamroukh/building-a-production-mcp-server-in-ruby-on-rails-lessons-from-robinreach-4f4c</guid>
      <description>&lt;h1&gt;
  
  
  Building a Remote MCP Server in Ruby on Rails, A Production Guide
&lt;/h1&gt;

&lt;p&gt;Most MCP tutorials are Python or Node.js.&lt;/p&gt;

&lt;p&gt;This is the Rails version, built in production, serving real users, powering an AI agent inside a SaaS product.&lt;/p&gt;

&lt;p&gt;Here's the architecture, the key decisions, and the gotchas that cost us time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What MCP Actually Is
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol is a standard for connecting AI models to external tools and data sources.&lt;/p&gt;

&lt;p&gt;Before MCP, you'd give Claude a document and ask it questions. After MCP, Claude can call your API, read your database, take actions in your product, all from a conversation.&lt;/p&gt;

&lt;p&gt;The protocol is simple: JSON-RPC 2.0. Claude sends requests, your server responds. That's it.&lt;/p&gt;

&lt;p&gt;What makes it powerful is the standard — Claude knows how to discover, authenticate with, and call any MCP server that implements the spec correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local vs Remote, Why This Distinction Matters
&lt;/h2&gt;

&lt;p&gt;Most tutorials show &lt;strong&gt;local MCP servers&lt;/strong&gt; a process running on the user's machine, communicating via stdio.&lt;/p&gt;

&lt;p&gt;That works for developer tools. It doesn't work for SaaS.&lt;/p&gt;

&lt;p&gt;For a multi-tenant web application, you need a &lt;strong&gt;remote MCP server&lt;/strong&gt; an HTTP endpoint that authenticates each user and serves their specific data.&lt;/p&gt;

&lt;p&gt;This changes everything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Local (stdio)&lt;/th&gt;
&lt;th&gt;Remote (HTTP)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;stdin/stdout&lt;/td&gt;
&lt;td&gt;HTTP + SSE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;None needed&lt;/td&gt;
&lt;td&gt;OAuth 2.0 required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-user&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;User's machine&lt;/td&gt;
&lt;td&gt;Your server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use case&lt;/td&gt;
&lt;td&gt;Dev tools, CLIs&lt;/td&gt;
&lt;td&gt;SaaS products&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rails is perfect for remote MCP. It's already an HTTP server. It already has auth. You're just adding a new endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude (client)
      ↓  JSON-RPC 2.0 over HTTPS
POST /mcp/v1/messages
      ↓
ApplicationController (auth + plan gating)
      ↓
MCP Tool Handler
      ↓
Your existing Rails services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No new infrastructure. No separate process. No Node.js. Just a new controller that speaks JSON-RPC.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — The Routes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="c1"&gt;# These must be at root level — outside any scope or namespace&lt;/span&gt;

&lt;span class="c1"&gt;# OAuth Discovery — Claude hits these before anything else&lt;/span&gt;
&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/.well-known/oauth-protected-resource"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"mcp/oauth#protected_resource"&lt;/span&gt;
&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/.well-known/oauth-authorization-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"mcp/oauth#authorization_server"&lt;/span&gt;

&lt;span class="c1"&gt;# OAuth Flow&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="s2"&gt;"/mcp/oauth"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s2"&gt;"register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"mcp/oauth#register"&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt;  &lt;span class="s2"&gt;"authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"mcp/oauth#authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :mcp_oauth_authorize&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s2"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"mcp/oauth#token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="ss"&gt;as: :mcp_oauth_token&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# MCP Endpoint&lt;/span&gt;
&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s2"&gt;"mcp/v1/messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"mcp/v1/messages#create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;defaults: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;format: :json&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; The &lt;code&gt;.well-known&lt;/code&gt; routes must exist before you test anything with Claude.ai. When a user adds your integration, Claude hits these discovery endpoints first. Without them the connection silently fails and you'll spend a day wondering why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Authentication
&lt;/h2&gt;

&lt;p&gt;For a SaaS product, you likely already have API keys. Use them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/mcp/v1/base_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Mcp&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;
      &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:authenticate!&lt;/span&gt;

      &lt;span class="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate!&lt;/span&gt;
        &lt;span class="c1"&gt;# Accept token from header or query param&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bearer "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:api_key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

        &lt;span class="vi"&gt;@api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ApiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@api_key&lt;/span&gt;
          &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;jsonrpc: &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;code: &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32_600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unauthorized&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="vi"&gt;@api_key&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:last_used_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="vi"&gt;@current_company&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@api_key&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;company&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: we accept the token from both the &lt;code&gt;Authorization&lt;/code&gt; header and a query param. This matters because some MCP clients pass it differently, and it lets users connect via a simple URL with &lt;code&gt;?api_key=xxx&lt;/code&gt; during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — The JSON-RPC Handler
&lt;/h2&gt;

&lt;p&gt;MCP uses JSON-RPC 2.0. Every request has a &lt;code&gt;method&lt;/code&gt;, optional &lt;code&gt;params&lt;/code&gt;, and an &lt;code&gt;id&lt;/code&gt;. Your response always includes the same &lt;code&gt;id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/mcp/v1/messages_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Mcp&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessagesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseController&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw_post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:no_content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:notification&lt;/span&gt;

        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;jsonrpc: &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ParserError&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;jsonrpc: &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;code: &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32_700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Parse error"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
               &lt;span class="ss"&gt;status: :bad_request&lt;/span&gt;
      &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[MCP] &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;jsonrpc: &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;code: &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32_603&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Internal error"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"initialize"&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;protocolVersion: &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;serverInfo: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"your-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;version: &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="ss"&gt;capabilities: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="ss"&gt;instructions: &lt;/span&gt;&lt;span class="n"&gt;system_prompt&lt;/span&gt;  &lt;span class="c1"&gt;# injected into Claude's context&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"tools/list"&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;YourMcpTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"tools/call"&lt;/span&gt;
          &lt;span class="n"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="sr"&gt;/\Anotifications\//&lt;/span&gt;
          &lt;span class="ss"&gt;:notification&lt;/span&gt;  &lt;span class="c1"&gt;# no response needed&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"Method not found: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;name&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;YourMcpTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@current_company&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
          &lt;span class="ss"&gt;isError: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;system_prompt&lt;/span&gt;
        &lt;span class="c1"&gt;# This is injected into Claude's context at connect time.&lt;/span&gt;
        &lt;span class="c1"&gt;# Use it to tell Claude how to behave, what order to call things,&lt;/span&gt;
        &lt;span class="c1"&gt;# and any domain-specific rules your application has.&lt;/span&gt;
        &lt;span class="c1"&gt;# Keep it focused — Claude reads this on every connection.&lt;/span&gt;
        &lt;span class="no"&gt;YOUR_SYSTEM_PROMPT&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key point:&lt;/strong&gt; Errors from tool calls should return &lt;code&gt;isError: true&lt;/code&gt; with a message in the content, not an HTTP error status. Claude reads the response body, not the HTTP status code. A descriptive error message in JSON is far more useful than a 500 with no body.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — The Tool Design Decision
&lt;/h2&gt;

&lt;p&gt;This is the most important architectural decision you'll make.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — Many small tools:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;list_posts, create_post, delete_post,
get_analytics, search_media, generate_image...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B — One tool, many actions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your_app (action: "list_posts" | "create_post" | ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We went with Option B. Here's why.&lt;/p&gt;

&lt;p&gt;With many tools, Claude has to pick the right tool before starting. With one tool and an &lt;code&gt;action&lt;/code&gt; parameter, Claude can reason about what to do mid-conversation without switching tools.&lt;/p&gt;

&lt;p&gt;It also makes the integration cleaner from the user's perspective — they see one integration in their Claude settings, not a wall of tools.&lt;/p&gt;

&lt;p&gt;The schema looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schema&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"your_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Brief description of what this tool does and when to use it."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;inputSchema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;properties: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;enum: &lt;/span&gt;&lt;span class="no"&gt;ACTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# your list of supported actions&lt;/span&gt;
          &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"The action to perform"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;# ... your other parameters&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5 — OAuth Discovery
&lt;/h2&gt;

&lt;p&gt;For Claude.ai's "Connect" button to work, you need to implement OAuth discovery.&lt;/p&gt;

&lt;p&gt;The good news: you don't need a full OAuth server. You can use OAuth as a thin wrapper around your existing API keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/mcp/oauth_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Mcp&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OauthController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;

    &lt;span class="c1"&gt;# Tells Claude where your MCP server is&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;protected_resource&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;resource: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp/v1/messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;authorization_servers: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;bearer_methods_supported: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Tells Claude how to authenticate&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorization_server&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;issuer: &lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;authorization_endpoint: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp/oauth/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;token_endpoint: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp/oauth/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;registration_endpoint: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp/oauth/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;response_types_supported: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;grant_types_supported: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;code_challenge_methods_supported: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;token_endpoint_auth_methods_supported: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;scopes_supported: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Claude registers itself as a client&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;client_id:                    &lt;/span&gt;&lt;span class="s2"&gt;"claude_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;client_secret:                &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;grant_types:                  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;token_endpoint_auth_method:   &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# User sees this page and approves the connection&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;
      &lt;span class="c1"&gt;# Require the user to be logged in&lt;/span&gt;
      &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;current_user_from_session&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:oauth_return_to&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullpath&lt;/span&gt;
        &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;login_path&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# Generate short-lived authorization code&lt;/span&gt;
      &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;REDIS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"mcp:oauth:code:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;current_user_from_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# Redirect back to Claude with the code&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:redirect_uri&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?code=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;state=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="ss"&gt;allow_other_host: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Claude exchanges code for an access token&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;token&lt;/span&gt;
      &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;REDIS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"mcp:oauth:code:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"invalid_grant"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :bad_request&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="no"&gt;REDIS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"mcp:oauth:code:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# single use&lt;/span&gt;

      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;access_token: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# your existing API key becomes the token&lt;/span&gt;
        &lt;span class="ss"&gt;token_type:   &lt;/span&gt;&lt;span class="s2"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;scope:        &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;base_url&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;protocol&lt;/span&gt;&lt;span class="si"&gt;}#{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host_with_port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_user_from_session&lt;/span&gt;
      &lt;span class="c1"&gt;# Hook into your existing session auth&lt;/span&gt;
      &lt;span class="c1"&gt;# For Devise: User.find_by(id: session["warden.user.user.key"]&amp;amp;.first&amp;amp;.first)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;your existing API key becomes the OAuth access token.&lt;/strong&gt; You don't need a new token system. OAuth is just a wrapper around what you already have. The code is a short-lived Redis key that maps to the real API key. Claude exchanges it once, gets the API key, and uses it as a Bearer token forever after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — The System Prompt
&lt;/h2&gt;

&lt;p&gt;This is the part nobody writes about. The &lt;code&gt;instructions&lt;/code&gt; field in your &lt;code&gt;initialize&lt;/code&gt; response gets injected into Claude's context on every connection.&lt;/p&gt;

&lt;p&gt;Use it to teach Claude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to behave in your domain&lt;/li&gt;
&lt;li&gt;What order to call actions in&lt;/li&gt;
&lt;li&gt;Rules specific to your application&lt;/li&gt;
&lt;li&gt;How to talk to users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few principles we learned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell Claude what to do first.&lt;/strong&gt; "Always call &lt;code&gt;get_account_status&lt;/code&gt; before anything else" — Claude follows this reliably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encode your validation rules.&lt;/strong&gt; If certain actions must happen before others, put it in the system prompt. "Always validate before creating. Never skip this step."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make it dynamic.&lt;/strong&gt; We append user-specific context — their preferred language, brand tone, timezone — at connect time. Claude immediately writes in the right voice without the user having to explain anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it focused.&lt;/strong&gt; The system prompt should be under 500 words. Long prompts dilute Claude's attention. Every sentence should earn its place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Test with curl before testing with Claude. If curl works, Claude will work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://yourapp.com"&lt;/span&gt;
&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_api_key"&lt;/span&gt;

&lt;span class="c"&gt;# 1. Test the MCP handshake&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/mcp/v1/messages"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 2. List your tools&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/mcp/v1/messages"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 3. Call a tool&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/mcp/v1/messages"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "your_app",
      "arguments": { "action": "get_account_status" }
    }
  }'&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 4. Test OAuth discovery&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/oauth-protected-resource"&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/.well-known/oauth-authorization-server"&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these in order. Each one confirms a layer of the stack is working before you move to the next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;.well-known&lt;/code&gt; routes are not optional.&lt;/strong&gt;&lt;br&gt;
This cost us a full day. Add them first. Test them before anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors belong in the response body, not HTTP status codes.&lt;/strong&gt;&lt;br&gt;
Claude reads JSON. Return &lt;code&gt;{ error: "descriptive message" }&lt;/code&gt; with &lt;code&gt;isError: true&lt;/code&gt;. A 500 with no body tells Claude nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The system prompt matters more than the tool schema.&lt;/strong&gt;&lt;br&gt;
We spent too long on the schema and not enough on the prompt. Claude's behavior improved more from prompt changes than from schema changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Redis for lightweight state.&lt;/strong&gt;&lt;br&gt;
User preferences, session data, short-lived codes — Redis handles all of it without schema migrations. Fast, simple, already in your stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential over parallel.&lt;/strong&gt;&lt;br&gt;
We considered letting Claude batch multiple actions. Don't. Claude reasons better when it processes one response before deciding what to do next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test from the user's perspective weekly.&lt;/strong&gt;&lt;br&gt;
Connect to your own MCP server as a real user and have a real conversation. You'll find issues that unit tests miss — awkward response formats, actions that don't chain well, edge cases in your tool schema.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A production MCP server in Rails that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authenticates via existing API keys&lt;/li&gt;
&lt;li&gt;Supports OAuth discovery for one-click connection in Claude.ai&lt;/li&gt;
&lt;li&gt;Serves multiple tenants from a single endpoint&lt;/li&gt;
&lt;li&gt;Reuses all existing Rails services&lt;/li&gt;
&lt;li&gt;Deploys exactly like any other Rails endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No new infrastructure. No Node.js. No separate process.&lt;/p&gt;

&lt;p&gt;Just Rails doing what Rails does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;We built this for RobinReach, a social media management platform where users manage their entire social presence through a conversation with Robin, our AI manager.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://robinreach.com" rel="noopener noreferrer"&gt;&lt;strong&gt;robinreach.com&lt;/strong&gt;&lt;/a&gt; → Settings → Connect Claude&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions? Drop a comment. Happy to go deeper on any part of this.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rails</category>
      <category>mcp</category>
      <category>claude</category>
    </item>
    <item>
      <title>AI Can Write Code But It Still Can’t Think Like a Developer</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Wed, 07 Jan 2026 17:32:16 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/ai-can-write-code-but-it-still-cant-think-like-a-developer-18pb</link>
      <guid>https://dev.to/shahershamroukh/ai-can-write-code-but-it-still-cant-think-like-a-developer-18pb</guid>
      <description>&lt;p&gt;Everyone is talking about AI replacing developers. Some say junior devs won’t even find jobs in 2026. As someone running a dev team at RobinReach, I think this is overblown and here’s why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing Code Is Not the Same as Solving Problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of people think coding is just typing lines of code or building a simple website. Real software development is way more than that. You have to understand what a feature should actually do, how it should behave in tricky situations, track down bugs and find the root cause, design a system that is scalable and maintainable, and make decisions about trade-offs between speed, performance, security, and user experience.&lt;/p&gt;

&lt;p&gt;And when it comes to full web apps, it gets even more complex. You are not just writing HTML or CSS. You have databases, backend logic, routing, state management, APIs, integrations, and frontend frameworks. AI cannot grasp the full system in context or reason about how all the pieces interact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static Websites Versus Full Web Apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where people get confused. A static website is something simple, like a landing page or portfolio. You can set up a WordPress site or a Wix page in a couple of hours without any AI. It’s straightforward and there is nothing revolutionary about AI doing the same thing a little faster.&lt;/p&gt;

&lt;p&gt;Full web apps are a different story. They have databases, backend logic, authentication, APIs, dynamic routing, state management, error handling, and scaling. Building and maintaining these apps requires problem-solving, debugging, and architectural thinking. AI is not ready to do that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Research Says&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A recent MIT CSAIL study reported by IBM Think confirms this. It says that AI can churn out code but cannot think like a software engineer. AI struggles with planning long-term code, understanding an entire codebase, and handling legacy systems or specialized libraries.&lt;/p&gt;

&lt;p&gt;Even the best AI does not remember previous prompts or understand how a project evolves over time. AI can write code but it cannot replace the reasoning, problem-solving, and judgment that real developers provide.&lt;/p&gt;

&lt;p&gt;You can read the full article here: &lt;a href="https://www.ibm.com/think/news/ai-write-code-can-beat-software-engineers" rel="noopener noreferrer"&gt;IBM Think – AI can write code, but can it beat software engineers?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Experience at &lt;a href="https://robinreach.com/" rel="noopener noreferrer"&gt;RobinReach&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with AI tools, my junior developer drives the AI, not the other way around. They review suggestions, debug issues, validate fixes, and make architectural decisions. AI speeds up repetitive work but the thinking, problem-solving, and judgment are still fully human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Future of Development&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The future is not AI replacing developers. It is humans working with AI. Developers who learn to use AI effectively will be faster and more productive. But understanding systems, reasoning through problems, and building robust software will always be human work.&lt;/p&gt;

&lt;p&gt;Are we overestimating AI, or is the future really about developers guiding AI to solve real software problems?&lt;/p&gt;

</description>
      <category>coding</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Managing Multiple Brands in Rails: Multi-Tenant Patterns from RobinReach</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Mon, 22 Dec 2025 20:47:02 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/managing-multiple-brands-in-rails-multi-tenant-patterns-from-robinreach-10ln</link>
      <guid>https://dev.to/shahershamroukh/managing-multiple-brands-in-rails-multi-tenant-patterns-from-robinreach-10ln</guid>
      <description>&lt;p&gt;Scaling a SaaS application is one thing; letting a single user manage multiple brands, each with its own isolated data, is another. &lt;br&gt;
At &lt;strong&gt;RobinReach&lt;/strong&gt;, we faced this challenge head-on: users needed to handle multiple workspaces, each with its own social profiles, posts, and team members, all while switching seamlessly between them. &lt;br&gt;
Here’s how we built it in Rails, and the lessons learned along the way.&lt;/p&gt;

&lt;p&gt;The Challenge: &lt;strong&gt;Multi-Brand Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a user managing three different companies on your platform. Each company has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own social media profiles&lt;/li&gt;
&lt;li&gt;Scheduled posts&lt;/li&gt;
&lt;li&gt;Team members and roles&lt;/li&gt;
&lt;li&gt;Analytics and performance data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without proper isolation, cross-tenant data leaks become a nightmare. &lt;br&gt;
Multi-tenancy is not just a database pattern, it’s a mindset for every layer of your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workspace-Based Multi-Tenancy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;RobinReach&lt;/strong&gt;, we modeled each brand or workspace as a tenant. All core models—Posts, SocialProfiles, Members, are scoped to a tenant. We use a thread-safe Current object to hold the tenant context:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;class Current &amp;lt; ActiveSupport::CurrentAttributes&lt;br&gt;
  attribute :company&lt;br&gt;
end&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This allows all models to reference the current tenant easily:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;class Post &amp;lt; ApplicationRecord&lt;br&gt;
  belongs_to :company&lt;br&gt;
  default_scope { where(company_id: Current.company.id) }&lt;br&gt;
end&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
With this pattern, every query automatically respects tenant boundaries.&lt;/p&gt;

&lt;p&gt;Seamless Workspace Switching&lt;/p&gt;

&lt;p&gt;Users expect to switch between workspaces without logging out. We store the current workspace in the session:&lt;br&gt;
&lt;code&gt;def switch_workspace(company_id)&lt;br&gt;
  session[:current_company_id] = company_id&lt;br&gt;
  Current.company = Company.find(company_id)&lt;br&gt;
end&lt;/code&gt;&lt;br&gt;
This ensures that every action like viewing posts, scheduling content, or managing members, is scoped correctly to the selected workspace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background Jobs in a Multi-Tenant Environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multi-tenancy isn’t just about database queries; background jobs must also respect tenant boundaries. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned:&lt;/strong&gt;&lt;br&gt;
Always pass the tenant ID to background jobs.&lt;/p&gt;

&lt;p&gt;Make jobs idempotent to prevent cross-tenant side effects.&lt;/p&gt;

&lt;p&gt;Use Sidekiq queues strategically; high-volume tenants might need separate queues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Practices for Workspace Multi-Tenancy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforce tenant isolation at all layers – models, services, jobs, and controllers.&lt;/li&gt;
&lt;li&gt;Use thread-safe context objects (Current) for tenant information.&lt;/li&gt;
&lt;li&gt;Design for fast workspace switching in the UI.&lt;/li&gt;
&lt;li&gt;Keep background jobs idempotent and tenant-aware.&lt;/li&gt;
&lt;li&gt;Cache per tenant when appropriate to reduce database load.&lt;/li&gt;
&lt;li&gt;Monitor performance per workspace to detect heavy tenants early.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building a multi-brand SaaS in Rails isn’t just a technical challenge, it’s a design mindset.&lt;br&gt;
RobinReach demonstrates that with careful scoping, background job design, and workspace isolation, you can deliver a seamless multi-tenant experience.&lt;/p&gt;

&lt;p&gt;For Rails developers building SaaS platforms, these patterns can save you from future headaches and set your app up for scale.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>rails</category>
      <category>saas</category>
    </item>
    <item>
      <title>Building an AI Social Media Manager with Ruby on Rails: Architecture, Automation, and Lessons Learned</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Thu, 23 Oct 2025 19:56:33 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/building-an-ai-social-media-manager-with-ruby-on-rails-architecture-automation-and-lessons-3lpj</link>
      <guid>https://dev.to/shahershamroukh/building-an-ai-social-media-manager-with-ruby-on-rails-architecture-automation-and-lessons-3lpj</guid>
      <description>&lt;p&gt;Building an AI Social Media Manager with Ruby on Rails: Architecture, Automation, and Lessons Learned&lt;/p&gt;

&lt;p&gt;In a previous post, &lt;a href="https://dev.to/shahershamroukh/how-were-building-a-social-media-empire-with-rails-and-sidekiq-5p0"&gt;How We’re Building a Social Media Empire with Rails and Sidekiq&lt;/a&gt;, &lt;br&gt;
I shared how we scaled background jobs and task automation using Rails and Sidekiq. That article focused on how &lt;a href="https://robinreach.com/" rel="noopener noreferrer"&gt;RobinReach&lt;/a&gt;&lt;br&gt;
Our social media management platform handled large-scale post scheduling and media processing.&lt;/p&gt;

&lt;p&gt;This time, we’re taking a deeper dive into how we integrated AI into our architecture and how Ruby on Rails continues to be the backbone of everything we build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Rails Still Works for AI SaaS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even in 2025, Ruby on Rails remains a perfect fit for startups building fast, scalable, and flexible SaaS products. When we started building RobinReach, we needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move fast with a clean architecture.&lt;/li&gt;
&lt;li&gt;Handle complex background automation.&lt;/li&gt;
&lt;li&gt;Support multi-tenant data isolation for agencies.&lt;/li&gt;
&lt;li&gt;Integrate AI models that evolve quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails gave us that foundation. With Hotwire and Tailwind CSS, we kept the frontend lean and responsive, no heavy SPAs needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Service-Driven Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of our early decisions was to adopt a service-driven architecture. Every core component from AI post generation to video rendering lives inside a dedicated service class.&lt;/p&gt;

&lt;p&gt;That means each domain (posts, media, analytics, automation) can evolve independently while staying testable and predictable.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Ai::ContentGeneratorService
 def initialize(post)
  @post = post
 end

 def call
  prompt = "Create a social media caption about #{@post.topic} in an engaging tone."
  response = RubyLLM::Client.new(model: "gpt-4o-    mini").complete(prompt)
 @post.update!(caption: response.text)
 end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By keeping logic inside services, we avoid bloated models and controllers, and it makes it easy to reuse across background jobs.&lt;/p&gt;

&lt;p&gt;Using AI for Content Refinement&lt;/p&gt;

&lt;p&gt;We started with the ruby-openai gem, but later switched to ruby-llm for its flexibility and broader model support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our AI layer powers features like:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RobinGen → generates captions, hashtags, and text variations.&lt;/li&gt;
&lt;li&gt;RobinPilot → automates post creation from articles or ideas.&lt;/li&gt;
&lt;li&gt;Content refinement → rewrites existing drafts to fit platform tone (LinkedIn, Instagram, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The workflow is simple:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User drafts or uploads content.&lt;/li&gt;
&lt;li&gt;AI refines tone, grammar, or platform style.&lt;/li&gt;
&lt;li&gt;User reviews and approves before scheduling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use background jobs to handle API calls asynchronously, ensuring the dashboard stays responsive while posts are being “AI-polished.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media Handling with MiniMagick and FFmpeg&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For visuals, we rely on MiniMagick for image resizing and Streamio-FFmpeg for short-form video generation. These two tools help us dynamically create social media content that looks like it was edited manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-Tenant Design for Agencies and Brands&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RobinReach was designed for both businesses and marketing agencies. Each company account can manage multiple brands, each brand having its own users, social profiles, posts, and analytics.&lt;/p&gt;

&lt;p&gt;We use a company context service to isolate data automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every query is scoped by company.&lt;/li&gt;
&lt;li&gt;Each background job inherits that context.&lt;/li&gt;
&lt;li&gt;Admins can switch between brands instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach ensures data security and scalability without introducing unnecessary complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned Building RobinReach&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI integration works best when it’s optional and assistive, not intrusive.&lt;/li&gt;
&lt;li&gt;Service objects keep Rails clean and maintainable as complexity grows.&lt;/li&gt;
&lt;li&gt;Multi-tenancy needs clear boundaries early, retrofitting it later is painful.&lt;/li&gt;
&lt;li&gt;Rails still excels when paired with modern, lightweight tools like Hotwire and Tailwind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Closing Thoughts&lt;/p&gt;

&lt;p&gt;Building RobinReach has been a continuous learning process. We’ve learned that Rails is still one of the best frameworks for quickly turning complex ideas, like AI-driven automation, into production-ready features.&lt;/p&gt;

&lt;p&gt;Our next steps include making RobinReach’s AI even smarter, from better content understanding to predicting the best times to post.&lt;/p&gt;

&lt;p&gt;If you’d like to follow our journey or see how AI can simplify your social media workflow, you can check out RobinReach at &lt;a href="https://robinreach.com/en" rel="noopener noreferrer"&gt;https://robinreach.com/en&lt;/a&gt;&lt;br&gt;
.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>How We’re Building a Social Media Empire with Rails and Sidekiq</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Tue, 05 Aug 2025 21:23:22 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/how-were-building-a-social-media-empire-with-rails-and-sidekiq-5p0</link>
      <guid>https://dev.to/shahershamroukh/how-were-building-a-social-media-empire-with-rails-and-sidekiq-5p0</guid>
      <description>&lt;p&gt;It’s been a minute since I last published a blog post, mostly because we’ve been busy building &lt;a href="https://robinreach.com/en" rel="noopener noreferrer"&gt;RobinReach&lt;/a&gt;, a modern social media management platform powered by Ruby on Rails.&lt;/p&gt;

&lt;p&gt;If you’ve ever tried building a tool that posts to multiple social platforms, handles different time zones, supports team approvals, automates publishing, and still provides clean analytics… you know it’s not a walk in the park.&lt;/p&gt;

&lt;p&gt;What could’ve easily turned into a tangled mess of conditionals, platform-specific logic, and fragile scheduling code became a clean, maintainable, and scalable system thanks to Rails' service-oriented design and Sidekiq for background job handling.&lt;/p&gt;

&lt;p&gt;The Stack That Powers RobinReach&lt;br&gt;
RobinReach runs on a streamlined and modern Rails stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby on Rails 7&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Sidekiq + Redis for background jobs&lt;/li&gt;
&lt;li&gt;Hotwire + Stimulus for reactive UI&lt;/li&gt;
&lt;li&gt;TailwindCSS for styling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We manage thousands of scheduled posts across platforms like Instagram, LinkedIn, TikTok, Twitter, and YouTube, all orchestrated through Sidekiq workers that handle everything from publishing to AI content generation.&lt;/p&gt;

&lt;p&gt;🧩 Why a Service-Oriented Architecture?&lt;br&gt;
Each social media platform comes with its own rules: different APIs, content requirements, authentication flows, error handling, rate limits, the list goes on.&lt;/p&gt;

&lt;p&gt;Instead of cluttering models or controllers with conditional logic, we split the logic by platform using dedicated service classes, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module SocialMedia
  module InstagramDirect
    class Post &amp;lt; SocialMedia::ApplicationService
      def initialize(post, profile)
        # setup context
      end

      def call
        # 1. create media container
        # 2. wait for readiness
        # 3. publish
        # 4. return success or failure
      end
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the same structure for:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SocialMedia::Twitter::Service&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
&lt;code&gt;SocialMedia::LinkedIn::Service&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Each class is responsible for only one job: posting to its platform.&lt;/p&gt;

&lt;p&gt;What This Gives Us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clean interface: &lt;code&gt;PlatformService.new(post, profile).call&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Separation of concerns&lt;/li&gt;
&lt;li&gt;Platform-specific logging + error handling&lt;/li&gt;
&lt;li&gt;Easier testing, mocking, and maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⏱ Why Sidekiq?&lt;/p&gt;

&lt;p&gt;Publishing a post isn’t instant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It needs to go out at the scheduled time&lt;/li&gt;
&lt;li&gt;Some platforms require polling&lt;/li&gt;
&lt;li&gt;Others need multi-step media uploads&lt;/li&gt;
&lt;li&gt;You don’t want users waiting for any of that&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Welcome Sidekiq.&lt;/p&gt;

&lt;p&gt;We use Sidekiq to handle everything asynchronously and at scale.&lt;/p&gt;

&lt;p&gt;A Typical Flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User schedules a post for a future time&lt;/li&gt;
&lt;li&gt;We enqueue the job like this:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;PostJob.perform_at(post.publish_time, post.id)&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The job runs, finds the post, and calls the right service object&lt;/li&gt;
&lt;li&gt;Everything happens in the background, with automatic retries if things fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💡The Sweet Spot: Service + Sidekiq&lt;br&gt;
Here’s why the combination is so powerful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔧 Service Objects&lt;/strong&gt;            &lt;strong&gt;⏱ Sidekiq&lt;/strong&gt;&lt;br&gt;
Encapsulate platform logic  Handles execution &amp;amp; timing&lt;br&gt;
Keep jobs thin                  Makes everything async&lt;br&gt;
Testable, predictable           Retryable, resilient&lt;br&gt;
Modular per platform            Scalable with queues&lt;/p&gt;

&lt;p&gt;We can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add new platforms without touching existing logic&lt;/li&gt;
&lt;li&gt;Retry failed posts automatically&lt;/li&gt;
&lt;li&gt;Track errors in logs or monitoring tools&lt;/li&gt;
&lt;li&gt;Move fast without fearing regression&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🧪 Testability Bonus&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each service object is unit-tested on its own.&lt;/li&gt;
&lt;li&gt;Jobs are tested separately with inline Sidekiq workers.&lt;/li&gt;
&lt;li&gt;Because logic is separated, we can:&lt;/li&gt;
&lt;li&gt;Mock external API calls in services&lt;/li&gt;
&lt;li&gt;Simulate platform failures&lt;/li&gt;
&lt;li&gt;Write isolated tests for platform behaviors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This would be a nightmare with fat models or controllers.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;br&gt;
If you’re building a Rails app that interacts with third-party APIs, handles user actions asynchronously, or needs to scale beyond a weekend project…&lt;/p&gt;

&lt;p&gt;💡 Split logic into service objects&lt;br&gt;
🧠 Run tasks with Sidekiq&lt;/p&gt;

&lt;p&gt;That combo gave us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cleaner codebase&lt;/li&gt;
&lt;li&gt;Faster iteration cycles&lt;/li&gt;
&lt;li&gt;Fewer bugs&lt;/li&gt;
&lt;li&gt;Less stress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails gives you the foundation. These two patterns help you build skyscrapers on top of it.&lt;/p&gt;

&lt;p&gt;Oh, and one more thing…&lt;br&gt;
Turns out we’re not the only social media management tool built with Rails,  shoutout to &lt;a href="https://publer.com/" rel="noopener noreferrer"&gt;Publer&lt;/a&gt;, whose product and UX has definitely inspired us along the way.&lt;/p&gt;

&lt;p&gt;Rails is alive and well. And with service objects + Sidekiq, it scales beautifully.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>robinreach</category>
      <category>sidekiq</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Integrate Your SaaS with AppSumo: A Step-by-Step Guide</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Thu, 13 Mar 2025 16:23:53 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/how-to-integrate-your-saas-with-appsumo-a-step-by-step-guide-288e</link>
      <guid>https://dev.to/shahershamroukh/how-to-integrate-your-saas-with-appsumo-a-step-by-step-guide-288e</guid>
      <description>&lt;p&gt;If you're launching your SaaS on AppSumo, integrating with their API is crucial for a smooth user experience. This guide walks you through the AppSumo authentication and license verification process, using a fully working Ruby on Rails example.&lt;/p&gt;

&lt;p&gt;This article was inspired by RobinReach’s AppSumo integration. &lt;br&gt;
Check out &lt;a href="https://robinreach.com/" rel="noopener noreferrer"&gt;RobinReach&lt;/a&gt; for automated social media management! 🎯&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01yvhk5sfwhml7brruvt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01yvhk5sfwhml7brruvt.jpeg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Whether you're a founder or developer, this guide will help you integrate AppSumo seamlessly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Integrate with AppSumo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AppSumo offers lifetime deals (LTDs) to a large number of users. &lt;br&gt;
To streamline onboarding and license verification, they provide an OpenID-based authentication system. By integrating with AppSumo's API, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify purchases and activate licenses automatically&lt;/li&gt;
&lt;li&gt;Handle license upgrades/downgrades&lt;/li&gt;
&lt;li&gt;Respond to AppSumo webhook events&lt;/li&gt;
&lt;li&gt;Improve user onboarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Setting Up the Authentication Flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first step in the integration is to set up an authentication flow that allows users to sign in via AppSumo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.1 Create an OAuth Redirect to AppSumo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;AppsumoAuthController&lt;/code&gt;, we define a method to generate the OAuth authorization URL and redirect users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AppsumoAuthController &amp;lt; ApplicationController
  skip_before_action :authenticate_user!
  skip_before_action :verify_authenticity_token

  CLIENT_ID = Rails.application.credentials.appsumo.client_id
  REDIRECT_URI = 'https://robinreach.com/appsumo/callback'

  def redirect
    auth_url = "https://appsumo.com/openid/authorize?client_id=#{CLIENT_ID}&amp;amp;response_type=code&amp;amp;redirect_uri=#{REDIRECT_URI}"
    redirect_to auth_url, allow_other_host: true
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method constructs the OAuth authorization URL, allowing users to authenticate via AppSumo. Upon successful authentication, users are redirected to the specified callback URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.2 Handle the Callback and Exchange Code for an Access Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When users approve the authentication, AppSumo redirects them back with an authorization code. We exchange this code for an access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def callback
  code = params[:code]
  return redirect_to auth_url if code.blank?

  response = fetch_access_token(code)
  if response['access_token']
    session[:appsumo_access_token] = response['access_token']
    session[:appsumo_refresh_token] = response['refresh_token']
    redirect_to appsumo_license_path
  else
    render json: { error: response['error'] || 'Failed to authenticate' }, status: :unauthorized
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fetch_access_token&lt;/code&gt; method sends a request to AppSumo to retrieve the access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def fetch_access_token(code)
  response = self.class.post('/token/', headers: { 'Content-Type' =&amp;gt; 'application/json' }, body: {
    client_id: CLIENT_ID,
    client_secret: Rails.application.credentials.appsumo.client_secret,
    code: code,
    redirect_uri: REDIRECT_URI,
    grant_type: 'authorization_code'
  }.to_json)

  response.parsed_response
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is essential for securely retrieving an access token, which will be used for further API requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Fetch and Validate the License Key&lt;/strong&gt;&lt;br&gt;
Once authenticated, we need to fetch the license key to check if the user has a valid AppSumo purchase&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def fetch_license
  access_token = session[:appsumo_access_token]
  if access_token
    response = self.class.get("/license_key/", query: { access_token: access_token })
    if response.success?
      license_key = response.parsed_response["license_key"]
      handle_license_verification(license_key)
    else
      refresh_access_token
    end
  else
    redirect_to new_user_session_path
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method retrieves the license key and checks if it belongs to an existing Company. If not, the user is directed to sign up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Handling Webhook Events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AppSumo sends webhooks for key events like purchases, upgrades, and deactivations. We need to process them accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def webhook
  payload = JSON.parse(request.body.read)
  license_key = payload['license_key']
  event = payload['event']
  tier = payload['tier'].to_i

  case event
  when 'activate', 'purchase'
    REDIS.set(license_key, { license_key: license_key, tier: tier }.to_json, ex: 60 * 60 * 24 * 60)
    render json: { success: true, event: event }, status: :ok
  when 'upgrade', 'downgrade'
    update_license(license_key, payload['prev_license_key'], tier)
  when 'deactivate'
    deactivate_license(license_key)
  else
    render json: { success: false, error: 'Unknown event' }, status: :bad_request
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Store in Redis?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We use Redis to temporarily store the license data for quick access. This reduces the number of database queries and speeds up license verification. The data is set to expire after 60 days, ensuring that stale licenses are not kept indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating the Company Based on the Event&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For events like purchases or upgrades, we update the Company record to reflect the new subscription tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def update_company_license(license_key, tier)
  company = Company.find_by(license_key: license_key)
  if company
    company.update(plan_tier: tier)
  else
    Company.create(license_key: license_key, plan_tier: tier)
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that companies have the correct access level based on their AppSumo purchase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Securing Webhooks with Signature Verification&lt;/strong&gt;&lt;br&gt;
Since webhooks can be exploited, we verify requests using HMAC-SHA256 signatures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def verify_signature
  timestamp = request.headers['X-Appsumo-Timestamp']
  signature = request.headers['X-Appsumo-Signature']
  body = request.raw_post

  return render json: { success: false, error: 'Missing headers' }, status: :unauthorized unless timestamp &amp;amp;&amp;amp; signature &amp;amp;&amp;amp; body

  message = "#{timestamp}#{body}"
  calculated_signature = OpenSSL::HMAC.hexdigest('SHA256', API_KEY, message)

  unless ActiveSupport::SecurityUtils.secure_compare(signature, calculated_signature)
    render json: { success: false, error: 'Invalid signature' }, status: :unauthorized
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents unauthorized requests and protects your system from fraudulent webhooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Integrating AppSumo’s API into your SaaS can automate user onboarding, license management etc.&lt;/p&gt;

&lt;p&gt;Key Takeaways:&lt;/p&gt;

&lt;p&gt;✅ Implement OAuth authentication for AppSumo users&lt;br&gt;
✅ Retrieve and validate AppSumo license keys&lt;br&gt;
✅ Handle webhook events for upgrades, downgrades, and deactivations&lt;br&gt;
✅ Secure webhooks using signature verification&lt;/p&gt;

&lt;p&gt;This integration ensures that your SaaS can handle AppSumo deals efficiently while providing a seamless experience to users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 Next Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test OAuth flow and webhook processing.&lt;/li&gt;
&lt;li&gt;Monitor webhook logs for debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more details, refer to the AppSumo Licensing &lt;a href="https://docs.licensing.appsumo.com/#overview" rel="noopener noreferrer"&gt;Guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This guide is inspired by RobinReach’s integration with AppSumo. Discover how RobinReach simplifies social media management with automation and smart scheduling! 🚀&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Just Launched RobinReach: Multi-Channel Social Media Management 🚀</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Wed, 18 Dec 2024 09:40:19 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/just-launched-robinreach-multi-channel-social-media-management-pc1</link>
      <guid>https://dev.to/shahershamroukh/just-launched-robinreach-multi-channel-social-media-management-pc1</guid>
      <description>&lt;p&gt;Hey Everyone! 👋&lt;/p&gt;

&lt;p&gt;I’m excited to share that RobinReach, my social media management platform, is now live on &lt;a href="https://www.producthunt.com/posts/robinreach" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt;! 🎉&lt;/p&gt;

&lt;p&gt;RobinReach is built with Ruby on Rails and designed for small businesses, teams, and agencies to simplify their social media workflows.&lt;/p&gt;

&lt;p&gt;Here’s what makes it stand out:&lt;/p&gt;

&lt;p&gt;🌍 Multi-channel support: Manage all your social media accounts (Instagram, LinkedIn, Facebook, Twitter, and more) in one place.&lt;/p&gt;

&lt;p&gt;✍️ Content creation: AI-assisted tools for generating and optimizing posts.&lt;/p&gt;

&lt;p&gt;🔁 Content repurposing: Easily reuse and adapt content for different platforms to maximize reach.&lt;/p&gt;

&lt;p&gt;📊 Analytics: Actionable insights to measure performance and improve engagement.&lt;/p&gt;

&lt;p&gt;Whether you're scheduling posts, repurposing old content, or reviewing metrics, RobinReach aims to save time and boost results.&lt;/p&gt;

&lt;p&gt;💡 Built primarily with Ruby on Rails, integrated with ChatGPT, FFmpeg, and Unsplash. It also supports features like TikTok/YouTube Shorts video generation and Canva image editing.&lt;/p&gt;

&lt;p&gt;👉 Check it out here: &lt;a href="https://www.producthunt.com/posts/robinreach" rel="noopener noreferrer"&gt;RobinReach on Product Hunt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Would love to hear your thoughts on:&lt;br&gt;
1️⃣ The concept—does it fill a gap you see in social media management?&lt;br&gt;
2️⃣ The tech side—any feedback or tips on scaling multi-channel management features?&lt;br&gt;
3️⃣ Advice on making the most of a PH launch if you've done one before!&lt;/p&gt;

&lt;p&gt;Let’s chat—I'm happy to answer questions about the tech stack, challenges, or lessons learned. 🚀&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>automation</category>
      <category>ruby</category>
      <category>ai</category>
    </item>
    <item>
      <title>ActiveRecord Calculations</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Tue, 29 Mar 2022 16:40:24 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/activerecord-calculations-3fd0</link>
      <guid>https://dev.to/shahershamroukh/activerecord-calculations-3fd0</guid>
      <description>&lt;p&gt;Let's talk about some useful methods that are very handy when we need to do some calculations to our database records.&lt;/p&gt;

&lt;h4&gt;
  
  
  So what is activerecord calculations?
&lt;/h4&gt;

&lt;h5&gt;
  
  
  ActiveRecord Calculations provide methods for calculating values of columns in the ActiveRecord models.
&lt;/h5&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;count&lt;/code&gt; Method
&lt;/h3&gt;

&lt;p&gt;Let's start with a method that is widely knowing for simple count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.count 
#returns the number of users.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that count is not always the most performant with that being said &lt;br&gt;
this &lt;a href="https://longliveruby.com/articles/active-record-counting-records" rel="noopener noreferrer"&gt;article&lt;/a&gt; demonstrates the difference between &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;length&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;count&lt;/code&gt; has much more to offer 🔥 so let's see the different usage of count below 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.count(:address)
# returns the count of all users whose address exist in our db.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.distinct.count(:city)
# returns the count of different cities we have in our db.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.group(:city).count
# returns a hash with each city and it's count.
#{ 'Cairo' =&amp;gt; 5, 'Qina' =&amp;gt; 3 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also do the above 👆 with multiple columns&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Post.group(:status, :category).count
# {["draft", "business"]=&amp;gt;10, ["published", "art"]=&amp;gt;5}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;maximum&lt;/code&gt; Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.maximum(:age)
# 98
# returns the maximum value on the given column.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;minimum&lt;/code&gt; Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.minimum(:age)
# 18
# returns the minimum value on the given column.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;average&lt;/code&gt; Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.average(:age)
# 27.5
# returns the average value on the given column.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;sum&lt;/code&gt; Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.sum(:age)
# 7845
# returns the sum of the values on the given column.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;ids&lt;/code&gt; Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.ids
# returns the users ids
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Please check out &lt;a href="https://api.rubyonrails.org/v7.0.2.3/classes/ActiveRecord/Calculations.html" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; for reference.&lt;/p&gt;

&lt;p&gt;I hope you found the article useful and enjoyed reading as much as i enjoyed writing it 😃.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Ruby Sets</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Tue, 15 Feb 2022 20:06:25 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/ruby-sets-1b55</link>
      <guid>https://dev.to/shahershamroukh/ruby-sets-1b55</guid>
      <description>&lt;h2&gt;
  
  
  What is a Ruby Set
&lt;/h2&gt;

&lt;p&gt;Ruby Set is a class implementing the mathematical concept of &lt;a href="https://en.wikipedia.org/wiki/Set_(mathematics)" rel="noopener noreferrer"&gt;Set&lt;/a&gt;.&lt;br&gt;
Meaning it stores items like an array without duplicate items  so all the items in a set are guaranteed to be unique and it is a lot more faster and has some special capabilities.&lt;/p&gt;

&lt;p&gt;Before getting into how to use set class let's first determine when do we use it.&lt;/p&gt;
&lt;h3&gt;
  
  
  When to use sets
&lt;/h3&gt;

&lt;p&gt;Here are the few cases where sets work better:-&lt;br&gt;
1- Elements order does not matter.&lt;br&gt;
2- No duplicate values allowed.&lt;br&gt;
3- The sequences needs to be compared for equality regardless of the order.&lt;/p&gt;
&lt;h3&gt;
  
  
  How to use sets
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt; require 'set'
&amp;gt;&amp;gt; fruits = Set.new
&amp;gt;&amp;gt; fruits &amp;lt;&amp;lt; "Apple"
&amp;gt;&amp;gt; fruits.add("Orange")
&amp;gt;&amp;gt; fruits
=&amp;gt; #&amp;lt;Set: {"Apple", "Orange"}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can also do this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt; numbers = Set[1, 2, 3, 4]
=&amp;gt; #&amp;lt;Set: {1, 2, 3, 4}&amp;gt;
Notice no duplicates allowed:
&amp;gt;&amp;gt; numbers &amp;lt;&amp;lt; 3
=&amp;gt; #&amp;lt;Set: {1, 2, 3, 4}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Here are some useful methods to use with sets
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#include?
&amp;gt;&amp;gt; numbers.include?(4)
=&amp;gt; true

#delete
&amp;gt;&amp;gt; numbers.delete(2)
=&amp;gt; #&amp;lt;Set: {1, 3, 4}&amp;gt;

#disjoint?
&amp;gt;&amp;gt; numbers
=&amp;gt; #&amp;lt;Set: {1, 3, 4}&amp;gt;
&amp;gt;&amp;gt; numbers.disjoint? Set[5, 6]
=&amp;gt; true      #No elements in common the opposite is intersect?

#union or |
&amp;gt;&amp;gt; numbers
=&amp;gt; #&amp;lt;Set: {1, 3, 4}&amp;gt;
&amp;gt;&amp;gt; numbers.union Set[2, 4, 5] 
=&amp;gt; #&amp;lt;Set: {1, 3, 4, 2, 5}&amp;gt;
&amp;gt;&amp;gt; numbers | Set[2, 4, 5] 
=&amp;gt; #&amp;lt;Set: {1, 3, 4, 2, 5}&amp;gt;

#delete_if &amp;amp; It's opposite keep_if
&amp;gt;&amp;gt; fruits
=&amp;gt; #&amp;lt;Set: {"Apple", "Orange", "melon"}&amp;gt;
&amp;gt;&amp;gt; fruits.delete_if { |f| f == "melon"}
=&amp;gt; #&amp;lt;Set: {"Apple", "Orange"}&amp;gt;

#clear
&amp;gt;&amp;gt; numbers.clear
=&amp;gt; #&amp;lt;Set: {}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As well as the method above Set is easy to use with Enumerable objects.&lt;/p&gt;

&lt;p&gt;For more details please check out &lt;a href="https://ruby-doc.org/stdlib-2.7.1/libdoc/set/rdoc/Set.html" rel="noopener noreferrer"&gt;ruby-doc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we know how to use ruby sets for better performance and easier coding.&lt;br&gt;
So if you are using Ruby in your current project and you think that any of the cases above apply to you, you should consider using ruby sets.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed reading the article as i enjoyed writing it if so Please share it so more people can find it 🙂&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>sets</category>
      <category>rubysets</category>
    </item>
    <item>
      <title>Working With Folders &amp; Files In Ruby</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Fri, 08 Oct 2021 20:05:27 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/working-with-folders-files-in-ruby-2l97</link>
      <guid>https://dev.to/shahershamroukh/working-with-folders-files-in-ruby-2l97</guid>
      <description>&lt;h2&gt;
  
  
  How to work with folders and files in ruby?
&lt;/h2&gt;

&lt;p&gt;Ruby has two built in classes for us to work with files and folders those classes are &lt;code&gt;Dir&lt;/code&gt; for directories and &lt;code&gt;File&lt;/code&gt; for the files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ruby &lt;code&gt;Dir&lt;/code&gt; class.
&lt;/h3&gt;

&lt;p&gt;To create a Dir instance, you pass a directory path to new like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;d = Dir.new("/home/shaher/work/test")
d.entries
=&amp;gt; ["..", ".", "file.txt", "main.rb", ".csv"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also use the class method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dir.entries("/home/shaher/work/test")
=&amp;gt; ["..", ".", "file.txt", "main.rb", ".csv"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can get hold of the entries using the entries method,&lt;br&gt;
or using the glob technique. &lt;br&gt;
And the main difference is that globbing the directory doesn’t return the hidden entries (entries whose names start with a period.)&lt;br&gt;
It also permits the  wildcard matching and the recursive matching in the subdirectories.&lt;/p&gt;

&lt;p&gt;Now with &lt;code&gt;entries&lt;/code&gt; method we have the files in a nicely structured array so let's dive into the file class to do the work on our files.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ruby &lt;code&gt;File&lt;/code&gt; class
&lt;/h3&gt;

&lt;p&gt;To create the file and write value we can do the following&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f = File.new("comment.txt", "w")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you will see the file created and we have the object to use.&lt;br&gt;
Let's add some text to the file we just created and close the file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f.puts "this text meant to be added to the comment file"
f.close
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;well we added the text but we wanna add more and update the file then we do the following&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f = File.new("comment.txt", "a")
f.puts "we added this extra text to update the file"
f.close
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have our file holds the text we added and updated.&lt;/p&gt;

&lt;p&gt;But using &lt;code&gt;File.new&lt;/code&gt; to create a File object make us close the file ourselves.&lt;br&gt;
Ruby as always being elegant and meant for our happiness it  provides an alternate way to open files that puts&lt;br&gt;
the task of closing the file in it's hands.&lt;br&gt;
&lt;code&gt;File.open&lt;/code&gt; with a code block.&lt;/p&gt;

&lt;p&gt;When we call File.open with a block, the block receives the File object as its single argument.&lt;br&gt;
So we use that File object inside the block and When the block ends, the File object is automatically closed.&lt;/p&gt;

&lt;p&gt;In the following example our file is opened and read in line by line for processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File.open("comment.txt") do |f|
  f.each do |line|
    puts line.upcase
  end
end
=&amp;gt; THIS TEXT MEANT TO BE ADDED TO THE COMMENT FILE
=&amp;gt; WE ADDED THIS EXTRA TEXT TO UPDATE THE FILE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ruby stops iterating when it hits the end of the file and closes the file.&lt;/p&gt;

&lt;p&gt;Another method ruby provides is &lt;code&gt;readlines&lt;/code&gt; which reads the whole file into an array like the example below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File.readlines("comment.txt") do |f| 
  f.each do |line| 
    puts line
  end
end
=&amp;gt; ["this text meant to be added to the comment file\n", "we added this extra text to update the file\n"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But why all that and  not just iterate on the file and avoid wasting the space required to hold the file’s contents in memory?&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Along with File and Dir classes there is also FileUtils module which provides some practical and convenient methods that make it easy to manipulate files from Ruby in a concise manner and in ways that correspond to familiar system commands.&lt;br&gt;
&lt;a href="https://ruby-doc.org/core-2.5.0/File.html" rel="noopener noreferrer"&gt;File class reference&lt;/a&gt;&lt;br&gt;
&lt;a href="https://ruby-doc.org/core-2.5.0/Dir.html" rel="noopener noreferrer"&gt;Dir class reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope you enjoyed reading this article and found it useful! 🙂&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>Ruby Regular Expressions</title>
      <dc:creator>Shaher Shamroukh</dc:creator>
      <pubDate>Mon, 04 Oct 2021 18:18:47 +0000</pubDate>
      <link>https://dev.to/shahershamroukh/ruby-regular-expressions-5d0p</link>
      <guid>https://dev.to/shahershamroukh/ruby-regular-expressions-5d0p</guid>
      <description>&lt;h2&gt;
  
  
  What is Ruby regular expressions (ruby regex)?
&lt;/h2&gt;

&lt;p&gt;Regular expressions appear in many programming languages, with minor differences.&lt;/p&gt;

&lt;p&gt;Their purpose is to specify character patterns that subsequently are determined to match or not match. &lt;br&gt;
Pattern matching, in turn, serves as the basis for operations like parsing log files, testing keyboard input for validity, etc.&lt;/p&gt;

&lt;p&gt;Regular expressions have a reputation of being incredibly powerful.&lt;/p&gt;

&lt;p&gt;Regular expressions are instances of the Regexp class.&lt;br&gt;
and it's literal constructor is a pair of forward slashes:&lt;br&gt;
//&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//.class
=&amp;gt; Regexp 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Between the slashes we add the specifics of the regexp.&lt;br&gt;
Now Any pattern-matching operation has two ends: a regexp and a string.&lt;br&gt;
and the simplest way to do that with the &lt;code&gt;match&lt;/code&gt; method.&lt;br&gt;
we can do this either way since regular-expression&lt;br&gt;
objects and string objects both respond to &lt;code&gt;match&lt;/code&gt;.&lt;br&gt;
Ruby also provides pattern-matching operator,&lt;br&gt;
&lt;code&gt;=~&lt;/code&gt;(equal sign and tilde).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p "Match!" if /ruby/.match("ruby is wonderful")
=&amp;gt; "Match!"

p "Match!" if "ruby is wonderful"=~(/ruby/)
=&amp;gt; "Match!" 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building regular expression pattern
&lt;/h2&gt;

&lt;p&gt;When we write a regexp, we put the definition of the pattern between the forward slashes //.&lt;br&gt;
and what we are putting simply a set of predictions that we want to look for in a string.&lt;/p&gt;
&lt;h3&gt;
  
  
  regex components
&lt;/h3&gt;

&lt;p&gt;Literal characters,&lt;code&gt;/r/&lt;/code&gt; “match this character”.&lt;br&gt;
The dot wildcard character (.), “match any character”.&lt;br&gt;
Character classes, [aeiou] matches any vowel.&lt;/p&gt;

&lt;p&gt;So far, we’ve looked at basic match operations which are true/false tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;regex.match(string)
string.match(regex)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Till now we got the basics of regexp, and for everyone &lt;br&gt;
I definitely recommend trying out &lt;a href="https://rubular.com/" rel="noopener noreferrer"&gt;Rubular&lt;/a&gt; to build your own regexp taking advantage of it's quick reference, which is awesome for practice, and it is very interactive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5mm2c845qv96ju7s72h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa5mm2c845qv96ju7s72h.png" alt="Alt Text" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope you enjoyed reading the topic as much as i enjoyed writing it. 🙂&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
