eduardosasso/bullish

View on GitHub
site/blog/building-a-micro-saas-with-mailerlite-netlify-stripe-and-zapier/index.html

Summary

Maintainability
Test Coverage
<!DOCTYPE html>

<html class="no-js" lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="ie=edge" />
    <title>Building a micro SaaS with MailerLite, Netlify, Stripe and Zapier</title>
    <meta property="og:title" content="From stock market email newsletter side project to micro SaaS" />
    <meta
      property="og:description"
      content="A couple of months back, during this crazy world pandemic, I had an idea for a Stock Market email newsletter."
    />
    <meta
      property="og:image"
      content="https://bullish.email/blog/building-a-micro-saas-with-mailerlite-netlify-stripe-and-zapier/social.png"
    />
    <meta property="og:image:width" content="1200" />
    <meta property="og:image:height" content="630" />
    <meta property="og:url" content="https://bullish.email" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta
      name="twitter:description"
      content="A couple of months back, during this crazy world pandemic, I had an idea for a Stock Market email newsletter."
    />
    <meta name="twitter:title" content="From stock market email newsletter side project to micro SaaS" />
    <meta
      name="twitter:image"
      content="https://bullish.email/blog/building-a-micro-saas-with-mailerlite-netlify-stripe-and-zapier/social.png"
    />
    <meta name="twitter:creator" content="@eduardosasso" />

    <meta name="generator" content="Leter" />
    <meta
      name="description"
      content="A couple of months back, during this crazy world pandemic, I had an idea for a Stock Market email newsletter."
    />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <link rel="preconnect" href="https://cdn.jsdelivr.net/" crossorigin />
    <link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />

    <link
      rel="icon"
      type="image/png"
      sizes="32x32"
      href="/assets/icons/favicon-32x32.png"
    />
    <link
      rel="icon"
      type="image/png"
      sizes="16x16"
      href="/assets/icons/favicon-16x16.png"
    />

    <style>
      :root {
        --background_color: #fff;
        --page_align: 0 auto;
        --text_font: "Rubik", sans-serif;
        --text_color: #212529;
        --text_size: 20px;
        --heading_font: "Montserrat";
        --heading_color: #1b262c;
        --accent_color: #1b262c;
        --link_color: #21bf73;
      }
    </style>

    <link rel="stylesheet" href="/bullish.css" />

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"
    />

    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Rubik|Montserrat:500,800&display=swap"
    />

    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@glidejs/glide@3.4.1/dist/css/glide.core.min.css"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/@glidejs/glide@3.4.1/dist/css/glide.theme.min.css"
    />

    <script
      async
      src="https://cdn.jsdelivr.net/npm/@glidejs/glide@3.4.1/dist/glide.min.js"
    ></script>

    <script>
      window.onload = function() {
        var conf = {
          type: "slider",
          perView: 1,
          focusAt: "center"
        };

        var sliders = document.querySelectorAll(".glide");

        for (var i = 0; i < sliders.length; i++) {
          var glide = new Glide(sliders[i], conf);
          glide.mount();
        }
      };
    </script>
    <!-- MailerLite Universal -->
    <script>
    (function(m,a,i,l,e,r){ m['MailerLiteObject']=e;function f(){
    var c={ a:arguments,q:[]};var r=this.push(c);return "number"!=typeof r?r:f.bind(c.q);}
    f.q=f.q||[];m[e]=m[e]||f.bind(f.q);m[e].q=m[e].q||f.q;r=a.createElement(i);
    var _=a.getElementsByTagName(i)[0];r.async=1;r.src=l+'?v'+(~~(new Date().getTime()/1000000));
    _.parentNode.insertBefore(r,_);})(window, document, 'script', 'https://static.mailerlite.com/js/universal.js', 'ml');

    var ml_account = ml('accounts', '2024694', 'v7f1i7g4p5', 'load');
    </script>
    <!-- End MailerLite Universal -->

  </head>

  <body id="classic">
    <section>
      <div class="logo"><a href="/"><img src="../logo.png" /></a></div>
      <h1>Building a micro SaaS with MailerLite, Netlify, Stripe and Zapier</h1>
      <hr />
      <p>
        A couple of months back, during this crazy world pandemic, I had an idea
        for a <a href="https://bullish.email">Stock Market email newsletter</a>.
      </p>
      <p>
        Nothing novel, the initial premise was to build a fully automated
        hands-off email with some key stats I care about and send it every
        weekday before the markets open, and that’s how
        <a href="https://bullish.email">https://bullish.email</a> came to be.
      </p>
      <p>
        Doing rounds trying to promote my little pet project, I posted it on
        <a href="https://news.ycombinator.com/item?id=22870667">Hacker News</a>
        and received great feedback, and one thousand people were convinced
        enough to subscribe.
      </p>
      <p>
        That initial traction, combined with all the positive comments and
        suggestions, was the signal I needed to upgrade from side hustle to tiny
        micro SaaS indie venture.
      </p>
      <p>
        I planned to offer a premium version of the newsletter with a lot more
        data points and insights and charge money for it. Plain and simple.
      </p>
      <p>
        But to do that, I had to do a good amount of code refactoring, hookup
        Stripe, update the website, make all the plumbing work, and promote it.
      </p>
      <p>And that’s where the fun begins again. Let’s get to work!</p>
      <p>
        The first thing I tackled was to
        <a href="https://github.com/eduardosasso/bullish/tree/master/services"
          >build new data points</a
        >, all of them based on variations of the unofficial
        <a
          href="https://github.com/eduardosasso/bullish/blob/master/services/config.rb"
          >Yahoo Finance API</a
        >.
      </p>
      <p>From those API’s I’ve extracted insights like</p>
      <ul>
        <li>Performance by sector</li>
        <li>Trending stocks</li>
        <li>Top gainers and losers</li>
        <li>All-time high stats</li>
        <li>Crypto performance</li>
      </ul>
      <p>There’s a lot you can do with Yahoo’s API.</p>
      <p>
        For each of those data points, I formatted their results with an
        emphasis on percentage performance to keep it consistent with the
        original version of Bullish and maintain its uniqueness.
      </p>
      <p>
        Next, I had to find a way to design and easily code email layouts.
        Creating email templates is a huge pain. You have to inline CSS
        everywhere, nothing works, and every change is a pain and hard to reuse.
        There’s gotta be a better way.
      </p>
      <p>
        After some research, I found <a href="https://mjml.io/">MJML</a> , which
        is essentially a markup layer on top of HTML built for designing
        responsive emails. It works great, no more writing arcane HTML and
        fighting email client compatibility.
      </p>
      <p>
        My mental model for Bullish Pro centered around the concept
        <a href="https://github.com/eduardosasso/bullish/tree/master/editions"
          >editions</a
        >, and that translated to a
        <a
          href="https://github.com/eduardosasso/bullish/blob/master/editions/free.rb"
          >free edition</a
        >, a
        <a
          href="https://github.com/eduardosasso/bullish/blob/master/editions/morning.rb"
          >morning edition</a
        >, and an
        <a
          href="https://github.com/eduardosasso/bullish/blob/master/editions/afternoon.rb"
          >afternoon edition</a
        >
        for paid subscribers.
      </p>
      <p>
        Editions are composed of
        <a
          href="https://github.com/eduardosasso/bullish/blob/master/editions/widgets.rb"
          >elements or widgets</a
        >
        and change daily, like a newspaper. Between Monday and Friday, each
        edition includes a set of widgets reused interchangeably to create the
        final email layout.
      </p>
      <p>
        This notion of daily editions combined with elements gives a lot of
        versatility when creating layouts.
      </p>
      <p></p>
      <div class="glide">
        <div class="glide__track" data-glide-el="track">
          <ul class="slider glide__slides">
            <li class="glide__slide">
              <figure>
                <img src="./email4.png" alt="Other variations" />
                <figcaption>Other variations</figcaption>
              </figure>
            </li>
            
            <li class="glide__slide">
              <figure>
                <img src="./email2.png" alt="Different elements combined" />
                <figcaption>Different elements combined</figcaption>
              </figure>
            </li>

            <li class="glide__slide">
              <figure>
                <img src="./email1.png" alt="Different elements combined" />
                <figcaption>Different elements combined</figcaption>
              </figure>
            </li>

            <li class="glide__slide">
              <figure>
                <img src="./email3.png" alt="Other variations" />
                <figcaption>Other variations</figcaption>
              </figure>
            </li>
          </ul>
        </div>

        <div class="glide__bullets" data-glide-el="controls[nav]">
          <button class="glide__bullet" data-glide-dir="=0"></button>

          <button class="glide__bullet" data-glide-dir="=1"></button>

          <button class="glide__bullet" data-glide-dir="=2"></button>

          <button class="glide__bullet" data-glide-dir="=3"></button>
        </div>
      </div>

      <p>
        Each element gets hooked up to a data point, so for trending stocks, you
        can expect to have a widget to render that, same thing for pre-market
        futures, crypto, etc.
      </p>
      <p>
        Elements use the
        <a href="http://mustache.github.io/">Mustache</a> template engine to
        take variables and replace them with data and do some formatting like
        green or red if the value is positive or negative.
      </p>
      <p>
        With this design, it’s easy to add new components and move them around
        to create unique
        <a
          href="https://github.com/eduardosasso/bullish/blob/master/templates/template.rb"
          >templates</a
        >
        with almost zero code.
      </p>
      <p>
        Editions have a tag specifying their group ID, so emails go out to the
        right group free or premium.
      </p>
      <p>
        Of course, there’s a lot of plumbing involved in stitching everything
        together like compressing emails to be
        <a href="https://mailchimp.com/help/gmail-is-clipping-my-email/"
          >under 102k</a
        >, so they don’t break in Gmail and things like that, but that’s a good
        overview of how I revamped Bullish to support different email formats
        without giving away the idea of full automation.
      </p>
      <p>
        The infrastructure is still pretty much the same, three CRON jobs set up
        in a Raspberry PI to create the HTML of each edition and calls to
        <a href="https://www.mailerlite.com/">MailerLite</a> API to schedule
        distribution. Check the
        <a
          href="https://bullish.email/blog/turning-my-obsession-in-the-stock-market-into-a-side-project/"
          >previous article</a
        >
        for more details.
      </p>
      <p>
        For payments, I used Stripe and their drop-in
        <a href="https://stripe.com/docs/payments/checkout">checkout flow</a> to
        accept payments and
        <a href="https://stripe.com/billing">Stripe Billing</a> for managing
        recurring subscriptions connected to a few
        <a href="https://zapier.com/">Zapier</a> recipes to handle users moving
        from free group to premium and downgrading from premium back to free
        when they cancel.
      </p>
      <p>
        With Stripe Billing and a sprinkle of Javascript
        <a href="https://www.netlify.com/products/functions/">cloud function</a>
        deployed to <a href="https://www.netlify.com">Netlify</a>, I’ve set up a
        magic link on premium emails, so paid users can update or cancel their
        subscriptions directly in Stripe securely.
      </p>
      <p>
        Based on the customer id, Stripe generates a unique link on demand so
        users can update their subscription without having to log in or create
        an account, it’s zero friction.
      </p>
      <p>
        Leveraging Zapier was a big time saver, and it’s less code I need to
        write and maintain. They offer a free plan that includes five zaps, and
        that’s what I use.
      </p>
      <p>
        Another huge time saver was moving the website over to Netlify. They
        have such an excellent product with killer features like branch previews
        for testing, automatic asset compression, and serverless functions that
        are ridiculously easy to use with no config whatsoever write Javascript
        and deploy plus a generous free tier.
      </p>
      <p>
        <a href="https://stripe.com">Stripe</a> is also another great product.
        From documentation to testing and configuration and support for Apple
        and Google pay, everything was a joy to use and simple to integrate into
        the flow I had in mind and seamless to users—big fan.
      </p>
      <p>
        I let this setup run for a few days to make sure everything worked as
        expected before I made my first move into upselling the premium version.
      </p>
      <p>Time to sell.</p>
      <p>
        My initial strategy was to convert existing subscribers first before
        going out to the world.
      </p>
      <p>
        So I crafted an email in a personal tone, where the subject was “<a
          href="https://preview.mailerlite.com/o2y8k1"
          >It’s launch day!</a
        >”. In this email, I started by giving an overview where Bullish was at
        and then introducing Bullish Premium for 4.99/mo along with all the
        features we were releasing and a big green call to action button saying,
        “<strong>Subscribe now</strong>.”
      </p>
      <p>
        My honest expectation was to get maybe four or five users to convert,
        but we end up closing ten on the first day. We celebrated with Sushi!
      </p>
      <p>
        That got us to an instant
        <strong>$50 monthly recurring revenue!</strong> Almost ramen
        profitability.
      </p>
      <p>
        The numbers are so small, but you’ve got to start somewhere. I’ve done
        lots of side projects over the years, and they all brought me some
        indirect form of monetization. It was through a side project that I
        ended up in Silicon Valley, but this is the first time I’m directly
        selling things to people.
      </p>
      <p>
        After some validation from the first sales, I moved on to the next big
        topic in my todo—a new website.
      </p>
      <p>
        I’ve put together the first version of the website in a couple of hours
        using my yet-to-be-released static site generator called
        <a href="https://github.com/eduardosasso/leter">Leter</a>, which also
        powers this article by the way.
      </p>
      <p>
        <figure>
          <img src="./first_site.png" alt="First version" />
          <figcaption>First version</figcaption>
        </figure>
      </p>
      <p>
        It was Ok but far from what I had in mind, but I had to ship something,
        so I pushed a professional better-looking website for later.
      </p>
      <p>
        I know my weaknesses, and web design is a time sink for me. I’m very
        opinionated, and I like to think I have a good eye for it, but so far, I
        couldn’t deliver anything I was proud of on my own.
      </p>
      <p>
        After learning from my fail attempts over the years trying to come up
        with an exclusive design this time around, I looked for references,
        inspiration, and templates that could help me jump-start the process.
      </p>
      <p>
        As an engineer, I always feel inclined to start everything from scratch,
        and with the new website was no different, the utopian dream of the
        perfect, valid HTML and clean CSS a trap that I fell into way too many
        times but not again.
      </p>
      <p>
        I ended up
        <a
          href="https://themeforest.net/item/fold-software-and-app-template/24295615"
          >finding a template</a
        >
        that was pretty close to what I had in mind. I just had to brush off my
        CSS skills and do some customization to take my vision to reality.
      </p>
      <p>
        Of course, I have a list of things to improve like adding archives or a
        way to update the sample email dynamically, but overall I’m pretty happy
        how it turned out.
      </p>
      <p>
        <figure>
          <img src="./new_site.png" alt="New version" />
          <figcaption>New version</figcaption>
        </figure>
      </p>
      <p>With the site up and running, it was time to promote it.</p>
      <p>
        Before trying the big leagues on
        <a href="https://www.producthunt.com">Product Hunt</a>, we tested the
        waters by engaging on Twitter and getting a few subscribers. Then, I
        asked my
        <a href="https://www.linkedin.com/in/abduzeedo/">influencer brother</a>
        to post on his
        <a
          href="https://www.linkedin.com/feed/update/urn:li:activity:6693739953026945024/"
          >LinkedIn</a
        >, which generated a nice amount of traffic and about fifty new readers,
        not hockey stick growth but decent.
      </p>
      <p>
        For
        <a href="https://www.producthunt.com/posts/bullish">Product Hunt</a>, we
        scheduled our launch for a Wednesday, and I remember going to sleep that
        night, excited to see what the next day would bring, and we bombed, we
        never made to the frontpage and only got like 20 upvotes it was a total
        disaster.
      </p>
      <p>
        The next day was business as usual, a little bruised, maybe, but that’s
        how things go, I guess it will take a few more years to be an overnight
        success.
      </p>
      <p>
        Fast forward to Sunday morning. Things are back to normal, I’m having
        pancakes for breakfast, and I start seeing this uptick in subscribers
        out of the blue.
      </p>
      <p>
        So it turns out
        <a href="https://www.producthunt.com/posts/bullish"
          >Product Hunt featured us</a
        >
        in their Sunday edition, and we were on the front page the whole day,
        gaining around 200 new subscribers. What a comeback!
      </p>
      <p>
        We're still trying to find the right niche to go after and experimenting with a bunch of ideas to see what sticks. It's all learning sometimes by trial and error. Most times it is by building something that I would like to use it myself and maybe also useful to others.
      </p>
      <p>
        All and all, Bullish has been a welcome surprise in
        <a href="https://eduardosasso.co">my life</a>. I’m happy about the
        consistency to which I can work on it almost every night and a few
        weekends and how having constraints helps you narrow your focus, and it
        compounds beautifully.
      </p>
      <p>Lots more to come.</p>
      <p>Give <a href="https://bullish.email">Bullish▲</a> a try!</p>
      <p>Cheers.</p>
    </section>
  </body>

  <script>
    window.ga =
      window.ga ||
      function() {
        (ga.q = ga.q || []).push(arguments);
      };
    ga.l = +new Date();
    ga("create", "UA-148146327-2", "auto");
    ga("send", "pageview");
  </script>
  <script async src="https://www.google-analytics.com/analytics.js"></script>
<