<?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: Brian Marsaw</title>
    <description>The latest articles on DEV Community by Brian Marsaw (@brino666).</description>
    <link>https://dev.to/brino666</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032631%2Faeaffb07-87dc-4265-9e68-b26953e37533.png</url>
      <title>DEV Community: Brian Marsaw</title>
      <link>https://dev.to/brino666</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brino666"/>
    <language>en</language>
    <item>
      <title>Ask HN: What process should I use to find SMB prospects for a B2B SaaS product?</title>
      <dc:creator>Brian Marsaw</dc:creator>
      <pubDate>Thu, 16 Jul 2026 23:31:26 +0000</pubDate>
      <link>https://dev.to/brino666/ask-hn-what-process-should-i-use-to-find-smb-prospects-for-a-b2b-saas-product-1fg6</link>
      <guid>https://dev.to/brino666/ask-hn-what-process-should-i-use-to-find-smb-prospects-for-a-b2b-saas-product-1fg6</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Finding SMB Prospects for Your B2B SaaS Product: A Practical Process That Actually Works"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"finding-smb-prospects-b2b-saas-product-process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Learn battle-tested processes to find and qualify SMB prospects for your B2B SaaS product. Includes automation scripts, targeting strategies, and outreach tactics."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"B2B SaaS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lead generation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SMB sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"automation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prospecting"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Finding SMB Prospects for Your B2B SaaS Product: A Practical Process That Actually Works&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Finding the right SMB (Small and Medium Business) prospects for your B2B SaaS product is fundamentally different from enterprise sales. SMBs move faster, have smaller budgets, and need solutions that work immediately—but they're also harder to reach at scale. After years of building developer tools and productivity SaaS products, I've learned that success comes from combining targeted research, ruthless qualification, and smart automation.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Let's skip the theory and dive into a process that actually works.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Start With Your Ideal Customer Profile (ICP), Not Your Product&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Most founders start by listing their product features and then searching for companies that might need them. This is backwards.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Instead, analyze your best existing customers (or, if you're pre-launch, your target segment). Document:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;- **Company size**: Number of employees (10-50? 50-200?)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Industry verticals**: Which specific industries get the most value?&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Technology stack**: What tools are they already using?&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Pain points**: What problem were they solving when they found you?&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Budget indicators**: Revenue range, funding status, growth signals&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;For developer tools, this might look like: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Series A startups with 20-100 employees, using React and TypeScript, experiencing scaling issues with their CI/CD pipeline.&lt;/span&gt;&lt;span class="se"&gt;\"\n\n&lt;/span&gt;&lt;span class="s2"&gt;Be specific. &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SMBs that need productivity tools&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; is useless. &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Marketing agencies with 15-50 employees struggling with client reporting&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; gives you something to work with.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Build Your Prospecting Data Pipeline&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Once you know who you're looking for, you need to find them systematically. Here's the tech stack I recommend:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Primary Data Sources&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;1. **LinkedIn Sales Navigator** - Still the best for B2B prospecting. Filter by company size, industry, technology, and growth signals.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;2. **Crunchbase** - Essential for tech companies. Filter by funding stage, employee count, and technology tags.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;3. **BuiltWith or Wappalyzer** - Identify companies using specific technology stacks.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;4. **Industry-specific directories** - Many verticals have specialized directories that aggregate companies.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Automate the Grunt Work&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Don't manually copy-paste data. Build a simple scraping and enrichment pipeline:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;```

python&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;import requests&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;import pandas as pd&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;from typing import List, Dict&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;class ProspectEnricher:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    def __init__(self, clearbit_api_key: str):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        self.clearbit_key = clearbit_api_key&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        self.base_url = &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;https://company.clearbit.com/v2/companies/find&lt;/span&gt;&lt;span class="se"&gt;\"\n&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;    def enrich_domain(self, domain: str) -&amp;gt; Dict:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="se"&gt;\"\"\"&lt;/span&gt;&lt;span class="s2"&gt;Enrich company data using Clearbit API&lt;/span&gt;&lt;span class="se"&gt;\"\"\"\n&lt;/span&gt;&lt;span class="s2"&gt;        params = {'domain': domain}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        headers = {'Authorization': f'Bearer {self.clearbit_key}'}&lt;/span&gt;&lt;span class="se"&gt;\n&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;        try:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            response = requests.get(self.base_url, params=params, headers=headers)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            if response.status_code == 200:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                data = response.json()&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                return {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                    'name': data.get('name'),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                    'domain': domain,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                    'employees': data.get('metrics', {}).get('employees'),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                    'industry': data.get('category', {}).get('industry'),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                    'tech_stack': data.get('tech', []),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                    'linkedin_url': data.get('linkedin', {}).get('handle')&lt;/span&gt;&lt;span class="se"&gt;\n&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;        except Exception as e:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            print(f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Error enriching {domain}: {e}&lt;/span&gt;&lt;span class="se"&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="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        return None&lt;/span&gt;&lt;span class="se"&gt;\n&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;    def batch_enrich(self, domains: List[str]) -&amp;gt; pd.DataFrame:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="se"&gt;\"\"\"&lt;/span&gt;&lt;span class="s2"&gt;Enrich a list of domains and return DataFrame&lt;/span&gt;&lt;span class="se"&gt;\"\"\"\n&lt;/span&gt;&lt;span class="s2"&gt;        enriched_data = []&lt;/span&gt;&lt;span class="se"&gt;\n&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;        for domain in domains:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            data = self.enrich_domain(domain)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            if data:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;                enriched_data.append(data)&lt;/span&gt;&lt;span class="se"&gt;\n&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;        return pd.DataFrame(enriched_data)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;# Usage&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;enricher = ProspectEnricher(api_key='your_clearbit_key')&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;domains = ['example.com', 'anothercompany.com']&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;prospects_df = enricher.batch_enrich(domains)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;# Filter by your ICP criteria&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;qualified = prospects_df[&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    (prospects_df['employees'] &amp;gt;= 20) &amp;amp; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    (prospects_df['employees'] &amp;lt;= 100) &amp;amp;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    (prospects_df['tech_stack'].str.contains('React', na=False))&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;qualified.to_csv('qualified_prospects.csv', index=False)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;

```&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;This script uses Clearbit (you can substitute with Apollo.io, Hunter.io, or similar) to enrich company data and automatically filter by your ICP criteria. Run this weekly to keep your pipeline fresh.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Qualify Before You Reach Out&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Not every company that matches your ICP is ready to buy. Add a qualification layer:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Trigger Events&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Look for signals that indicate buying intent:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;- **Recent funding** - Fresh capital means budget for new tools&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Job postings** - Hiring specific roles suggests they're scaling that function&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Product launches** - New products often require new infrastructure&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Technology changes** - Switching stack components creates opportunity&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Leadership changes** - New executives bring new priorities&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;You can automate trigger detection:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;```

typescript&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;import axios from 'axios';&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;interface TriggerEvent {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  company: string;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  eventType: 'funding' | 'hiring' | 'product_launch' | 'tech_change';&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  date: Date;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  relevanceScore: number;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;class TriggerEventDetector {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  private crunchbaseKey: string;&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;  constructor(crunchbaseKey: string) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    this.crunchbaseKey = crunchbaseKey;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  }&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;  async detectFundingEvents(companies: string[]): Promise&amp;lt;TriggerEvent[]&amp;gt; {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    const events: TriggerEvent[] = [];&lt;/span&gt;&lt;span class="se"&gt;\n&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;    for (const company of companies) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;      try {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        const response = await axios.get(&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;          `https://api.crunchbase.com/v4/entities/organizations/${company}`,&lt;/span&gt;&lt;span class="se"&gt;\n&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;            headers: { 'X-cb-user-key': this.crunchbaseKey },&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            params: { card_ids: 'funding_rounds' }&lt;/span&gt;&lt;span class="se"&gt;\n&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="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;        const latestRound = response.data.cards.funding_rounds[0];&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        const daysSinceFunding = this.daysSince(new Date(latestRound.announced_on));&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;        if (daysSinceFunding &amp;lt; 90) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;          events.push({&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            company,&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            eventType: 'funding',&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            date: new Date(latestRound.announced_on),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;            relevanceScore: 10 - (daysSinceFunding / 9) // Higher score for recent events&lt;/span&gt;&lt;span class="se"&gt;\n&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="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;      } catch (error) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        console.error(`Error checking funding for ${company}:`, error);&lt;/span&gt;&lt;span class="se"&gt;\n&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="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;    return events.sort((a, b) =&amp;gt; b.relevanceScore - a.relevanceScore);&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  }&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;  private daysSince(date: Date): number {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    return Math.floor((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24));&lt;/span&gt;&lt;span class="se"&gt;\n&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="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;

```&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## The Outreach Process That Doesn't Suck&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Here's the controversial part: cold email still works for SMBs, but only if you do it right.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### The Formula&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;1. **Personalized subject lines**: Reference something specific about their company&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;2. **Lead with their problem**: Not your solution&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;3. **One clear value proposition**: How you solve that specific problem&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;4. **Soft CTA**: Ask a question, don't push a demo&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;5. **Follow up strategically**: 3-4 touches over 2 weeks, then move on&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;**Example for a CI/CD tool:**&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;*Subject: Your React deploys at [Company]*&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;*Hi [Name],*&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;*I noticed [Company] is hiring frontend engineers and saw you're using React + TypeScript (nice choice). As your team grows, CI/CD pipeline time becomes a real bottleneck.*&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;*We built [Tool] specifically for React/TS teams scaling from 20-100 engineers. Most teams see 40% faster builds within a week.*&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;*Curious if slow deploys are on your radar right now?*&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;*[Your Name]*&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### Automation vs. Personalization&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Use tools like Lemlist, Reply.io, or build your own with:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;- **Automated sequences** for follow-ups&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Merge tags** for basic personalization&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Manual research** for the first line of each email&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Automate the process, not the message.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Track, Measure, Iterate&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Your prospecting process should be data-driven. Track:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;- **Conversion rates** at each stage (identified → qualified → contacted → responded → demo → customer)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Time to response** (are you reaching out too early/late?)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **Channel effectiveness** (LinkedIn vs. email vs. other)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- **ICP accuracy** (do converted customers match your profile?)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Use a simple spreadsheet or build a dashboard:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;```

python&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;# Simple pipeline analytics&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;import pandas as pd&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;import matplotlib.pyplot as plt&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;df = pd.read_csv('prospects.csv')&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;# Conversion funnel&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;funnel = {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    'Identified': len(df),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    'Qualified': len(df[df['qualified'] == True]),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    'Contacted': len(df[df['contacted'] == True]),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    'Responded': len(df[df['responded'] == True]),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    'Demo': len(df[df['demo_scheduled'] == True]),&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    'Customer': len(df[df['converted'] == True])&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;# Calculate conversion rates&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;for i, (stage, count) in enumerate(list(funnel.items())[1:], 1):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    prev_stage = list(funnel.values())[i-1]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    conversion = (count / prev_stage * 100) if prev_stage &amp;gt; 0 else 0&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    print(f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{stage}: {conversion:.1f}% conversion&lt;/span&gt;&lt;span class="se"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
\n\nReview these metrics weekly and adjust your ICP, messaging, or channels accordingly.\n\n## The Bottom Line\n\nFinding SMB prospects isn't about blasting thousands of companies with generic messages. It's about:\n\n1. &lt;strong&gt;Defining a tight ICP&lt;/strong&gt; based on real customer data\n2. &lt;strong&gt;Building automated systems&lt;/strong&gt; to identify and qualify prospects\n3. &lt;strong&gt;Detecting trigger events&lt;/strong&gt; that signal buying intent\n4. &lt;strong&gt;Personalizing outreach&lt;/strong&gt; that speaks to specific problems\n5. &lt;strong&gt;Iterating based on data&lt;/strong&gt;, not hunches\n\nThe companies that succeed at SMB prospecting treat it like product development: ship fast, measure everything, and improve continuously. Your first ICP will be wrong. Your first email template will convert poorly. That's fine. The goal is to build a system that gets better every week.\n\nStart&lt;/p&gt;

</description>
      <category>python</category>
      <category>typescript</category>
      <category>react</category>
      <category>saas</category>
    </item>
    <item>
      <title>Simple Email SaaS? Here's What Actually Works in 2024</title>
      <dc:creator>Brian Marsaw</dc:creator>
      <pubDate>Thu, 16 Jul 2026 23:29:27 +0000</pubDate>
      <link>https://dev.to/brino666/simple-email-saas-heres-what-actually-works-in-2024-3j3b</link>
      <guid>https://dev.to/brino666/simple-email-saas-heres-what-actually-works-in-2024-3j3b</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
Simple Email SaaS? Here's What Actually Works in 2024&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "Ask HN: Simple email SaaS?" question has been asked repeatedly on Hacker News because email remains one of the most critical—and frustrating—parts of building a SaaS product. You need transactional emails, marketing campaigns, and reliable delivery, but most solutions are either overpriced, overcomplicated, or both.&lt;/p&gt;

&lt;p&gt;Let's cut through the noise and examine what actually works for developers building SaaS products in 2024.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem With Email SaaS Solutions
&lt;/h2&gt;

&lt;p&gt;Most email services fall into two camps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Marketing-focused platforms&lt;/strong&gt; (Mailchimp, ConvertKit) that are overkill if you just need to send password resets and receipts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactional email APIs&lt;/strong&gt; (SendGrid, Postmark) that become expensive fast and lack basic marketing features&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What developers actually need is something in between: a service that handles transactional emails reliably, allows occasional broadcasts, doesn't break the bank, and provides a simple API without forcing you through a maze of enterprise features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Simple Email SaaS Options Right Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Pure Transactional Email
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Resend&lt;/strong&gt; has emerged as the developer-first choice. Founded by ex-Vercel engineers, it offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean API designed for developers&lt;/li&gt;
&lt;li&gt;React Email template support&lt;/li&gt;
&lt;li&gt;Generous free tier (3,000 emails/month)&lt;/li&gt;
&lt;li&gt;$20/month for 50,000 emails&lt;/li&gt;
&lt;li&gt;Excellent deliverability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postmark&lt;/strong&gt; remains the gold standard for transactional reliability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laser-focused on transactional email only&lt;/li&gt;
&lt;li&gt;Industry-leading deliverability rates&lt;/li&gt;
&lt;li&gt;100 emails/month free, then $15/month for 10,000&lt;/li&gt;
&lt;li&gt;Detailed analytics and bounce handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how simple Resend is to integrate:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
import { Resend } from 'resend';&lt;/p&gt;

&lt;p&gt;const resend = new Resend(process.env.RESEND_API_KEY);&lt;/p&gt;

&lt;p&gt;export async function sendWelcomeEmail(userEmail: string, userName: string) {&lt;br&gt;
  try {&lt;br&gt;
    const data = await resend.emails.send({&lt;br&gt;
      from: '&lt;a href="mailto:onboarding@yoursaas.com"&gt;onboarding@yoursaas.com&lt;/a&gt;',&lt;br&gt;
      to: userEmail,&lt;br&gt;
      subject: 'Welcome to Our Platform',&lt;br&gt;
      html: &lt;code&gt;&amp;lt;h1&amp;gt;Hi ${userName}!&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Thanks for signing up...&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;br&gt;
    });&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return { success: true, id: data.id };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;} catch (error) {&lt;br&gt;
    console.error('Email failed:', error);&lt;br&gt;
    return { success: false, error };&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;h3&gt;
  
  
  For Transactional + Light Marketing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Loops&lt;/strong&gt; is the new kid on the block, purpose-built for SaaS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactional emails AND marketing campaigns&lt;/li&gt;
&lt;li&gt;Event-triggered sequences&lt;/li&gt;
&lt;li&gt;$30/month for 10,000 contacts&lt;/li&gt;
&lt;li&gt;Visual email builder plus API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Buttondown&lt;/strong&gt; for email newsletters with API access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Markdown-based emails&lt;/li&gt;
&lt;li&gt;Simple API for programmatic sending&lt;/li&gt;
&lt;li&gt;$9/month for 1,000 subscribers&lt;/li&gt;
&lt;li&gt;Perfect for product updates and changelog notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Budget-Conscious Startups
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Amazon SES&lt;/strong&gt; remains unbeatable on price:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$0.10 per 1,000 emails&lt;/li&gt;
&lt;li&gt;Requires more setup and management&lt;/li&gt;
&lt;li&gt;Works perfectly with libraries like Nodemailer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a production-ready Python example using SES:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
import boto3&lt;br&gt;
from botocore.exceptions import ClientError&lt;/p&gt;

&lt;p&gt;class EmailService:&lt;br&gt;
    def &lt;strong&gt;init&lt;/strong&gt;(self):&lt;br&gt;
        self.ses_client = boto3.client('ses', region_name='us-east-1')&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def send_email(self, recipient: str, subject: str, html_body: str, &lt;br&gt;
               text_body: str = None):&lt;br&gt;
    try:&lt;br&gt;
        response = self.ses_client.send_email(&lt;br&gt;
            Source='&lt;a href="mailto:noreply@yoursaas.com"&gt;noreply@yoursaas.com&lt;/a&gt;',&lt;br&gt;
            Destination={'ToAddresses': [recipient]},&lt;br&gt;
            Message={&lt;br&gt;
                'Subject': {'Data': subject, 'Charset': 'UTF-8'},&lt;br&gt;
                'Body': {&lt;br&gt;
                    'Html': {'Data': html_body, 'Charset': 'UTF-8'},&lt;br&gt;
                    'Text': {'Data': text_body or '', 'Charset': 'UTF-8'}&lt;br&gt;
                }&lt;br&gt;
            }&lt;br&gt;
        )&lt;br&gt;
        return response['MessageId']&lt;br&gt;
    except ClientError as e:&lt;br&gt;
        print(f"Email send failed: {e.response['Error']['Message']}")&lt;br&gt;
        raise&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Usage&lt;br&gt;
&lt;/h1&gt;

&lt;p&gt;email_service = EmailService()&lt;br&gt;
email_service.send_email(&lt;br&gt;
    recipient='&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;',&lt;br&gt;
    subject='Your Receipt',&lt;br&gt;
    html_body='&lt;/p&gt;
&lt;h1&gt;Thanks for your purchase!&lt;/h1&gt;',&lt;br&gt;
    text_body='Thanks for your purchase!'&lt;br&gt;
)

&lt;h2&gt;
  
  
  Should You Build Your Own Email Service?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short answer: No.&lt;/strong&gt; But let me explain when you might consider it.&lt;/p&gt;

&lt;p&gt;Email deliverability is &lt;em&gt;hard&lt;/em&gt;. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proper SPF, DKIM, and DMARC records&lt;/li&gt;
&lt;li&gt;IP reputation management&lt;/li&gt;
&lt;li&gt;Bounce and complaint handling&lt;/li&gt;
&lt;li&gt;Spam filter avoidance&lt;/li&gt;
&lt;li&gt;Retry logic and queue management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unless you're sending millions of emails and can justify a dedicated email engineer, use a service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exception:&lt;/strong&gt; If you're already using SES for transactional emails and want to add a simple marketing layer, building a thin wrapper for campaigns can work. But start with existing services first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Framework
&lt;/h2&gt;

&lt;p&gt;Choose based on your actual needs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Resend or Postmark if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You only need transactional emails&lt;/li&gt;
&lt;li&gt;Developer experience matters to you&lt;/li&gt;
&lt;li&gt;You value reliability over features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose Loops or Buttondown if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need occasional marketing emails&lt;/li&gt;
&lt;li&gt;You want event-triggered sequences&lt;/li&gt;
&lt;li&gt;You prefer an all-in-one solution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose Amazon SES if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're on a tight budget&lt;/li&gt;
&lt;li&gt;You're comfortable with AWS&lt;/li&gt;
&lt;li&gt;You can handle the setup complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid SendGrid/Mailchimp if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're a solo developer or small team&lt;/li&gt;
&lt;li&gt;You don't need their enterprise features&lt;/li&gt;
&lt;li&gt;You want predictable pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started Today
&lt;/h2&gt;

&lt;p&gt;Here's my recommended approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with Resend&lt;/strong&gt; for transactional emails (it's free to 3,000 emails)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Buttondown&lt;/strong&gt; later if you need newsletters ($9/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrate to SES&lt;/strong&gt; only if costs become significant (&amp;gt;100k emails/month)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't overthink it. Email is infrastructure—it should be boring and reliable, not a project in itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The best simple email SaaS in 2024 isn't the one with the most features—it's the one you can integrate in 30 minutes and forget about. For most developers building SaaS products, that means Resend for transactional emails, possibly combined with Buttondown for newsletters.&lt;/p&gt;

&lt;p&gt;Stop shopping for email providers and start shipping features your users actually care about. The emails will get delivered either way.&lt;/p&gt;

</description>
      <category>email</category>
      <category>saas</category>
      <category>python</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Why There's No Example Code for B2B SaaS Apps (And What to Do About It)</title>
      <dc:creator>Brian Marsaw</dc:creator>
      <pubDate>Thu, 16 Jul 2026 23:23:17 +0000</pubDate>
      <link>https://dev.to/brino666/why-theres-no-example-code-for-b2b-saas-apps-and-what-to-do-about-it-2oma</link>
      <guid>https://dev.to/brino666/why-theres-no-example-code-for-b2b-saas-apps-and-what-to-do-about-it-2oma</guid>
      <description>&lt;h1&gt;
  
  
  Why There's No Example Code for B2B SaaS Apps (And What to Do About It)
&lt;/h1&gt;

&lt;p&gt;If you've ever searched for "B2B SaaS starter code" or "complete SaaS application example," you've probably come up empty-handed. While GitHub overflows with todo apps, blog platforms, and e-commerce demos, comprehensive examples of actual B2B SaaS applications are frustratingly rare.&lt;/p&gt;

&lt;p&gt;This isn't an accident. There are legitimate reasons why battle-tested B2B SaaS code remains locked away in private repositories. But more importantly, you don't need a complete example—you need the right patterns. Let's break down why examples are scarce and, more usefully, what architectural patterns you should implement instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why B2B SaaS Examples Are Rare
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Competitive Advantage&lt;/strong&gt;: Unlike a todo app, a well-architected B2B SaaS application represents thousands of hours of refined engineering decisions. Companies rightfully view their codebase as intellectual property. The multi-tenancy strategy, billing integration, and permission system that took months to perfect aren't going to be open-sourced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity&lt;/strong&gt;: B2B SaaS applications are complex beasts. They involve authentication, authorization at multiple levels, billing, subscription management, team management, audit logs, webhooks, API rate limiting, and data isolation. A truly representative example would be tens of thousands of lines of code—too large to be useful as a learning resource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context-Dependency&lt;/strong&gt;: What works for a project management tool differs drastically from what works for a CRM or analytics platform. There's no one-size-fits-all B2B SaaS architecture, which makes creating a "canonical" example nearly impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liability Concerns&lt;/strong&gt;: Publishing production-grade code for handling payments, user data, and multi-tenant isolation opens companies to scrutiny. If someone finds a security vulnerability in your open-source example, it reflects poorly on your brand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Critical Patterns You Actually Need
&lt;/h2&gt;

&lt;p&gt;Instead of waiting for a mythical complete example, focus on implementing these core patterns correctly. Master these, and you'll be 80% of the way there.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Multi-Tenancy with Row-Level Security
&lt;/h3&gt;

&lt;p&gt;The foundation of any B2B SaaS is proper data isolation. Here's a battle-tested PostgreSQL approach using row-level security:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
// schema.prisma (using Prisma)&lt;br&gt;
model Organization {&lt;br&gt;
  id        String   &lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt; &lt;a class="mentioned-user" href="https://dev.to/default"&gt;@default&lt;/a&gt;(cuid())&lt;br&gt;
  name      String&lt;br&gt;
  slug      String   &lt;a class="mentioned-user" href="https://dev.to/unique"&gt;@unique&lt;/a&gt;&lt;br&gt;
  users     User[]&lt;br&gt;
  projects  Project[]&lt;br&gt;
  createdAt DateTime &lt;a class="mentioned-user" href="https://dev.to/default"&gt;@default&lt;/a&gt;(now())&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;model Project {&lt;br&gt;
  id             String       &lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt; &lt;a class="mentioned-user" href="https://dev.to/default"&gt;@default&lt;/a&gt;(cuid())&lt;br&gt;
  name           String&lt;br&gt;
  organizationId String&lt;br&gt;
  organization   Organization @relation(fields: [organizationId], references: [id])&lt;br&gt;
  createdBy      String&lt;/p&gt;

&lt;p&gt;@@index([organizationId])&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Middleware to enforce tenant isolation&lt;br&gt;
import { PrismaClient } from '@prisma/client'&lt;/p&gt;

&lt;p&gt;export function createTenantClient(organizationId: string) {&lt;br&gt;
  const prisma = new PrismaClient()&lt;/p&gt;

&lt;p&gt;return prisma.$extends({&lt;br&gt;
    query: {&lt;br&gt;
      $allModels: {&lt;br&gt;
        async $allOperations({ args, query, model }) {&lt;br&gt;
          // Skip for Organization model&lt;br&gt;
          if (model === 'Organization') return query(args)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      // Inject organizationId filter for all operations
      if (args.where) {
        args.where = { ...args.where, organizationId }
      } else {
        args.where = { organizationId }
      }

      // Inject organizationId for creates
      if (args.data &amp;amp;&amp;amp; model !== 'User') {
        args.data = { ...args.data, organizationId }
      }

      return query(args)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;})&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This pattern ensures you can never accidentally query across tenant boundaries. It's saved me from data leakage bugs more times than I can count.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Role-Based Access Control (RBAC) That Scales
&lt;/h3&gt;

&lt;p&gt;Skip the simple &lt;code&gt;isAdmin&lt;/code&gt; boolean. You'll regret it. Here's a flexible RBAC system:&lt;/p&gt;

&lt;p&gt;python&lt;/p&gt;

&lt;h1&gt;
  
  
  models.py (using SQLAlchemy)
&lt;/h1&gt;

&lt;p&gt;from enum import Enum&lt;br&gt;
from sqlalchemy import Table, Column, String, ForeignKey&lt;br&gt;
from sqlalchemy.orm import relationship&lt;/p&gt;

&lt;p&gt;class Permission(Enum):&lt;br&gt;
    PROJECT_CREATE = "project:create"&lt;br&gt;
    PROJECT_READ = "project:read"&lt;br&gt;
    PROJECT_UPDATE = "project:update"&lt;br&gt;
    PROJECT_DELETE = "project:delete"&lt;br&gt;
    BILLING_MANAGE = "billing:manage"&lt;br&gt;
    USERS_INVITE = "users:invite"&lt;/p&gt;

&lt;p&gt;user_roles = Table('user_roles', Base.metadata,&lt;br&gt;
    Column('user_id', String, ForeignKey('users.id')),&lt;br&gt;
    Column('role_id', String, ForeignKey('roles.id'))&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;role_permissions = Table('role_permissions', Base.metadata,&lt;br&gt;
    Column('role_id', String, ForeignKey('roles.id')),&lt;br&gt;
    Column('permission', String)&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;class Role(Base):&lt;br&gt;
    &lt;strong&gt;tablename&lt;/strong&gt; = 'roles'&lt;br&gt;
    id = Column(String, primary_key=True)&lt;br&gt;
    name = Column(String, nullable=False)&lt;br&gt;
    organization_id = Column(String, ForeignKey('organizations.id'))&lt;br&gt;
    permissions = Column(ARRAY(String))  # Store as array for simplicity&lt;/p&gt;

&lt;p&gt;class User(Base):&lt;br&gt;
    &lt;strong&gt;tablename&lt;/strong&gt; = 'users'&lt;br&gt;
    id = Column(String, primary_key=True)&lt;br&gt;
    email = Column(String, unique=True, nullable=False)&lt;br&gt;
    organization_id = Column(String, ForeignKey('organizations.id'))&lt;br&gt;
    roles = relationship('Role', secondary=user_roles)&lt;/p&gt;

&lt;h1&gt;
  
  
  Authorization decorator
&lt;/h1&gt;

&lt;p&gt;from functools import wraps&lt;br&gt;
from flask import g, abort&lt;/p&gt;

&lt;p&gt;def require_permission(permission: Permission):&lt;br&gt;
    def decorator(f):&lt;br&gt;
        &lt;a class="mentioned-user" href="https://dev.to/wraps"&gt;@wraps&lt;/a&gt;(f)&lt;br&gt;
        def decorated_function(*args, **kwargs):&lt;br&gt;
            user = g.current_user&lt;br&gt;
            user_permissions = set()&lt;br&gt;
            for role in user.roles:&lt;br&gt;
                user_permissions.update(role.permissions)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        if permission.value not in user_permissions:
            abort(403, "Insufficient permissions")

        return f(*args, **kwargs)
    return decorated_function
return decorator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
  
  
  Usage
&lt;/h1&gt;

&lt;p&gt;@app.route('/api/projects', methods=['POST'])&lt;br&gt;
@require_permission(Permission.PROJECT_CREATE)&lt;br&gt;
def create_project():&lt;br&gt;
    # Your logic here&lt;br&gt;
    pass&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Subscription and Usage Tracking
&lt;/h3&gt;

&lt;p&gt;You need to track usage limits and gate features based on subscription tiers:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
// subscriptionService.ts&lt;br&gt;
import Stripe from 'stripe'&lt;/p&gt;

&lt;p&gt;const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)&lt;/p&gt;

&lt;p&gt;export enum PlanTier {&lt;br&gt;
  FREE = 'free',&lt;br&gt;
  PRO = 'pro',&lt;br&gt;
  ENTERPRISE = 'enterprise'&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;interface PlanLimits {&lt;br&gt;
  maxProjects: number&lt;br&gt;
  maxUsers: number&lt;br&gt;
  apiCallsPerMonth: number&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;const PLAN_LIMITS: Record = {&lt;br&gt;
  [PlanTier.FREE]: { maxProjects: 3, maxUsers: 5, apiCallsPerMonth: 1000 },&lt;br&gt;
  [PlanTier.PRO]: { maxProjects: 50, maxUsers: 50, apiCallsPerMonth: 100000 },&lt;br&gt;
  [PlanTier.ENTERPRISE]: { maxProjects: -1, maxUsers: -1, apiCallsPerMonth: -1 }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;export class SubscriptionService {&lt;br&gt;
  async canPerformAction(&lt;br&gt;
    organizationId: string,&lt;br&gt;
    action: keyof PlanLimits&lt;br&gt;
  ): Promise {&lt;br&gt;
    const org = await prisma.organization.findUnique({&lt;br&gt;
      where: { id: organizationId },&lt;br&gt;
      include: { subscription: true }&lt;br&gt;
    })&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (!org?.subscription) return false

const limits = PLAN_LIMITS[org.subscription.tier as PlanTier]
const limit = limits[action]

// -1 means unlimited
if (limit === -1) return true

const currentUsage = await this.getCurrentUsage(organizationId, action)
return currentUsage &amp;lt; limit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;private async getCurrentUsage(&lt;br&gt;
    organizationId: string,&lt;br&gt;
    metric: keyof PlanLimits&lt;br&gt;
  ): Promise {&lt;br&gt;
    switch (metric) {&lt;br&gt;
      case 'maxProjects':&lt;br&gt;
        return await prisma.project.count({ where: { organizationId } })&lt;br&gt;
      case 'maxUsers':&lt;br&gt;
        return await prisma.user.count({ where: { organizationId } })&lt;br&gt;
      case 'apiCallsPerMonth':&lt;br&gt;
        return await this.getAPICallsThisMonth(organizationId)&lt;br&gt;
      default:&lt;br&gt;
        return 0&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Your Own Reference Architecture
&lt;/h2&gt;

&lt;p&gt;Rather than searching for a complete example, build your own reference implementation incrementally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with authentication&lt;/strong&gt;: Use Auth0, Clerk, or Supabase Auth. Don't roll your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add multi-tenancy&lt;/strong&gt;: Implement the row-level security pattern above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement RBAC&lt;/strong&gt;: Start simple, but use a structure that can grow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate billing&lt;/strong&gt;: Stripe is the standard. Use their webhooks religiously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add audit logging&lt;/strong&gt;: Track every important action from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build API rate limiting&lt;/strong&gt;: Use Redis and the token bucket algorithm.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these deserves its own deep dive, but this foundation will serve you better than any todo-app-style example ever could.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Patterns Over Examples
&lt;/h2&gt;

&lt;p&gt;The lack of complete B2B SaaS examples isn't a problem to solve—it's a reality to accept. The real value isn't in seeing someone else's monolithic codebase; it's in understanding the patterns that make B2B SaaS applications work at scale.&lt;/p&gt;

&lt;p&gt;Focus on multi-tenancy, authorization, subscription management, and audit logging. Get these right from the start, and you'll avoid the painful rewrites that plague SaaS startups.&lt;/p&gt;

&lt;p&gt;The code you write today, informed by these patterns, will become your own reference architecture. And it'll be better than any generic example because it'll be tailored to your specific domain and requirements.&lt;/p&gt;

&lt;p&gt;Now stop searching for the perfect example and start building. The patterns above are your blueprint.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>b2b</category>
      <category>typescript</category>
      <category>python</category>
    </item>
  </channel>
</rss>
