<?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>Forem: Aoun Abu Hassan</title>
    <description>The latest articles on Forem by Aoun Abu Hassan (@aounah).</description>
    <link>https://forem.com/aounah</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%2F2514942%2Fa5aecde3-afe7-400f-9de7-de25bd6f9e54.jpg</url>
      <title>Forem: Aoun Abu Hassan</title>
      <link>https://forem.com/aounah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/aounah"/>
    <language>en</language>
    <item>
      <title>I Built a Discord SaaS in 3 Months at 19. Here's What It Actually Took.</title>
      <dc:creator>Aoun Abu Hassan</dc:creator>
      <pubDate>Sun, 05 Apr 2026 15:32:18 +0000</pubDate>
      <link>https://forem.com/aounah/i-built-a-discord-saas-in-3-months-at-19-heres-what-it-actually-took-4gm</link>
      <guid>https://forem.com/aounah/i-built-a-discord-saas-in-3-months-at-19-heres-what-it-actually-took-4gm</guid>
      <description>&lt;p&gt;I'm 19. I build from my bedroom. I have no co-founder, no investors, and no team.&lt;/p&gt;

&lt;p&gt;Three months ago I started building Nexcord — a community management platform for Discord servers. Today it's live, it charges real money via Paddle, and real servers are using it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Discord?
&lt;/h2&gt;

&lt;p&gt;I've been building Discord bots since I was 16. JavaScript/Node.js was where I started. I'd watched server admins run the same broken workflows for years: manually copying ticket conversations into Google Docs, DMing users just to verify they're human, no way to search past discussions.&lt;/p&gt;

&lt;p&gt;Nobody had built a clean, unified platform that treated community management as a serious product problem. That felt like the gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Nexcord Does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated transcripts&lt;/strong&gt; — every ticket or thread saved automatically, searchable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI summarization&lt;/strong&gt; — Mistral 7B running locally via Ollama. No data sent to OpenAI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web-based verification&lt;/strong&gt; — custom flows in the browser, not bot commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;$4.99/month Pro with a 14-day trial.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monorepo: Yarn Workspaces
Bot:       Discord.js v14
API:       Fastify 4 → Railway (all business logic here)
Dashboard: Next.js 16 → Vercel (UI only)
DB:        Supabase
Cache:     Upstash Redis (pay-as-you-go)
AI:        Ollama — Mistral 7B + LLaVA 7B (local RTX 3080 Ti)
Billing:   Paddle (Merchant of Record)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard rule: all business logic lives in Fastify. Next.js is UI only. If the dashboard breaks, billing and plan enforcement still work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mistake I Already Fixed
&lt;/h2&gt;

&lt;p&gt;I built the summarization queue with BullMQ backed by Upstash Redis. Looked solid on paper.&lt;/p&gt;

&lt;p&gt;The problem: Upstash's serverless Redis doesn't support the Lua scripts BullMQ uses internally for atomic operations. Jobs were failing silently. I was burning Redis commands on broken queue overhead.&lt;/p&gt;

&lt;p&gt;The fix was simpler than I expected — I replaced the entire thing with a &lt;code&gt;summarization_jobs&lt;/code&gt; table in Supabase and a polling worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Poll for pending jobs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarization_jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;attempts&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;

&lt;span class="c1"&gt;// Claim it&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarization_jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// optimistic lock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deduplication, retry, stall recovery — all handled cleanly. Upstash now only does what it's good at: fast reads, rate limit counters, session caching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: if you're not at scale yet, start with Postgres for job queues. You probably don't need Redis for this.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  One More Thing — Ollama Cold Start
&lt;/h2&gt;

&lt;p&gt;LLaVA 7B was taking 20+ seconds on first inference. Fix was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OLLAMA_KEEP_ALIVE=30m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And bumping the LLaVA timeout to 60 seconds. Model stays warm between requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current State (honest numbers)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Servers on real bot: ~5&lt;/li&gt;
&lt;li&gt;Bot verification with Discord: pending (required to scale past 100 servers)&lt;/li&gt;
&lt;li&gt;Lighthouse: 91 / 92 / 96 / 100&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Building in public means posting before things are perfect. This is that post.&lt;/p&gt;

&lt;p&gt;If you want to follow along or try Nexcord: &lt;a href="https://nexcord.app" rel="noopener noreferrer"&gt;nexcord.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
