<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ SaaS - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ SaaS - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 07 May 2026 04:04:44 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/saas/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Multi-Tenant SaaS Platform with Next.js, Express, and Prisma ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever wondered how platforms like Webflow, Notion, or Hashnode serve thousands of users from a single codebase — each with their own unique URL? The answer is multi-tenancy: an architecture wh ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-multi-tenant-saas-platform-with-next-js-express-and-prisma/</link>
                <guid isPermaLink="false">69f213e46e0124c05e19e1af</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ prisma ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Michael Okolo ]]>
                </dc:creator>
                <pubDate>Wed, 29 Apr 2026 14:21:24 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ef0c87aa-4455-4230-9669-bf2c13db9947.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever wondered how platforms like Webflow, Notion, or Hashnode serve thousands of users from a single codebase — each with their own unique URL?</p>
<p>The answer is multi-tenancy: an architecture where one application dynamically serves isolated experiences to many different users, often through subdomains.</p>
<p>In this tutorial, you'll build a multi-tenant portfolio SaaS platform from scratch using Next.js, Express, and Prisma. Each user who signs up gets their own portfolio site, served on their own subdomain — generated instantly, powered by a single backend, and stored in a single database.</p>
<p>Here's what you'll build:</p>
<ul>
<li><p>A landing page where users fill out a form to create their portfolio</p>
</li>
<li><p>An Express + Prisma backend that stores each user as a "tenant"</p>
</li>
<li><p>A Next.js middleware layer that detects subdomains and routes requests dynamically</p>
</li>
<li><p>A JSON-driven template system that controls which sections appear on each portfolio</p>
</li>
<li><p>A production-ready portfolio page served at <code>name.localhost:3000</code> in development and <code>name.yourdomain.com</code> in production</p>
</li>
</ul>
<p>You can find the complete source code in the GitHub repositories linked at the end of this tutorial.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-multi-tenancy">What is Multi-Tenancy?</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-backend">How to Set Up the Backend</a></p>
<ul>
<li><p><a href="#heading-how-to-install-dependencies">How to Install Dependencies</a></p>
</li>
<li><p><a href="#heading-how-to-configure-typescript-for-esm">How to Configure TypeScript for ESM</a></p>
</li>
<li><p><a href="#heading-how-to-initialize-prisma">How to Initialize Prisma</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-define-the-prisma-schema">How to Define the Prisma Schema</a></p>
</li>
<li><p><a href="#heading-how-to-run-your-first-migration">How to Run Your First Migration</a></p>
</li>
<li><p><a href="#heading-how-to-generate-and-instantiate-the-prisma-client">How to Generate and Instantiate the Prisma Client</a></p>
<ul>
<li><p><a href="#heading-how-to-generate-the-client">How to Generate the Client</a></p>
</li>
<li><p><a href="#heading-how-to-instantiate-the-client">How to Instantiate the Client</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-seed-a-template">How to Seed a Template</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-express-api">How to Build the Express API</a></p>
<ul>
<li><p><a href="#heading-how-to-install-express">How to Install Express</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-server-entry-point">How to Create the Server Entry Point</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-express-app">How to Create the Express App</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-tenant-controller">How to Create the Tenant Controller</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-tenant-routes">How to Create the Tenant Routes</a></p>
</li>
<li><p><a href="#heading-how-to-start-the-server">How to Start the Server</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-create-the-nextjs-frontend">How to Create the Next.js Frontend</a></p>
</li>
<li><p><a href="#heading-how-to-add-subdomain-routing-with-middleware">How to Add Subdomain Routing with Middleware</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-landing-page">How to Build the Landing Page</a></p>
<ul>
<li><p><a href="#heading-how-to-update-the-layout">How to Update the Layout</a></p>
</li>
<li><p><a href="#heading-how-to-create-the-home-page">How to Create the Home Page</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-tenant-portfolio-page">How to Build the Tenant Portfolio Page</a></p>
</li>
<li><p><a href="#heading-how-to-test-the-full-flow">How to Test the Full Flow</a></p>
</li>
<li><p><a href="#heading-next-steps">Next Steps</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-source-code">Source Code</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have the following:</p>
<ul>
<li><p>Node.js (version 18 or higher) installed on your machine</p>
</li>
<li><p>A basic understanding of React, TypeScript, and REST APIs</p>
</li>
<li><p>Familiarity with Prisma ORM (you don't need to be an expert)</p>
</li>
<li><p>A code editor like VS Code</p>
</li>
</ul>
<p>You'll use Prisma Postgres as your database, so you won't need to set up a separate database server locally. Prisma handles the connection string and adapter configuration for you.</p>
<h2 id="heading-what-is-multi-tenancy">What is Multi-Tenancy?</h2>
<p>Multi-tenancy is an architectural pattern where a single application serves multiple users — called tenants — each with isolated data and often their own URL.</p>
<p>Here's how the flow works in this tutorial:</p>
<ol>
<li><p>A user visits your landing page and fills out a form with their name, bio, and skills.</p>
</li>
<li><p>Your Express backend creates a new tenant record in the database and generates a slug from their name.</p>
</li>
<li><p>The browser redirects the user to <code>their-name.localhost:3000</code>.</p>
</li>
<li><p>Your Next.js middleware detects the subdomain, extracts the slug, and rewrites the request to <code>/tenant/their-name</code> internally.</p>
</li>
<li><p>The tenant page fetches that user's data from the API and renders their portfolio.</p>
</li>
</ol>
<p>The key insight is that the URL in the browser never changes — the rewrite is invisible to the user. One Next.js app serves every tenant dynamically.</p>
<h2 id="heading-how-to-set-up-the-backend">How to Set Up the Backend</h2>
<p>Start by creating a project folder with separate directories for the backend and frontend:</p>
<pre><code class="language-bash">mkdir portfolio-saas &amp;&amp; cd portfolio-saas
mkdir portfolio-api portfolio-client
</code></pre>
<p>Navigate into the backend directory and initialize a new Node.js project:</p>
<pre><code class="language-bash">cd portfolio-api
npm init -y
</code></pre>
<h3 id="heading-how-to-install-dependencies">How to Install Dependencies</h3>
<p>Install TypeScript, Prisma, and the supporting packages:</p>
<pre><code class="language-bash">npm install typescript tsx @types/node --save-dev
npx tsc --init
npm install prisma @types/node @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg pg dotenv
</code></pre>
<h3 id="heading-how-to-configure-typescript-for-esm">How to Configure TypeScript for ESM</h3>
<p>Open <code>tsconfig.json</code> and replace its contents with:</p>
<pre><code class="language-json">{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "target": "ES2023",
    "strict": true,
    "esModuleInterop": true,
    "ignoreDeprecations": "6.0",
    "types": ["node"]
  }
}
</code></pre>
<p>Then open <code>package.json</code> and add <code>"type": "module"</code> to enable ESM:</p>
<pre><code class="language-json">{
  "type": "module"
}
</code></pre>
<h3 id="heading-how-to-initialize-prisma">How to Initialize Prisma</h3>
<p>Run the following command to initialize Prisma and generate your schema setup:</p>
<pre><code class="language-bash">npx prisma init --db --output ../generated/prisma
</code></pre>
<p>This command creates a <code>prisma/schema.prisma</code> file, a <code>.env</code> file with your database connection string, and a generated Prisma configuration folder.</p>
<h2 id="heading-how-to-define-the-prisma-schema">How to Define the Prisma Schema</h2>
<p>Open <code>prisma/schema.prisma</code> and replace its contents with the following:</p>
<pre><code class="language-prisma">generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model Tenant {
  id         String   @id @default(uuid())
  slug       String   @unique
  name       String
  bio        String
  skills     String[]
  templateId String
  createdAt  DateTime @default(now())
}

model Template {
  id     String @id @default(uuid())
  name   String
  config Json
}

model Post {
  id         String   @id @default(uuid())
  title      String
  content    String
  tenantSlug String
  createdAt  DateTime @default(now())
}
</code></pre>
<p>Let's look at what each model does.</p>
<p>The <code>Tenant</code> model represents a user who has signed up and created a portfolio. The <code>slug</code> field is generated from their name (for example, "John Doe" becomes <code>john-doe</code>) and is used as their subdomain. The <code>templateId</code> links each tenant to a template that controls their portfolio's layout.</p>
<p>The <code>Template</code> model stores layout configuration as JSON. Instead of hardcoding sections like "hero" or "skills" into your components, you store them in the database. This means you can add or remove sections for different templates without touching any component code.</p>
<p>The <code>Post</code> model is included for future extensibility — you can use it to let tenants publish blog posts on their portfolio.</p>
<h2 id="heading-how-to-run-your-first-migration">How to Run Your First Migration</h2>
<p>Run the following command to create your database tables based on the schema:</p>
<pre><code class="language-bash">npx prisma migrate dev --name init
</code></pre>
<p>This command creates the database tables, generates a migration file, and applies the migration to your database. After it runs, your Postgres database structure matches your Prisma schema exactly.</p>
<h2 id="heading-how-to-generate-and-instantiate-the-prisma-client">How to Generate and Instantiate the Prisma Client</h2>
<h3 id="heading-how-to-generate-the-client">How to Generate the Client</h3>
<p>Run this command to generate a fully type-safe Prisma Client based on your schema:</p>
<pre><code class="language-bash">npx prisma generate
</code></pre>
<p>You only need to run this once after each schema change. The generated client lives in the <code>../generated/prisma</code> folder you configured earlier.</p>
<h3 id="heading-how-to-instantiate-the-client">How to Instantiate the Client</h3>
<p>Create a new file at <code>lib/prisma.ts</code> and add the following:</p>
<pre><code class="language-typescript">import 'dotenv/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';

const connectionString = process.env.DATABASE_URL as string;

const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });

export default prisma;
</code></pre>
<p>This file creates a single shared Prisma Client instance that your entire backend will import. The <code>PrismaPg</code> adapter connects Prisma to your Postgres database using the connection string from your <code>.env</code> file.</p>
<h2 id="heading-how-to-seed-a-template">How to Seed a Template</h2>
<p>Your platform needs at least one template in the database before any tenant can sign up. Instead of hardcoding layout decisions into your components, you'll store the template configuration as JSON and read it at runtime.</p>
<p>Create a new file at <code>prisma/seed.ts</code> and add the following:</p>
<pre><code class="language-typescript">import prisma from '../lib/prisma';

async function main() {
  await prisma.template.create({
    data: {
      name: 'minimal',
      config: {
        theme: {
          primaryColor: '#6366f1',
          background: 'dark',
        },
        sections: {
          hero: true,
          about: true,
          skills: true,
          projects: true,
          blog: true,
          contact: true,
        },
      },
    },
  });

  console.log('Template seeded successfully.');
}

main()
  .catch((e) =&gt; {
    console.error(e);
    process.exit(1);
  })
  .finally(async () =&gt; {
    await prisma.$disconnect();
  });
</code></pre>
<p>The <code>config</code> field is stored as JSON in Postgres. When a tenant's portfolio loads, your frontend reads this JSON to decide which sections to show. Setting <code>hero: false</code> on a template would hide the hero section for every tenant using that template — no code changes needed.</p>
<p>Now add a seed script to your <code>package.json</code>:</p>
<pre><code class="language-json">{
  "scripts": {
    "seed": "tsx prisma/seed.ts"
  }
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">npm run seed
</code></pre>
<p>Your database now has a default template ready to attach to new tenants.</p>
<h2 id="heading-how-to-build-the-express-api">How to Build the Express API</h2>
<p>Now you'll build the backend API that creates tenants and retrieves their data.</p>
<h3 id="heading-how-to-install-express">How to Install Express</h3>
<pre><code class="language-bash">npm install express cors
npm install -D @types/express @types/cors
</code></pre>
<h3 id="heading-how-to-create-the-server-entry-point">How to Create the Server Entry Point</h3>
<p>Create <code>src/index.ts</code>:</p>
<pre><code class="language-typescript">import app from './app';

const PORT = 8080;

app.listen(PORT, () =&gt; {
  console.log('Server is running on port 8080');
});
</code></pre>
<h3 id="heading-how-to-create-the-express-app">How to Create the Express App</h3>
<p>Create <code>src/app.ts</code>:</p>
<pre><code class="language-typescript">import express from 'express';
import cors from 'cors';
import tenantRoutes from './routes/tenant.routes';

const app = express();

app.use(cors());
app.use(express.json());
app.use('/api', tenantRoutes);

export default app;
</code></pre>
<p>This file sets up CORS so your Next.js frontend can communicate with the API, parses JSON request bodies, and mounts all tenant routes under the <code>/api</code> prefix.</p>
<h3 id="heading-how-to-create-the-tenant-controller">How to Create the Tenant Controller</h3>
<p>Create <code>src/controllers/tenant.controller.ts</code>:</p>
<pre><code class="language-typescript">import { Request, Response } from 'express';
import prisma from '../../lib/prisma';

export async function createTenant(req: Request, res: Response) {
  const { name, bio, skills } = req.body;

  if (!name || !bio || !skills) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  const slug = name.toLowerCase().replace(/\s+/g, '-');

  const template = await prisma.template.findFirst();
  if (!template) {
    return res.status(500).json({ error: 'No template found' });
  }

  const tenant = await prisma.tenant.create({
    data: {
      slug,
      name,
      bio,
      skills,
      templateId: template.id,
    },
  });

  res.json({ slug: tenant.slug });
}

export async function getTenant(req: Request, res: Response) {
  const slug = req.params.slug;

  if (!slug || typeof slug !== 'string') {
    return res.status(400).json({ error: 'Invalid slug parameter' });
  }

  const tenant = await prisma.tenant.findUnique({
    where: { slug },
  });

  if (!tenant) {
    return res.status(404).json({ error: 'Tenant not found' });
  }

  const template = await prisma.template.findUnique({
    where: { id: tenant.templateId },
  });

  res.json({ tenant, template });
}
</code></pre>
<p>Let's break down what this controller does.</p>
<p><code>createTenant</code> takes the user's name, bio, and skills from the request body. It generates a slug by lowercasing the name and replacing spaces with hyphens — so "Jane Smith" becomes <code>jane-smith</code>. It then finds the first available template and creates the tenant record in the database, linking the template to the new tenant via <code>templateId</code>.</p>
<p><code>getTenant</code> looks up a tenant by their slug and also fetches the template attached to them. Both pieces of data are returned together so the frontend can render the portfolio and apply the correct layout configuration in a single API call.</p>
<h3 id="heading-how-to-create-the-tenant-routes">How to Create the Tenant Routes</h3>
<p>Create <code>src/routes/tenant.routes.ts</code>:</p>
<pre><code class="language-typescript">import { Router } from 'express';
import { createTenant, getTenant } from '../controllers/tenant.controller';

const router = Router();

router.post('/tenants', createTenant);
router.get('/tenants/:slug', getTenant);

export default router;
</code></pre>
<p>Your API now exposes two endpoints:</p>
<pre><code class="language-plaintext">POST   /api/tenants        — creates a new tenant
GET    /api/tenants/:slug  — retrieves a tenant and their template
</code></pre>
<h3 id="heading-how-to-start-the-server">How to Start the Server</h3>
<p>Add the dev script to your <code>package.json</code>:</p>
<pre><code class="language-json">{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "seed": "tsx prisma/seed.ts"
  }
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<p>You should see <code>Server is running on port 8080</code> in your terminal. Your backend is ready.</p>
<h2 id="heading-how-to-create-the-nextjs-frontend">How to Create the Next.js Frontend</h2>
<p>Navigate to the <code>portfolio-client</code> directory and create a new Next.js project. Make sure to select <strong>Yes</strong> when the installer asks if you want to use Tailwind CSS:</p>
<pre><code class="language-bash">cd ../portfolio-client
npx create-next-app@latest . --typescript --app --tailwind --eslint
npm install
</code></pre>
<p>Since <code>create-next-app</code> sets up Tailwind for you automatically, no extra configuration is needed. The <code>tailwind.config.ts</code> and the <code>@tailwind</code> directives in <code>globals.css</code> are already in place.</p>
<h2 id="heading-how-to-add-subdomain-routing-with-middleware">How to Add Subdomain Routing with Middleware</h2>
<p>This is the heart of the multi-tenant architecture. You need a piece of code that runs before every request, reads the subdomain from the URL, and rewrites the request to the correct internal route — all without the user ever seeing the URL change.</p>
<p>Create a file called <code>proxy.ts</code> in the root directory:</p>
<pre><code class="language-typescript">import { NextRequest, NextResponse } from 'next/server';

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const host = request.headers.get('host') ?? '';

  const hostname = host.split(':')[0];
  const parts = hostname.split('.');

  // Local development: john.localhost:3000
  if (hostname.endsWith('localhost')) {
    const subdomain = parts[0];

    // Root localhost — load the landing page normally
    if (subdomain === 'localhost') {
      return NextResponse.next();
    }

    // Already rewritten — don't rewrite again
    if (pathname.startsWith('/tenant')) {
      return NextResponse.next();
    }

    return NextResponse.rewrite(new URL(`/tenant/${subdomain}`, request.url));
  }

  // Production: john.yourdomain.com
  if (parts.length &gt; 2) {
    const subdomain = parts[0];

    if (subdomain !== 'www') {
      if (pathname.startsWith('/tenant')) {
        return NextResponse.next();
      }

      return NextResponse.rewrite(new URL(`/tenant/${subdomain}`, request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)',
  ],
};
</code></pre>
<p>Here's exactly what this proxy does, step by step.</p>
<p>It reads the <code>host</code> header from every incoming request and splits it on <code>.</code> to extract the subdomain. For a request to <code>john.localhost:3000</code>, the subdomain is <code>john</code>. For a request directly to <code>localhost:3000</code>, the subdomain is <code>localhost</code> itself — in which case the proxy lets the request through unchanged so the landing page loads normally.</p>
<p>When a subdomain is detected, the proxy rewrites the request URL from <code>/</code> to <code>/tenant/john</code> internally. This rewrite is invisible to the browser — the user still sees <code>john.localhost:3000</code> in their address bar, but Next.js routes the request to your <code>/tenant/[slug]</code> page.</p>
<p>The <code>if (pathname.startsWith('/tenant'))</code> guard prevents infinite rewrite loops. Without it, the already-rewritten request would be rewritten again on the next pass through the middleware.</p>
<h2 id="heading-how-to-build-the-landing-page">How to Build the Landing Page</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6792df3bde63bedd84d043e5/5b879d6d-6307-42ce-9a6e-d1f47a789380.png" alt="The PortfolioSaaS landing page showing an empty form with fields for name, bio, and skills on a dark background" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-how-to-update-the-layout">How to Update the Layout</h3>
<p>Open <code>app/layout.tsx</code> and update it:</p>
<pre><code class="language-typescript">import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
});

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
});

export const metadata: Metadata = {
  title: 'Portfolio SaaS App',
  description: 'Create and host your portfolio with subdomains',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang="en"&gt;
      &lt;body className={`\({geistSans.variable} \){geistMono.variable} antialiased`}&gt;
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<h3 id="heading-how-to-create-the-home-page">How to Create the Home Page</h3>
<p>Create <code>app/page.tsx</code>:</p>
<pre><code class="language-typescript">'use client';

import { useState } from 'react';

export default function Home() {
  const [name, setName] = useState('');
  const [bio, setBio] = useState('');
  const [skills, setSkills] = useState('');

  const handleSubmit = async () =&gt; {
    const res = await fetch('http://localhost:8080/api/tenants', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name,
        bio,
        skills: skills.split(',').map((s) =&gt; s.trim()),
      }),
    });

    const data = await res.json();
    window.location.href = `http://${data.slug}.localhost:3000`;
  };

  return (
    &lt;div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-white flex flex-col"&gt;
      {/* Header */}
      &lt;header className="flex items-center justify-between px-10 py-5"&gt;
        &lt;span className="text-xl font-semibold tracking-wide"&gt;PortfolioSaaS&lt;/span&gt;
        &lt;nav className="flex gap-6 text-sm text-slate-400"&gt;
          &lt;a href="#features" className="hover:text-white transition-colors"&gt;Features&lt;/a&gt;
          &lt;a href="#pricing" className="hover:text-white transition-colors"&gt;Pricing&lt;/a&gt;
          &lt;a href="#docs" className="hover:text-white transition-colors"&gt;Docs&lt;/a&gt;
        &lt;/nav&gt;
      &lt;/header&gt;

      {/* Main */}
      &lt;main className="flex flex-1 items-center justify-center px-6 py-10"&gt;
        &lt;div className="w-full max-w-md bg-slate-800 rounded-2xl p-10 shadow-2xl"&gt;
          &lt;h1 className="text-3xl font-bold mb-3"&gt;Create Your Portfolio&lt;/h1&gt;
          &lt;p className="text-slate-400 text-sm mb-8"&gt;
            Launch your personal portfolio instantly with your own subdomain.
          &lt;/p&gt;

          &lt;input
            type="text"
            placeholder="Your Name"
            onChange={(e) =&gt; setName(e.target.value)}
            className="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-3 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 mb-4"
          /&gt;

          &lt;textarea
            placeholder="Short Bio"
            rows={4}
            onChange={(e) =&gt; setBio(e.target.value)}
            className="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-3 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 mb-4 resize-none"
          /&gt;

          &lt;input
            type="text"
            placeholder="Skills (comma separated)"
            onChange={(e) =&gt; setSkills(e.target.value)}
            className="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-3 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 mb-6"
          /&gt;

          &lt;button
            onClick={handleSubmit}
            className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-semibold py-3 rounded-lg transition-colors"
          &gt;
            Create Portfolio
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/main&gt;

      {/* Footer */}
      &lt;footer className="text-center text-xs text-slate-500 py-5 border-t border-slate-800"&gt;
        © {new Date().getFullYear()} PortfolioSaaS. All rights reserved.
      &lt;/footer&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>When a user submits the form, three things happen in sequence. First, a <code>POST</code> request creates a new tenant in your database. Second, the API returns the generated slug. Third, the browser redirects the user to their subdomain — <code>their-name.localhost:3000</code> — where the middleware takes over and renders their portfolio.</p>
<h2 id="heading-how-to-build-the-tenant-portfolio-page">How to Build the Tenant Portfolio Page</h2>
<p>Create <code>app/tenant/[slug]/page.tsx</code>:</p>
<pre><code class="language-typescript">import type { Metadata } from 'next';

async function getTenant(slug: string) {
  const res = await fetch(`http://localhost:8080/api/tenants/${slug}`, {
    cache: 'no-store',
  });
  return res.json();
}

export async function generateMetadata({
  params,
}: {
  params: Promise&lt;{ slug: string }&gt;;
}): Promise&lt;Metadata&gt; {
  const { slug } = await params;
  const { tenant } = await getTenant(slug);

  if (!tenant) {
    return {
      title: 'Portfolio Not Found',
      description: 'This portfolio does not exist.',
      robots: { index: false, follow: false },
    };
  }

  return {
    title: tenant.name,
    description:
      tenant.bio?.slice(0, 160) ||
      `Explore ${tenant.name}'s professional portfolio.`,
    openGraph: {
      title: tenant.name,
      description: tenant.bio,
      type: 'website',
    },
  };
}

function initials(name: string) {
  return name
    .split(' ')
    .filter(Boolean)
    .slice(0, 2)
    .map((n) =&gt; n[0]?.toUpperCase())
    .join('');
}

export default async function TenantPage({
  params,
}: {
  params: Promise&lt;{ slug: string }&gt;;
}) {
  const { slug } = await params;
  const data = await getTenant(slug);

  const tenant = data?.tenant;
  const template = data?.template;

  if (!tenant) {
    return (
      &lt;div className="min-h-screen bg-slate-900 text-white flex items-center justify-center"&gt;
        &lt;h1 className="text-2xl font-bold text-slate-400"&gt;Portfolio not found&lt;/h1&gt;
      &lt;/div&gt;
    );
  }

  const primary = template?.config?.theme?.primaryColor || '#6366f1';

  // Template-driven section toggles with safe defaults
  const sections = {
    hero: true,
    about: true,
    skills: true,
    projects: true,
    blog: true,
    contact: true,
    ...(template?.config?.sections ?? {}),
  };

  const avatarUrl = tenant.avatarUrl as string | undefined;

  return (
    &lt;div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100"&gt;

      {/* Header */}
      &lt;header className="sticky top-0 z-20 backdrop-blur-md bg-slate-900/60 border-b border-white/5"&gt;
        &lt;div className="max-w-5xl mx-auto px-5 py-4 flex items-center justify-between gap-4"&gt;
          &lt;div className="flex items-center gap-3 min-w-0"&gt;
            &lt;span
              className="w-2.5 h-2.5 rounded-full shrink-0"
              style={{ backgroundColor: primary }}
            /&gt;
            &lt;span className="font-semibold truncate"&gt;{tenant.name}&lt;/span&gt;
          &lt;/div&gt;

          &lt;nav className="hidden md:flex items-center gap-5 text-sm text-slate-400"&gt;
            {(sections.hero || sections.about) &amp;&amp; (
              &lt;a href="#about" className="hover:text-white transition-colors"&gt;About&lt;/a&gt;
            )}
            {sections.skills &amp;&amp; (
              &lt;a href="#skills" className="hover:text-white transition-colors"&gt;Skills&lt;/a&gt;
            )}
            {sections.projects &amp;&amp; (
              &lt;a href="#projects" className="hover:text-white transition-colors"&gt;Projects&lt;/a&gt;
            )}
            {sections.blog &amp;&amp; (
              &lt;a href="#blog" className="hover:text-white transition-colors"&gt;Blog&lt;/a&gt;
            )}
            {sections.contact &amp;&amp; (
              &lt;a href="#contact" className="hover:text-white transition-colors"&gt;Contact&lt;/a&gt;
            )}
          &lt;/nav&gt;

          {sections.contact &amp;&amp; (
            &lt;a
              href="#contact"
              className="text-sm font-semibold px-4 py-2 rounded-full transition-transform hover:-translate-y-px"
              style={{ backgroundColor: primary, color: '#0b1020' }}
            &gt;
              Hire me
            &lt;/a&gt;
          )}
        &lt;/div&gt;
      &lt;/header&gt;

      {/* Hero / About */}
      {(sections.hero || sections.about) &amp;&amp; (
        &lt;section className="px-5 pt-20 pb-14" id="about"&gt;
          &lt;div className="max-w-5xl mx-auto bg-white/[0.04] border border-white/[0.08] rounded-2xl p-7 shadow-2xl grid grid-cols-[110px_1fr] gap-6 items-center"&gt;

            {/* Avatar */}
            &lt;div className="w-[110px] h-[110px] rounded-full overflow-hidden border border-white/10 bg-white/5 flex items-center justify-center shrink-0"&gt;
              {avatarUrl ? (
                // eslint-disable-next-line @next/next/no-img-element
                &lt;img src={avatarUrl} alt={`${tenant.name} avatar`} className="w-full h-full object-cover" /&gt;
              ) : (
                &lt;span className="text-2xl font-extrabold text-slate-200 tracking-tight"&gt;
                  {initials(tenant.name)}
                &lt;/span&gt;
              )}
            &lt;/div&gt;

            {/* Text */}
            &lt;div className="min-w-0"&gt;
              &lt;h1
                className="text-5xl font-extrabold tracking-tight leading-tight mb-3"
                style={{ color: primary }}
              &gt;
                {tenant.name}
              &lt;/h1&gt;
              &lt;p className="text-slate-400 text-base leading-relaxed max-w-2xl"&gt;
                {tenant.bio}
              &lt;/p&gt;
              &lt;div className="flex flex-wrap gap-3 mt-5"&gt;
                {sections.contact &amp;&amp; (
                  &lt;a
                    href="#contact"
                    className="inline-flex items-center px-5 py-2.5 rounded-full text-sm font-semibold transition-transform hover:-translate-y-px"
                    style={{ backgroundColor: primary, color: '#0b1020' }}
                  &gt;
                    Let&amp;apos;s connect
                  &lt;/a&gt;
                )}
                {sections.skills &amp;&amp; (
                  &lt;a
                    href="#skills"
                    className="inline-flex items-center px-5 py-2.5 rounded-full text-sm font-semibold text-slate-200 border border-white/10 bg-white/5 hover:border-white/20 transition-all hover:-translate-y-px"
                  &gt;
                    View skills
                  &lt;/a&gt;
                )}
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/section&gt;
      )}

      {/* Skills */}
      {sections.skills &amp;&amp; (
        &lt;section className="px-5 py-14 max-w-5xl mx-auto" id="skills"&gt;
          &lt;h2 className="text-2xl font-bold text-center tracking-tight mb-7"&gt;Skills&lt;/h2&gt;
          &lt;div className="flex flex-wrap gap-3 justify-center"&gt;
            {tenant.skills.map((skill: string) =&gt; (
              &lt;span
                key={skill}
                className="px-4 py-2 rounded-full text-sm font-semibold bg-white/5 backdrop-blur-sm hover:-translate-y-0.5 hover:shadow-lg transition-all"
                style={{ border: `1px solid ${primary}` }}
              &gt;
                {skill}
              &lt;/span&gt;
            ))}
          &lt;/div&gt;
        &lt;/section&gt;
      )}

      {/* Projects */}
      {sections.projects &amp;&amp; (
        &lt;section className="px-5 py-14 max-w-5xl mx-auto" id="projects"&gt;
          &lt;h2 className="text-2xl font-bold text-center tracking-tight mb-7"&gt;Projects&lt;/h2&gt;
          &lt;div className="flex flex-wrap gap-3 justify-center"&gt;
            {['Portfolio SaaS', 'Multi-tenant Routing', 'Template Builder'].map((p) =&gt; (
              &lt;span
                key={p}
                className="px-4 py-2 rounded-full text-sm font-semibold bg-white/5 backdrop-blur-sm hover:-translate-y-0.5 hover:shadow-lg transition-all"
                style={{ border: `1px solid ${primary}` }}
              &gt;
                {p}
              &lt;/span&gt;
            ))}
          &lt;/div&gt;
        &lt;/section&gt;
      )}

      {/* Blog */}
      {sections.blog &amp;&amp; (
        &lt;section className="px-5 py-14 max-w-5xl mx-auto" id="blog"&gt;
          &lt;h2 className="text-2xl font-bold text-center tracking-tight mb-7"&gt;Blog&lt;/h2&gt;
          &lt;div className="flex flex-wrap gap-3 justify-center"&gt;
            {[
              'How I built this portfolio',
              'Next.js Middleware Tips',
              'Designing Templates',
            ].map((post) =&gt; (
              &lt;span
                key={post}
                className="px-4 py-2 rounded-full text-sm font-semibold bg-white/5 backdrop-blur-sm hover:-translate-y-0.5 hover:shadow-lg transition-all"
                style={{ border: `1px solid ${primary}` }}
              &gt;
                {post}
              &lt;/span&gt;
            ))}
          &lt;/div&gt;
        &lt;/section&gt;
      )}

      {/* Contact */}
      {sections.contact &amp;&amp; (
        &lt;section className="px-5 pt-2 pb-16" id="contact"&gt;
          &lt;div className="max-w-3xl mx-auto bg-white/[0.04] border border-white/[0.08] rounded-2xl p-7"&gt;
            &lt;h2 className="text-xl font-bold mb-2"&gt;Contact&lt;/h2&gt;
            &lt;p className="text-slate-400 leading-relaxed mb-5"&gt;
              Want to work together? Send a message and I&amp;apos;ll reply quickly.
            &lt;/p&gt;
            &lt;div className="flex flex-wrap gap-3"&gt;
              &lt;a
                href={`mailto:hello@${tenant.slug}.com`}
                className="inline-flex items-center px-5 py-2.5 rounded-full text-sm font-semibold transition-transform hover:-translate-y-px"
                style={{ backgroundColor: primary, color: '#0b1020' }}
              &gt;
                Email me
              &lt;/a&gt;
              &lt;a
                href="#about"
                className="inline-flex items-center px-5 py-2.5 rounded-full text-sm font-semibold text-slate-200 border border-white/10 bg-white/5 hover:border-white/20 transition-all hover:-translate-y-px"
              &gt;
                Back to top
              &lt;/a&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/section&gt;
      )}

      {/* Footer */}
      &lt;footer className="text-center text-xs text-slate-500 py-5 border-t border-white/5"&gt;
        © {new Date().getFullYear()} {tenant.name}
      &lt;/footer&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>There are a few important details in this page worth calling out.</p>
<p>The <code>generateMetadata</code> function runs server-side before the page renders and sets the page title, description, and Open Graph tags for each tenant individually. This means every portfolio gets its own SEO metadata — important for a real SaaS product.</p>
<p>The <code>sections</code> object merges safe defaults (<code>hero: true</code>, <code>skills: true</code>, and so on) with whatever the tenant's template specifies. This means even if the template JSON is missing a key, the page won't break — the section will simply fall back to being shown.</p>
<p>The <code>initials</code> helper generates a two-letter avatar placeholder from the tenant's name when no profile image is available. "Jane Smith" produces "JS" — a small detail that makes the portfolio look polished even before a user adds a photo.</p>
<p>Notice that the primary color from the template JSON (<code>theme.primaryColor</code>) is applied using the <code>style</code> prop rather than a Tailwind class. This is intentional. Tailwind generates class names at build time and cannot know the dynamic color value stored in your database. Inline styles are the correct approach whenever a CSS value is truly dynamic.</p>
<h2 id="heading-how-to-test-the-full-flow">How to Test the Full Flow</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6792df3bde63bedd84d043e5/ed282eea-60a0-456c-a6b5-ba4463bae338.png" alt="The PortfolioSaaS form filled in with Alex Morgan's name, bio, and a comma-separated list of skills ready to submit" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Start both servers in separate terminal windows:</p>
<pre><code class="language-bash"># Terminal 1 — Backend
cd portfolio-api
npm run dev

# Terminal 2 — Frontend
cd portfolio-client
npm run dev
</code></pre>
<p>Now test the complete flow:</p>
<ol>
<li><p>Visit <code>http://localhost:3000</code> and fill out the form with your name, a short bio, and a comma-separated list of skills.</p>
</li>
<li><p>Click <strong>Create Portfolio</strong>. The form submits to your Express API, which creates the tenant record and returns the slug.</p>
</li>
<li><p>Your browser redirects to <code>http://your-name.localhost:3000</code>.</p>
</li>
<li><p>The Next.js middleware detects the subdomain, rewrites the request to <code>/tenant/your-name</code>, and your portfolio page fetches and renders your data.</p>
</li>
</ol>
<p>You should see a fully rendered portfolio page with your name, bio, skills, and the placeholder projects and blog sections — all styled with Tailwind utility classes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6792df3bde63bedd84d043e5/6bb311aa-e55b-4444-9b5e-2b778daccc5e.png" alt="Alex Morgan's generated portfolio page showing the hero section with initials avatar, bio, skills badges, and projects section" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-next-steps">Next Steps</h2>
<p>You now have a working multi-tenant SaaS foundation. Here are some extensions worth considering for a production build:</p>
<p>You could add authentication with NextAuth.js so tenants can log in and update their portfolio without losing their data between sessions.</p>
<p>You could also add custom domain support so tenants can point their own domain (for example, <code>janedoe.com</code>) to their portfolio by adding a CNAME record. You would need to handle wildcard SSL certificates on your hosting provider.</p>
<p>You could add image uploads for avatars using Cloudinary or AWS S3, then store the URL in the tenant record and replace the initials fallback with a real photo.</p>
<p>You could add real blog post management using the <code>Post</code> model already defined in your schema. Tenants could write and publish posts that appear on their portfolio.</p>
<p>And you could add Stripe subscriptions so tenants pay a monthly fee to keep their portfolio live. The architecture from the Stripe Connect tutorial maps directly onto this.</p>
<p>Finally, you could deploy the backend to Railway or Render and the frontend to Vercel. Just make sure to update your API URLs from <code>localhost:8080</code> to your production URL before deploying.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a complete multi-tenant SaaS platform where users can sign up, get their own subdomain, and have a portfolio site generated instantly — all from a single codebase.</p>
<p>You learned how to use Next.js middleware to detect subdomains and rewrite requests dynamically, model multi-tenant data in Prisma with a slug-based routing system, build a JSON-driven template system that controls page layout without code changes, and style a production-ready Next.js frontend entirely with Tailwind CSS utility classes.</p>
<p>The core insight is that multi-tenancy isn't magic. It's subdomain detection plus dynamic routing plus isolated data. Once you understand those three moving parts, you can apply this pattern to any SaaS product you build.</p>
<p>If you found this tutorial helpful, share it with someone who is learning to build full-stack applications. Happy coding!</p>
<h2 id="heading-source-code">Source Code</h2>
<p>You can find the complete source code for both parts of this project on GitHub:</p>
<ul>
<li><strong>Frontend (Next.js multi-tenant app) + Backend (Express + Prisma API):</strong> <a href="https://github.com/michaelokolo/portfolio-saas-v1">https://github.com/michaelokolo/portfolio-saas-v1</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Get Your First SaaS Customers ]]>
                </title>
                <description>
                    <![CDATA[ Starting a SaaS (Software as a Service) business is exciting. You’ve put in the long hours building something you believe people will love. But now comes the big question: How do you get your first customers? Getting those first few users can feel li... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-get-your-first-saas-customers/</link>
                <guid isPermaLink="false">681242328ca367c7e5d58873</guid>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ marketing ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Udemezue John ]]>
                </dc:creator>
                <pubDate>Wed, 30 Apr 2025 15:30:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746027034713/42b1e6bd-3f5f-4e41-b3d0-ead8168661a6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Starting a SaaS (Software as a Service) business is exciting. You’ve put in the long hours building something you believe people will love. But now comes the big question: <strong>How do you get your first customers?</strong></p>
<p>Getting those first few users can feel like climbing a mountain. It’s the hardest part, but also the most important. Without customers, your SaaS doesn’t grow.</p>
<p>Without feedback from real users, you can’t improve. And let’s be honest – seeing people pay for your product is a huge motivation boost.</p>
<p>I’ve seen this challenge many times, and I know the pressure is real. That’s why I’m breaking it all down in the simplest way possible. No confusing words, no vague advice – just a clear plan you can use.</p>
<p>Let’s jump right in.</p>
<h2 id="heading-why-getting-your-first-saas-customers-matters-so-much">Why Getting Your First SaaS Customers Matters So Much</h2>
<p>Before we talk strategy, let’s be super clear about why the first customers are gold:</p>
<ul>
<li><p><strong>They validate your idea.</strong> If strangers are willing to pay, you’re onto something real.</p>
</li>
<li><p><strong>They help you shape the product.</strong> Early users are honest about what works and what’s broken.</p>
</li>
<li><p><strong>They fuel your momentum.</strong> A few early wins can give you the confidence to keep pushing forward.</p>
</li>
<li><p><strong>They help you spread the word.</strong> Happy first customers are often your best marketers.</p>
</li>
</ul>
<p>Your first ten customers matter more than your next hundred. So it’s worth taking the time to get this right.</p>
<h2 id="heading-real-ways-to-get-your-first-saas-customers">Real Ways to Get Your First SaaS Customers</h2>
<h3 id="heading-1-start-with-people-you-know">1. Start With People You Know</h3>
<p>Don’t be shy to reach out to friends, family, former coworkers, and old clients. Tell them about your new SaaS and ask if they know anyone who might need it.</p>
<p>And remember: you’re not begging – you’re offering something valuable.</p>
<p><strong>Pro Tip:</strong> Focus on <em>the people they know,</em> not just whether they want to buy themselves.</p>
<h3 id="heading-2-join-communities-where-your-customers-hang-out">2. Join Communities Where Your Customers Hang Out</h3>
<p>Find Facebook groups, Reddit forums, Slack communities, or LinkedIn groups where your ideal users already spend time.</p>
<p>Be helpful. Answer questions. Join conversations. Build trust. Then, when it’s natural, mention your product.</p>
<p><strong>Real Example:</strong> If you built a tool for yoga instructors, find Facebook groups for yoga teachers.</p>
<h3 id="heading-3-launch-on-product-hunt">3. Launch on Product Hunt</h3>
<p>Product Hunt is full of early adopters looking for new tools.</p>
<p>If you do a good launch (with a nice-looking page, clear description, and a bit of buzz on the side), you could get your first few hundred signups in one day.</p>
<p><strong>Helpful read:</strong> <a target="_blank" href="https://www.producthunt.com/blog/how-to-launch-on-product-hunt">How to Launch on Product Hunt Successfully</a></p>
<h3 id="heading-4-offer-a-beta-version">4. Offer a “Beta” Version</h3>
<p>Invite early users to test your software before the official launch.<br>Make it feel special – they get early access, and their feedback will shape the product.</p>
<p><strong>Bonus Tip:</strong> Beta users are often more forgiving about bugs. They <em>expect</em> a few rough edges.</p>
<h3 id="heading-5-use-cold-outreach-but-do-it-right">5. Use Cold Outreach (But Do It Right)</h3>
<p>Sending emails to strangers sounds scary. But if you write short, respectful, personalized emails, it works.<br>Focus on how you can solve a real problem they have – not on how great your product is.</p>
<p><strong>Good Cold Email Example:</strong></p>
<blockquote>
<p>"Hi [Name],<br>I noticed you [specific detail]. I built a tool that might save you [X hours / $X per month].<br>Would you like me to send a quick demo?"</p>
</blockquote>
<p>Keep it about them, not you.</p>
<h3 id="heading-6-get-active-on-linkedin">6. Get Active on LinkedIn</h3>
<p>Post helpful tips, mini case studies, and personal stories. Show people you understand their struggles.<br>It builds trust. It shows you’re serious. And it keeps you top-of-mind when someone <em>does</em> need your product.</p>
<h3 id="heading-7-partner-with-other-small-businesses">7. Partner With Other Small Businesses</h3>
<p>Find companies that serve the same audience but aren’t direct competitors.</p>
<p>Maybe you made a booking app for barbershops. You could partner with a payment processor or marketing agency that also targets barbers.</p>
<p>Offer a referral deal or co-host a webinar together.</p>
<h3 id="heading-8-content-marketing-start-small">8. Content Marketing (Start Small)</h3>
<p>You don’t need a full blog with 50 articles right away. Start with 2–3 simple, super-useful blog posts that answer real questions your customers have.</p>
<p>Example:</p>
<p>If you built an invoicing tool, write posts like:</p>
<ul>
<li><p>“How to Send Your First Invoice as a Freelancer”</p>
</li>
<li><p>“5 Mistakes New Freelancers Make With Payments”</p>
</li>
</ul>
<p>Helpful Link: <a target="_blank" href="https://moz.com/beginners-guide-to-content-marketing">Beginner’s Guide to Content Marketing</a></p>
<h3 id="heading-9-run-a-tiny-paid-ad-test">9. Run a Tiny Paid Ad Test</h3>
<p>A small Facebook or Google ad campaign (even $5–10 a day) can give you quick feedback.</p>
<p>Target very specifically – not just "small business owners," but "independent yoga teachers in New York."</p>
<p>Watch which messages work, which don’t, and adjust fast.</p>
<h3 id="heading-10-create-simple-case-studies">10. Create Simple Case Studies</h3>
<p>Even if you only have <em>one</em> early customer, ask them if you can tell their story. How they used your product. What problem it solved. What results they saw.</p>
<p>Nothing builds trust faster than real success stories.</p>
<h3 id="heading-11-offer-a-founders-deal">11. Offer a Founder's Deal</h3>
<p>Offer lifetime discounts or special plans to your first customers. This rewards them for taking a chance on you early, and it creates urgency (“only 20 spots left!”).</p>
<p>Example: Lifetime access for $99 instead of $20/month.</p>
<h3 id="heading-12-make-it-ridiculously-easy-to-share">12. Make It Ridiculously Easy to Share</h3>
<p>Add a simple “Share with a friend” button in your app, emails, and thank-you pages. Happy users want to help – just make it easy for them.</p>
<h2 id="heading-faqs">FAQs</h2>
<h3 id="heading-should-i-offer-my-saas-for-free-at-first"><strong>Should I offer my SaaS for free at first?</strong></h3>
<p>I would recommend a free <em>trial</em> instead of free <em>forever.</em> Free users are often less serious. You want customers who see value and are willing to pay for it.</p>
<h3 id="heading-how-long-does-it-usually-take"><strong>How long does it usually take?</strong></h3>
<p>Honestly? It depends. Some people land their first customers in a week. Others need a few months. Stay patient and keep improving both your product and your outreach.</p>
<h3 id="heading-what-if-no-one-seems-interested"><strong>What if no one seems interested?</strong></h3>
<p>It happens. If you're hearing crickets, it might be time to talk directly to potential customers, ask more questions, and tweak your offer. Sometimes it’s just a positioning problem.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Getting your first SaaS customers isn’t about being perfect. It’s about getting real people to trust you enough to give your product a shot. Start small. Talk to people. Solve real problems.</p>
<p>And keep improving a little every day.</p>
<p>If you need a hand with SaaS marketing or growing your idea, I’m always happy to connect. You can find me on X at <a target="_blank" href="https://x.com/_udemezue">x.com/_udemezue</a>, or check out my portfolio: <a target="_blank" href="https://udemezue.pages.dev">Udemezue.pages.dev</a>.</p>
<h3 id="heading-further-resources">Further Resources</h3>
<p>If you want to dive deeper, check out:</p>
<ul>
<li><p><a target="_blank" href="https://momtestbook.com/">The Mom Test (book)</a> — How to talk to customers and get honest feedback.</p>
</li>
<li><p><a target="_blank" href="https://www.indiehackers.com/">Indie Hackers</a> — A community where founders share how they got their first users.</p>
</li>
<li><p><a target="_blank" href="https://www.demandcurve.com/">First 1000 Users Guide (from Demand Curve)</a> — Very detailed but still easy to follow.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Granular Segmentation with Feature Flags ]]>
                </title>
                <description>
                    <![CDATA[ These days, SaaS has become an integral part of running many businesses. So rolling out new features that resonate with the user base is key to a business’s growth. Imagine a feature that promises to enhance user experience but that ends up resonatin... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-granular-segmentation-with-feature-flags/</link>
                <guid isPermaLink="false">6793a744fc2bd29e0ece1253</guid>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ user experience ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kayode Adeniyi ]]>
                </dc:creator>
                <pubDate>Fri, 24 Jan 2025 14:44:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737681693640/2cd6aa99-94bf-48c6-b657-4cc0743312e3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>These days, SaaS has become an integral part of running many businesses. So rolling out new features that resonate with the user base is key to a business’s growth.</p>
<p>Imagine a feature that promises to enhance user experience but that ends up resonating with only a small subset of users. This scenario underscores the importance of precision in feature rollouts.</p>
<p>Fortunately, <a target="_blank" href="https://www.flagsmith.com/">feature flagging management tools</a> like Flagsmith can help with granular segmentation. This process helps your team make sure that new features are introduced to the most relevant audiences. Granular Segmentation makes it easier to understand your user base, leading to higher engagement and satisfaction.  </p>
<p>In this article, we will be focusing on the concept of granular user segmentation and its significance in enhancing feature rollouts. We’ll also explore some best practices, pitfalls to avoid, and will look at how Flagsmith facilitates granular segmentation with feature flags.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-is-flagsmith">What is Flagsmith?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-granular-segmentation">What is Granular Segmentation?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-feature-flags-enable-granular-segmentation">How Feature Flags Enable Granular Segmentation?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-granular-segmentation-in-flagsmith">How to Implement Granular Segmentation in Flagsmith</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-benefits-of-granular-segmentation-for-user-engagement">Benefits of Granular Segmentation for User Engagement</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-is-flagsmith">What is Flagsmith?</h2>
<p>Flagsmith is an open-source feature management platform that helps teams control feature rollouts with precision. It helps developers toggle features on or off for specific users, environments, or groups without redeploying code.</p>
<p>Ideal for A/B testing, phased rollouts, and remote configuration, Flagsmith ensures real-time adjustments and seamless feature delivery. With flexible deployment options – hosted, private cloud, or on-premises – it adapts to the needs of organizations of all sizes.</p>
<h2 id="heading-the-importance-of-granular-segmentation"><strong>The Importance of Granular Segmentation</strong></h2>
<h3 id="heading-what-is-granular-segmentation"><strong>What is Granular Segmentation?</strong></h3>
<p>Granular segmentation is a process in which the user base is divided into groups based on unique attributes such as behavior demographics or engagement levels. These groups can be identified as segments of users of a platform, and each segment is based on several traits that help teams tailor feature rollouts to meet the needs of each segment.  </p>
<p>This granular level of control over rollouts helps product teams release features that resonate with their user base. This creates a more personalized experience for the end user that can improve the effectiveness of the feature. </p>
<p>Now, let’s discuss what kind of an impact feature rollouts might have. </p>
<h3 id="heading-impact-of-feature-rollouts"><strong>Impact of Feature Rollouts</strong></h3>
<p>The advantages of granular segmentation in feature rollouts include:</p>
<ul>
<li><p><strong>Targeted relevance:</strong> Features are delivered to users who will benefit most from them, making the updates more relevant and useful. This targeted approach increases the likelihood of user engagement.</p>
</li>
<li><p><strong>Optimized user experience:</strong> Because of this targeted approach, businesses can prevent rollouts of features that overwhelm their users in any way. This means that users would receive updates according to their interests, leading to a better user experience. </p>
</li>
<li><p><strong>Higher adoption rates:</strong> All this would also lead to higher adoption rates. An increased adoption rate is a sign of good engagement from a business’s users as well as of business growth</p>
</li>
<li><p><strong>Less risk when</strong> <strong>rolling out new features</strong>: Segmenting your user base and releasing new features to, say, a select 10% of users reduces risk. Teams can see how features do with those users and adjust accordingly before rolling out to the next segment. Or they can roll back if the impact is negative, which helps them avoid incidents like the latest high-publicity one we saw with CrowdStrike.</p>
</li>
</ul>
<p>To put things into perspective, let’s discuss an example of an e-commerce store. </p>
<h4 id="heading-the-mart-example"><strong>The MART Example</strong></h4>
<p>The MART is an online store that sells various products. They want to introduce an AI-powered recommendation engine, but only to a subset of their user base that shows less engagement on the platform buying products. AI-powered recommendation engines would target this user segment to generate more sales from the platform and increase business growth.</p>
<p>Here we see the concept of segmentation in practice where a feature is dedicatedly exposed to a user group's explicit attributes, thus leading to increased relevance and user satisfaction.</p>
<p>If the feature proves to be successful with the targeted segment, the next phase would be to expand its availability to other user groups.</p>
<h2 id="heading-how-feature-flags-enable-granular-segmentation"><strong>How Feature Flags Enable Granular Segmentation</strong></h2>
<p>You can integrate Flagsmith into your development workflow by using <a target="_blank" href="https://www.flagsmith.com/sdks">SDKs</a>. The user segmentation adds a layer of granular control to the product teams with over-feature releases. This control helps product teams to minimize the risk of degradation of a new feature. They can leverage the GUI to interact with Flagsmith and roll out/roll back features according to their needs.</p>
<h3 id="heading-what-are-segments-in-feature-rollouts"><strong>What are Segments in Feature Rollouts?</strong></h3>
<p>A segment is a subset of identities, defined by a set of rules based on traits associated with identities. So a single identity can be a part of many segments and is associated with an environment, such as staging or production.</p>
<p>You might be wondering – how can product teams use segments in their feature rollouts?</p>
<p>You can use segments to create ‘overrides’ on any number of features in your application. This allows you to control the state and/or value of a feature for a selection of your users, as defined by the segment.</p>
<p>Now that you understand segments, let’s discuss what key features allow you to use detailed user segmentation.</p>
<ul>
<li><p><strong>User attributes:</strong> Flagsmith allows you to define and manage user attributes, such as location, behavior, subscription levels, or platform activity. These are attributes you can use to create highly specific user segments.</p>
</li>
<li><p><strong>Segment definitions:</strong> You can create custom segment definitions from these user attributes. For instance, you can define a segment for users who have been highly active on the platform since last month or users who live in a different region than most of your user base. This granularity ensures that you can target features to the most relevant user groups.</p>
</li>
<li><p><strong>Dynamic targeting:</strong> Dynamic targeting can help you adjust feature rollouts on the basis of user attributes. This means that you can progressively roll out features to segments of users, monitor their performance, and make adjustments to the feature accordingly.</p>
</li>
</ul>
<h3 id="heading-flexibility-and-control"><strong>Flexibility and Control</strong></h3>
<p>Flexibility and control are a rare combination when it comes to such tools, but with Flagsmith you get the best of both worlds. User segments and feature management ensure you have precision and control over your feature rollouts:</p>
<ul>
<li><p><strong>Granular control:</strong> Multiple segment creation and control access are available in Flagsmith with a variety of criteria, allowing feature rollouts that cater to specific user needs.</p>
</li>
<li><p><strong>Analytics and feedback:</strong> Analytics and feedback are an integral part of the feature testing loop. They provide tracking of how different segments interact with new features. It’s invaluable for understanding the user’s behavior on the platform which helps you make informed decisions for further rollouts.</p>
</li>
</ul>
<p>So now you’ve learned what segments are, what you can do with them, and how segments help in granular control over rollouts. Now, let’s move on and see how you can implement segmentation using Flagsmith.</p>
<h2 id="heading-how-to-implement-granular-segmentation-in-flagsmith"><strong>How to Implement Granular Segmentation in Flagsmith</strong></h2>
<h3 id="heading-set-up-flagsmith-in-your-project"><strong>Set Up Flagsmith in Your Project</strong></h3>
<p>You can integrate Flagsmith into your application using the available SDKs for the language of your choice. For example, to integrate the SDK in Node.js, you’ll first need to install the npm package as follows:</p>
<pre><code class="lang-javascript">npm i flagsmith-nodejs --save
</code></pre>
<p>After installing the package, you will use the following code to initialize Flagsmith in your project:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> Flagsmith = <span class="hljs-built_in">require</span>(<span class="hljs-string">'flagsmith-nodejs'</span>);
<span class="hljs-keyword">const</span> flagsmith = <span class="hljs-keyword">new</span> Flagsmith({ <span class="hljs-attr">environmentKey</span>: <span class="hljs-string">'FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY'</span>,});
</code></pre>
<p>Once it’s integrated, configure your Flagsmith instance by creating a new project. We’ll go through this below.</p>
<h3 id="heading-how-to-create-identities-and-define-user-traits-and-segments"><strong>How to Create Identities and Define User Traits and Segments</strong></h3>
<p>Now you’ll need to create the identities and traits you want to use for segmentation. These could include user profile information, behavior metrics, or any other relevant data.</p>
<p>So, let’s create a user named John Doe.</p>
<ul>
<li><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcorqiG6dhrwu_3IARs_A3Rgn39I_g_9_cGNEyawmu6SWwqOFCXm_vXm8VGbgDHSzo4LMnnlSQ7DgvE_1_EH_MLBta2_eGhlMSPfabjGR7YwFvTCq3lnBWdoQDdu16x5elbFWp6zGHgmBbpiqdD9PnK4Hgb?key=CLsy_98J-hXFutqrVNKvTw" alt="create new ID on Flagsmith" width="600" height="400" loading="lazy"></li>
</ul>
<p>Now click on the created user and define a trait country.</p>
<p>In Flagsmith, creating a <em>trait country</em> involves defining a user attribute that specifies their geographic location. Traits are key-value pairs assigned to user identities, allowing for precise segmentation. For example, you can define the "country" trait with values like "USA," "Canada," or "Germany."</p>
<p>This enables product teams to create segments based on location and target feature rollouts accordingly. For instance, a feature can be activated only for users with the "country" trait set to "USA," facilitating controlled and region-specific rollouts.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfsPeipIa35FrYuK7UuEz4g_5wrnwVvlk1YiJs1nNNmWiwszZcSVmb7zfD8CpN81Vh6rxNasuZHk5ze6nFPmkIF4JxFDWmb1gU68hd0CoDbuN5pjOMAZyJnZTCQWwxJPigYeooK7AlC0Mwjte74S9F_PbY?key=CLsy_98J-hXFutqrVNKvTw" alt="Defining trait and country on flagsmith" width="600" height="400" loading="lazy"></p>
<p>Next, you’ll create some segments. You’ll use the Flagsmith dashboard to create custom segments based on these attributes. For example, you can create a segment for users who are from the USA. Define your segment, for example (western_users ), as below:</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdPPGFxPEnBJXEIXNkDGyuC3IJQfE2G4wEtsSWtinIm3Yg_evRmo_ly1_ZPwCqwuWojv7XYI2DP_MMXBQqQy80FFIrccL-KXdmsS9cTrz5T5f9485vDcfiZlH-wkKTZBrk9-Lt9hvKZJgA-3ugQbeoiSfRS?key=CLsy_98J-hXFutqrVNKvTw" alt="Define segments on flagmith" width="600" height="400" loading="lazy"></p>
<h3 id="heading-how-to-create-and-manage-feature-flags"><strong>How to Create and Manage Feature Flags</strong></h3>
<p>Create a feature flag called ai_recommendation_engine in Flagsmith for the features you want to roll out. Each flag represents a specific feature or configuration option that can be toggled on or off.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXciWtuMzy24Sl_n-i8_lGigMUfbCbV5KdmlAqEotHQiVp7CIw7myLIsVTqltTmZp1STUkAdwNPhGB11PI5tvdHB9dp84x3mjI9rR6ycu7Z-nHYFPUddjBu2adQceVkW8YLvUj6s_tOVpNdA78z3-tL6X06U?key=CLsy_98J-hXFutqrVNKvTw" alt="Specific feature or configuration option on Flagsmith" width="600" height="400" loading="lazy"></p>
<p>Next, assign your feature flags to the segment you created. For instance, if you have a recommendation engine, you can target it specifically to users that match the segment created in the previous step. Use the Flagsmith dashboard to set these targeting rules and manage feature flag settings.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe4XKYMDUOMCrECTgvp9wK2I2j_HIvDBeDEr1EG0Nf3OdfxducIE-xiDn6GSPRi84veq2K2r0OnvPaCgyuO7xkRVWlpYLjXuJC5F7PS0rP-xzbUL52MO1fHl_E08wAXLsxI8JSLkZP4Q4_NMvrgPRl2OVUw?key=CLsy_98J-hXFutqrVNKvTw" alt="Setting up targetting rules on Flagsmith" width="600" height="400" loading="lazy"></p>
<h3 id="heading-how-to-target-segments-for-rollouts"><strong>How to Target Segments for Rollouts</strong></h3>
<p>After configuring Flagsmith and setting up your segments and traits, you can start rolling out features to your defined segments.</p>
<p>First, you’ll want to do gradual rollouts. Using the percentage split operator, you can initially release the feature to a small percentage of users within the segment. Based on performance and feedback, you can gradually expand the rollout to a larger portion of the segment or additional segments, ensuring a controlled and data-driven approach.</p>
<p>Second, monitoring is a crucial part of feature rollouts and Flagsmith can help you with its analytics tools. You can track the performance of your feature flags and user segments, monitor how different segments interact with the new features, and make adjustments as needed.</p>
<p>For example, you might decide to increase the rollout percentage or adjust segment definitions based on user feedback.</p>
<h4 id="heading-some-best-practices">Some best practices:</h4>
<ul>
<li><p><strong>Start small:</strong> To test out segmentation, it’s a good idea to start small and create well-defined segments to test new features. This will help you gather valuable feedback and will prevent you from being overwhelmed in case of degraded performance or a rollback scenario.</p>
</li>
<li><p><strong>Use data:</strong> Analytical tools are a great help in gathering data on how different segments interact with your features. You can use this data to refine your targeting and improve the user experience.</p>
</li>
<li><p><strong>Iterate:</strong> You’ll likely make better decisions after several iterations. So remember that you should iterate your segmentation and rollouts based on metrics and user feedback.</p>
</li>
</ul>
<h4 id="heading-some-common-pitfalls">Some common pitfalls:</h4>
<ul>
<li><p><strong>Overlapping segments:</strong> Distinction between segmentations is the key to avoiding conflicts between feature targeting. Always be careful while defining segments for your user groups.</p>
</li>
<li><p><strong>Ignoring feedback:</strong> The greatest mistake a product team can make is to overlook early user feedback. Early feedback is crucial for identifying issues and making informed decisions about a feature rollout.</p>
</li>
</ul>
<p>By following these steps and best practices, you can effectively use this granular segmentation approach, ensuring that your feature rollouts are targeted, relevant, and successful.</p>
<h2 id="heading-benefits-of-granular-segmentation-for-user-engagement"><strong>Benefits of Granular Segmentation for User Engagement</strong></h2>
<h3 id="heading-improved-user-satisfaction"><strong>Improved User Satisfaction</strong></h3>
<p>Granular segmentation helps your users out, as it gives them specifically personalized features according to their needs and inclinations. You can build more personalized experiences by aiming certain features at particular users that match their behavior or interest.</p>
<p>For example, a fitness app might launch an update that contains a workout feature for those users who have shown interest in strength building, rather than for all users. This targeted approach ensures that users receive updates related to and suitable to them, which leads to a positive experience, increased satisfaction, and better recognition of your product.</p>
<h3 id="heading-increased-engagement"><strong>Increased Engagement</strong></h3>
<p>When users get features or updates that are targeted toward their specific needs, it’s more likely that they’ll engage with that feature. Granular segmentation helps maximize engagement by providing users with upgrades that are pertinent to their interests and usage patterns.</p>
<p>For example, an e-commerce platform could propose a new recommendation system and try it out on users who recurrently browse specific categories. This relevant targeting will likely increase the probability that those users will respond to those recommendations, leading to increased engagement and potentially higher conversions.</p>
<h3 id="heading-enhanced-feature-adoption"><strong>Enhanced Feature Adoption</strong></h3>
<p>Targeting specific segments of users with features that address their needs should lead to higher adoption rates. Presenting new features to users who are very likely to benefit from them, you increase the probability of these features being adopted and utilized.</p>
<p>For example, a software company introducing a new improved analytics tool would likely target power users who consistently use analytics features. After those users provide positive feedback and adopt the tool, it can be deployed on other segments. Then the team can be confident that the feature is approved and effective.</p>
<h3 id="heading-data-driven-insights"><strong>Data-Driven Insights</strong></h3>
<p>Granular segmentation offers valuable insights into how various user groups engage with new features. Analyzing this data can provide you insights into the behavior and inclinations your users as well as the overall impact of your features.</p>
<p>For example, you might realize that users are responsive to new features in a specific segment compared to other segments. Such information helps you refine your feature strategy, making rational decisions regarding future launches, and enhancing user engagement across different segments.</p>
<h3 id="heading-optimized-resource-allocation"><strong>Optimized Resource Allocation</strong></h3>
<p>Centering on targeted segments lets you allocate resources more effectively. instead of investing in a broad, one-size-fits-all approach, you can direct your initiative towards segments that are likely to benefit from and engage with new features. This optimized allocation assures that your resources are utilized efficiently, leading to positive outcomes and a higher return on investment.</p>
<p>By leveraging granular segmentation, you can enhance user engagement, improve feature adoption, and gain valuable insights, all of which contribute to a more successful and user-centric feature rollout strategy.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this article, we discussed the power of granular user segmentation in driving successful feature rollouts, highlighting how it can improve user satisfaction, engagement, and adoption rates. We also explored how Flagsmith enables this approach, offering tools to manage and target features with precision.</p>
<p>By leveraging these strategies, you can ensure that your product updates are more relevant and impactful. If you're interested in optimizing your feature rollouts, consider exploring Flagsmith’s capabilities to start making data-driven decisions.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Choose a Tech Stack for Your SaaS Product – Lessons from a Developer ]]>
                </title>
                <description>
                    <![CDATA[ As a developer, I've seen how choosing the "right" tech stack can be a double-edged sword. I often fell into the trap of chasing shiny new technologies, thinking they were the key to building the next great product. But experience has taught me that ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/choose-a-tech-stack-for-your-saas-product/</link>
                <guid isPermaLink="false">66d039d6daf2a38a6b1e1cfd</guid>
                
                    <category>
                        <![CDATA[ career advice ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Juan Cruz Martinez ]]>
                </dc:creator>
                <pubDate>Fri, 24 May 2024 08:52:18 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/05/Live-Stream-Post.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a developer, I've seen how choosing the "right" tech stack can be a double-edged sword. I often fell into the trap of chasing shiny new technologies, thinking they were the key to building the next great product. But experience has taught me that prioritizing speed to market trends often trumps the pursuit of technological ideals.</p>
<p>In the early stages of a SaaS product, it's easy to get caught up in the excitement of designing elaborate architectures, experimenting with bleeding-edge frameworks, and optimizing every line of code. While these things are important, they can also become significant roadblocks if taken too far.</p>
<p>I've seen projects stall for months as developers (myself) debated the merits of different databases or attempted to master a complex framework before writing a single line of product code. This kind of overengineering can drain resources, delay launches, and ultimately put the entire project at risk.</p>
<p>One of the most valuable lessons I've learned is the power of leveraging familiar technologies. When you and your team know a toolset inside and out, you can build features faster, troubleshoot more efficiently, and deliver a more stable product.</p>
<p>This doesn't mean you should never learn new things. But in the early stages of a product, where speed is crucial, it's often more beneficial to focus on building something that works, rather than something that's technically impressive but takes forever to complete.</p>
<p>As your product matures and gains traction, there will be opportunities to experiment with new technologies and optimize your tech stack. But in the beginning, the most important thing is to get your product in front of users and start gathering feedback.</p>
<h2 id="heading-what-tech-stack-should-i-use">What Tech Stack Should I Use?</h2>
<p>There's no one-size-fits-all answer when it comes to the perfect tech stack. The best choice for you will depend on several factors:</p>
<ul>
<li><strong>Your Use Case:</strong> What kind of SaaS product are you building? Different types of applications may benefit from different technologies. For example, a real-time collaboration tool might prioritize WebSocket and reactive frameworks, while a data-heavy analytics platform might favor a robust database and powerful server-side processing.</li>
<li><strong>Your Team's Expertise:</strong> Don't underestimate the value of familiarity. If your team is already proficient in a particular language or framework, leverage that expertise. It'll save you valuable time and reduce the risk of running into unexpected issues.</li>
<li><strong>Scalability and Performance Requirements:</strong> Do you anticipate rapid growth? If so, choose a tech stack that can scale with your user base and traffic. Consider cloud-based solutions and technologies that are known for their performance and reliability.</li>
</ul>
<h2 id="heading-general-recommendations">General Recommendations</h2>
<p>While there's no magic formula, here are some general recommendations for SaaS tech stacks (for web apps) that have proven successful:</p>
<h3 id="heading-front-end">Front-End</h3>
<ul>
<li><strong>React/NextJS:</strong> A popular JavaScript library for building user interfaces. It's known for its flexibility, component-based architecture, and large community.</li>
<li><strong>Vue.js:</strong> Another popular JavaScript framework that's easy to learn and integrate into existing projects.</li>
<li><strong>Angular:</strong> A full-featured framework developed by Google, offering a structured approach to building complex applications.</li>
</ul>
<h3 id="heading-back-end">Back-End</h3>
<ul>
<li><strong>Node.js:</strong> A JavaScript runtime environment that allows you to use JavaScript for server-side development. It's known for its speed, scalability, and large ecosystem of libraries and frameworks.</li>
<li><strong>Python (with Django or FastAPI):</strong> A versatile language that's great for rapid development and data-intensive applications. Django and Flask are popular frameworks that provide structure and simplify common tasks.</li>
<li><strong>Ruby (with Rails):</strong> Known for its convention-over-configuration approach and developer-friendly tools, Rails can help you build web applications quickly and efficiently.</li>
</ul>
<h3 id="heading-database">Database</h3>
<ul>
<li><strong>PostgreSQL:</strong> A powerful and reliable open-source relational database that offers strong support for complex queries, data integrity, and scalability.</li>
<li><strong>MongoDB/DynamoDB:</strong> A NoSQL database that's flexible and scalable, making it a good choice for applications with evolving data models or unstructured data.</li>
</ul>
<h3 id="heading-additional-considerations">Additional Considerations</h3>
<ul>
<li><strong>Authentication:</strong> Authentication is one of those systems you don’t want to build, so use third party services like <a target="_blank" href="https://auth0.com/">Auth0</a> to get you started quickly and that would scale as you grow.</li>
<li><strong>Caching:</strong> Consider using a caching layer like <a target="_blank" href="https://redis.io/">Redis</a> to improve performance and reduce database load.</li>
<li><strong>Queueing:</strong> For background tasks and asynchronous processing, message queues like <a target="_blank" href="https://www.rabbitmq.com/">RabbitMQ</a>, <a target="_blank" href="https://kafka.apache.org/">Kafka</a>, or <a target="_blank" href="https://aws.amazon.com/sqs/">Amazon SQS</a> can be valuable.</li>
<li><strong>Monitoring and Logging:</strong> Implement tools like <a target="_blank" href="https://www.datadoghq.com/">Datadog</a>, <a target="_blank" href="https://sentry.io/welcome/">Sentry</a> to monitor your application's performance and track errors.</li>
</ul>
<h3 id="heading-why-this-stack">Why this Stack?</h3>
<ul>
<li><strong>Speed to Market:</strong> This stack combines familiar technologies (JavaScript, Python) with modern frameworks (React, Django/Flask, Express/NestJS) that facilitate rapid development.</li>
<li><strong>Scalability:</strong> AWS, Azure, and GCP offer auto-scaling and other features that allow your application to grow with your user base. PostgreSQL and MongoDB are known for their scalability.</li>
<li><strong>Flexibility:</strong> This stack supports both relational and NoSQL databases, giving you flexibility to choose the right data model for your application. The three major cloud providers offer a variety of services to meet your evolving needs.</li>
<li><strong>Community and Support:</strong> All of these technologies have large, active communities and extensive documentation, making it easier to find help and resources when you need them.</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Choosing the right tech stack for your SaaS product is a critical decision, but it's important to remember that technology is just one ingredient in the recipe for success. A well-validated idea, a strong team, and a relentless focus on delivering value to customers are all equally important.</p>
<p>Throughout my journey as a developer, I've learned a few key lessons:</p>
<ul>
<li><strong>Prioritize Speed to Market:</strong> Don't let the pursuit of technological perfection delay your launch. Get your product in front of users as quickly as possible to gather feedback and iterate.</li>
<li><strong>Embrace Familiarity:</strong> Leverage the technologies you know and love to minimize the learning curve and maximize productivity.</li>
<li><strong>Start Simple, Then Scale:</strong> Begin with a minimum viable product (MVP) and the simplest tech stack that meets your needs. You can always evolve and optimize as you grow.</li>
<li><strong>Don't Be Afraid to Pivot:</strong> Be open to changing your tech stack if it no longer serves your needs. The right tools at one stage of your product's lifecycle may not be the right tools later on.</li>
<li><strong>Focus on the User:</strong> Ultimately, your tech stack is just a means to an end. The most important thing is to build a product that solves a real problem for your users and delivers exceptional value.</li>
</ul>
<p>By prioritizing speed to market, leveraging your team's strengths, and remaining adaptable, you'll be well on your way to building a successful SaaS product. Remember, the best tech stack is the one that empowers you to create something truly meaningful for your customers.  </p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Build and Deploy an AI SaaS with Paid Subscriptions ]]>
                </title>
                <description>
                    <![CDATA[ It is easier than ever to launch your own Software as a Service (SaaS) company! (The hard part is getting people interested in it). We just posted a course on the freeCodeCamp.org YouTube channel that will guide you through every step of building and... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-and-deploy-an-ai-saas-with-paid-subscriptions/</link>
                <guid isPermaLink="false">66b2010508bc664c3c097e59</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Mon, 18 Sep 2023 17:14:49 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/09/saas.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>It is easier than ever to launch your own Software as a Service (SaaS) company! (The hard part is getting people interested in it).</p>
<p>We just posted a course on the freeCodeCamp.org YouTube channel that will guide you through every step of building and deploying a full-stack AI SaaS application.</p>
<p>Elliott Chong created this course. He is known for his in-depth tutorials on AI-powered applications. By the end of this course, you will have a fully functional app that allows users to chat with any PDF using the OpenAI API.</p>
<p>Here are some of the course highlights.</p>
<p>🌐 <strong>Building a Full Stack AI SaaS from Scratch:</strong><br>Dive deep into the world of SaaS development as you construct a cutting-edge AI-powered application from the ground up. Learn how to create a fully functional SaaS using Next.js, OpenAI, TypeScript, and Tailwind.</p>
<p>💰 <strong>Monetization with Stripe:</strong><br>Discover how to monetize your SaaS application by integrating Stripe payment processing seamlessly. Implement a subscription-based revenue models.</p>
<p>🌟 <strong>Next.js 13.4 and App Router:</strong><br>Harness the power of Next.js 13's state-of-the-art App Router to create dynamic and responsive web applications.</p>
<p>🎨 <strong>Tailwind CSS and Shadcn for Stunning UI:</strong><br>Learn to design stunning user interfaces with the elegance of Tailwind CSS and the beauty of Shadcn.</p>
<p>🧠 <strong>OpenAI's Language Model API:</strong><br>Unleash the capabilities of OpenAI's API to empower your application with natural language understanding and generation.</p>
<p>🗄️ <strong>Efficient Database Management with ORMs:</strong><br>Master the art of efficient database interactions using DrizzleORM and gain valuable insights into managing data effectively.</p>
<p>The course is divided into the following sections:</p>
<ul>
<li>Intro &amp; Demo</li>
<li>Set Up NextJS</li>
<li>Set Up Shadcn</li>
<li>Set Up ClerkAuth</li>
<li>Home Page</li>
<li>DrizzleORM</li>
<li>File Upload Component</li>
<li>Set Up AWS S3</li>
<li>AI RAG Explanation</li>
<li>React Query Set Up</li>
<li>Set Up Pinecone DB</li>
<li>Chat Side Bar</li>
<li>PDF Viewer</li>
<li>Chat Component ( Vercel AI SDK )</li>
<li>Pinecone OpenAI Get Context</li>
<li>Persist Chat Logs to DB</li>
<li>Stripe Integration</li>
<li>Deploy and Outro  </li>
</ul>
<p>You can watch the complete course <a target="_blank" href="https://youtu.be/r895rFUbGtE">on the freeCodeCamp.org YouTube channel</a> (4-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/r895rFUbGtE" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Your Own SaaS – PagerDuty Clone ]]>
                </title>
                <description>
                    <![CDATA[ One of the best ways to learn software development is to create a slimmed-down version of software you use every day to get a better understanding of how it might work. This process helps you understand the problem space constraints and techniques re... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-your-own-saas-pagerduty-clone/</link>
                <guid isPermaLink="false">66b202faeea9870582e16c58</guid>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Tue, 20 Dec 2022 13:47:49 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/12/maxresdefault.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>One of the best ways to learn software development is to create a slimmed-down version of software you use every day to get a better understanding of how it might work. This process helps you understand the problem space constraints and techniques required to build a real-world use-case.</p>
<p>We just published a course on the freeCodeCamp.org YouTube channel that will teach you how to build your own SaaS app. In this case, a PagerDuty clone.</p>
<p>Ania Kubów developed this course. She creates popular software tutorials on both the freeCodeCamp channel and her own channel. </p>
<p>In this tutorial, Ania will teach you how to build a dashboard to let you know if your app is down. And if your app goes down you will be notified via email and SMS. This is a clone of the popular software as a service app called PagerDuty.</p>
<p>In this tutorial, we will recreate some of the key components of this application using JavaScript for our application logic, Postgres to store our data, and Twilio to power SMS notifications &amp; SMTP.</p>
<p>By the end of this tutorial, you will be able to:</p>
<ul>
<li>Create data structures in Postgres to feed into your application.</li>
<li>Create a UI and feed data into the UI.</li>
<li>Add functionality to the UI to interact and modify the data.</li>
<li>And finally hook up Twilio and SMTP to send email notifications and SMS notifications</li>
</ul>
<p>Here are all the sections in this course:</p>
<ul>
<li>Working with pre-made UI Components</li>
<li>Setting up our Postgres database</li>
<li>Creating Tables in Postgres</li>
<li>Feeding in Data to our Dashboard</li>
<li>Adding new Incidents</li>
<li>Deleting Incidents</li>
<li>The Team members page</li>
<li>Hooking up the Twilio and SMPT API</li>
</ul>
<p>Watch the full course below or on <a target="_blank" href="https://youtu.be/4xuBT3BbsYU">the freeCodeCamp.org YouTube channel</a> (1-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/4xuBT3BbsYU" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<h2 id="heading-video-transcript">Video Transcript</h2>
<p>(autogenerated)</p>
<p>Hey everyone here on the freeCodeCamp channel.In this tutorial, I'm going to be building a tool that every app on startup should have.</p>
<p>I'm going to build a dashboard to let you know if your app is down. And if it does notify you via email and SMS.</p>
<p>So in other words, it clone of the popular software as a service app pager duty.</p>
<p>My name is Ania Kubow. And I'm a course creator here on Free Code Camp, as well as on my own channel.</p>
<p>And I'm going to be your guide to this wonderful tutorial on building this platform here today.</p>
<p>One of the best ways to learn software development is to create a slimmed down version of software you use every day to get a better understanding of how it might work.</p>
<p>This process helps you understand the problem space constraints and techniques required to build a real world use case pager duty is awesome as it helps alert teams to high priority incidents like web outages or security issues using pre configured configuration strategies, and multi channel notifications.</p>
<p>In this tutorial, we will recreate some of the key components of this application using JavaScript for our application logic.</p>
<p>Postgres is store our data, results of other user interface and back end workflows.</p>
<p>And finally Twilio to power SMS notification as well as SMTP.</p>
<p>By the end of this tutorial, you will be able to create data structures and Postgres to feed into your application.</p>
<p>This will include creating an incidence table as well as team members table, we will then feed this data into our UI so we can see it and interact with it.</p>
<p>So as you will see here under the incidents tab, and the team members tab, that we are bringing back all that data.</p>
<p>The Team Members tab will also help us see who is working on what, as well as their shift times so that we can tell who is on call.</p>
<p>And when we will also then be able to see all the incidents that are currently acknowledged and being worked on, as well as filter them down to just the ones assigned to a specific user.</p>
<p>We will add functionality to bring up data if we click on specific rows, and much more, we will also add the ability to add team members from the interface as well as add in new incidents via the interface as well as delete a team member or delete an interface.</p>
<p>So bunch of stuff going on here.</p>
<p>And finally, we will hook up Twilio and SMTP. So that we can send email notifications and SMS notifications to our team members in case the website theoretically goes down.</p>
<p>This video is created thanks to a grant from resource, the local solution for building internal tools and platforms.</p>
<p>So what are we waiting for? Let's do it. Okay, so let's do it.</p>
<p>First off, I'm just going to create a new app.</p>
<p>So I'm just going to go ahead and click on here.</p>
<p>And I'm going to call this pager duty club.</p>
<p>Okay, and click create the app.</p>
<p>So there we go, we've just created our app, essentially, this is essentially our work zone.</p>
<p>And I'm going to start off by just dragging in some UI in order to make it look like a pager duty clone.</p>
<p>So the first thing I'm going to do is actually just drag in some tabs containers, as we're gonna have two tabs here, one to view the incidents, and one to view all our team members.</p>
<p>So let's make that reflect here.</p>
<p>We can also rename this actual UI component.</p>
<p>So I can call it main tabs if I wish.</p>
<p>Okay, great.</p>
<p>And I'm here, I'm just going to change this to say incidents.  </p>
<p>And this one to say team members.</p>
<p>So we are literally just creating our tabs container, just like so.</p>
<p>We can of course, also change the colors of this.</p>
<p>So if you just highlight the whole UI component, and then go down here, I can choose the header background color, I can choose the footer background color, or I could just choose the background, I'm going to go ahead with Canvas to make it much the background that we have.  </p>
<p>Okay, and if we wanted a little darker, we can also choose our own custom color.</p>
<p>So for example, I could do hex E, E, E, or just three E's for short.</p>
<p>And that will also be applied.</p>
<p>Wonderful.</p>
<p>To get rid of the third view that we are not going to be using simply just go ahead and click on here and delete it from view.</p>
<p>So there we have it, one view and a second view.</p>
<p>We can also personalize the actual selected colors.</p>
<p>So I'm gonna go with green for now as I think it looks better.</p>
<p>Okay.</p>
<p>Wonderful.</p>
<p>Now, under  </p>
<p>incidents, what do we want to show? Well, we should probably have some kind of header, right.</p>
<p>So let's go ahead and select a text option, we're going to have a simple text, or we're going to have a text area, or rich text editor, or even editable text, I'm just going to go ahead and select the text option.</p>
<p>And we can even choose if we would like to use HTML elements to kind of decide how big each of the text elements are going to be.</p>
<p>So I can have incidents on all teams as a title, just like so.</p>
<p>And it's styled like an h1 element title.</p>
<p>If you wanted to give a custom styling to this h1 element, that is also possible, okay, all you would have to do is go to scripts and styles and add in some custom CSS for this whole document.</p>
<p>We are not going to be doing that now.</p>
<p>But I just thought it was worth giving you a sneak peek at that.</p>
<p>Next up, what do I want? Well, we can have dividers to kind of separate out everything in here.</p>
<p>So I'm going to in fact, use a divider to separate the title from the table that we are going to input.</p>
<p>Now let's put in a table.</p>
<p>So our table at the moment is not going to mean much.</p>
<p>Okay, it's just going to have some dummy data in there.</p>
<p>So there is some dummy data that's being fed in, we're going to delete all of this as we're going to use some actual data from a Postgres database that we are going to make.</p>
<p>So just leave that blank for now.</p>
<p>In the meantime, I'm also just going to drag in some text to go below here.</p>
<p>And I'm going to use the H five element this time, so a smaller header  </p>
<p>to say, your open incidents  </p>
<p>and then just drag in some more texts, so that we can actually see those incidents.</p>
<p>For now I'm going to put zero triggered and make this red.  </p>
<p>So the text is going to be danger.</p>
<p>And then just copy and paste that.</p>
<p>And this time, this is going to be to show our acknowledged incidents.  </p>
<p>And I'm going to change this color to be info.  </p>
<p>Okay,  </p>
<p>wonderful.</p>
<p>So there's a lot you can do with styling with this text, I'm going to leave it like that for now.</p>
<p>And we're going to choose this number dynamically based off our you guessed it, our database.</p>
<p>So in fact, I'm just going to copy this and paste it because we're going to drag this over to the right and see all our open incidents as well.</p>
<p>So we've got your open incidents, let's also have all open incidents.</p>
<p>And again, we're going to feed that number in dynamically on component text seven, and component text five.</p>
<p>Great.  </p>
<p>Now I'm going to add a button, this button was clicked, it's going to help us add a new incident to our database.</p>
<p>So we can actually do that from our UI, which is kind of cool.</p>
<p>And this button, we can style it up.</p>
<p>So I can have an incident if I wish.</p>
<p>And let's make this blue, green just to match the rest of this board.</p>
<p>And I can even choose to make it a little bit bigger.</p>
<p>Next, I'm just going to use a container to contain a bunch of stuff that I want to add next.</p>
<p>So I'm just going to put that in here like so for now.</p>
<p>And this time, in fact, I'm going to get rid of the header.</p>
<p>So we can do that, we can select the whole UI component, and then I can choose to hide the header.</p>
<p>So at the moment, we're just showing the body if you wanted to show footer, then you can do that.</p>
<p>That is totally up to you.</p>
<p>Okay, wonderful.</p>
<p>So we have one container, I'm actually going to have to so I'm just going to drag in another one right below here.</p>
<p>Okay, so that we go.</p>
<p>This one again, I don't want to have a header showing, so I'm just going to hide it now and the first one, we're going to just display who is on call.</p>
<p>So I'm going to just have the title here of encore.</p>
<p>Okay, and I'm just going to make it green.</p>
<p>So I'm going to change the color to be green of the text.</p>
<p>We can also make it bold if we wish.</p>
<p>Okay, so I'm just going to use a opening and a closing tag for that to make that bold.</p>
<p>That name next thing I'm going to drag in is a place to actually display whoever is currently on call.</p>
<p>So for now I'm just going to put x x x as we don't really know who's on call yet.  </p>
<p>because, once again, we're going to get that from our database.</p>
<p>So I'm just going to also make sure that it's an h3 element, just like that, just to make sure it's a bit bolder.</p>
<p>And we can even use an avatar.</p>
<p>So I can just drag in a premade component.</p>
<p>That's right, that will show us our avatar.</p>
<p>At the moment.</p>
<p>Again, this is just based off my information, my user detail, I'm the current user of this app.</p>
<p>But we will over write this.</p>
<p>So in fact, just so I don't forget to do this, I'm just going to put x x x for all of these so that we don't forget to add that in dynamically.</p>
<p>Great.</p>
<p>Now, we can put a divider or we can add a footer is totally up to you.</p>
<p>Let's choose to add a footer.</p>
<p>And in the footer, here, I'm going to add some more information, I'm going to drag in a text component, just like so.</p>
<p>And here, I'm going to put in a bolt you are on call for.</p>
<p>And once again, this is going to be fed dynamically, I'm going to essentially create a list of tags.</p>
<p>So we can find these tags in here too.</p>
<p>And I'm just going to put them in below.</p>
<p>Just like sir.</p>
<p>Okay, so that's going to display what we as the person on call on call for.</p>
<p>In here, again, I'm just going to drag in some more tax, this is going to say and called on call.</p>
<p>Now.  </p>
<p>Let's go ahead and make this color green.</p>
<p>And here, we're just going to list out all the incidents that we are currently on call for.  </p>
<p>Okay, so I'm going to exercise for now as that will be fed dynamically.</p>
<p>And we're also going to show the shift of the current person that is on call.</p>
<p>So it's really obvious.  </p>
<p>I'm going to put once again and fold your shift,  </p>
<p>and then close this tag.  </p>
<p>And then actually display the two shift times as well as some contact information.</p>
<p>So this is going to be from, we're going to put our starting time of the shift, and this is going to be two and we're going to put the end time of the shift of the current person on call.</p>
<p>And here, I'm just going to put some information like contact.  </p>
<p>And then I'm going to put info at  </p>
<p>Free Code camp.org as well pretending business free code camps pager duty app to make any changes to your shift.  </p>
<p>Okay, wonderful.</p>
<p>So that's really it.  </p>
<p>Okay, so that's for showing all the incidents.</p>
<p>Of course, we haven't hooked this up to any data quite yet.</p>
<p>Let's work on the team members tab next.</p>
<p>So just like with the team members tab, in fact, what I'm going to do is so super simple, I'm just going to copy this, I'm going to copy the divider, we're going to copy the button.</p>
<p>And I'm also going to copy this container  </p>
<p>and click Command C.</p>
<p>And inside here, I'm just going to paste it in.</p>
<p>Okay, so it looks kind of similar.</p>
<p>We're going to change this title to say, team members, okay.</p>
<p>And on the Add incident button, well, I'm going to put add, new team member, of course, as this is dealing with team members.</p>
<p>And in here, well, let's just delete that we don't really want that, I'm just going to show the selected team member from the table that we're going to have, we're not going to have an avatar.</p>
<p>So let's delete that we're not going to have your own code for instead, we're going to have a signed operations.</p>
<p>So we can see the assigned operations that are there for the user.</p>
<p>Okay, so there we go, we have the same operations that will be viewed as a tag list.</p>
<p>And I'm also going to put in the shift dates for the user we are selecting.  </p>
<p>OK, so once again, in bold, I'm just going to put shift dates  </p>
<p>and close that off.  </p>
<p>Okay, and once again, let's just put from  </p>
<p>two  </p>
<p>and then I'm also going to put the evil of the person that we want to contact.  </p>
<p>Okay, so our emails gonna go.  </p>
<p>Great.  </p>
<p>Now one small is also dragging a table, as this is going to show all our team members from what you guessed, our Postgres database.</p>
<p>So let's go ahead and delete all that data.</p>
<p>That is some fake data that has been passed through  </p>
<p>And finally, outside of the incidents and outside of the members, so outside of this whole tab container, I'm going to have another container.</p>
<p>Okay, so I'm going to put in a container.</p>
<p>In fact, let's make a form, as essentially, we are going to be sending something from here, we're gonna be sending an email.</p>
<p>Okay.</p>
<p>So there we go, there is an email form for us.</p>
<p>And I'm just going to put email  </p>
<p>composer, as that is what I want to build, let's make the title of this the header, I'm going to change the color of the header.</p>
<p>So if we find the header background, I'm just going to go with highlight as the color of choice.</p>
<p>And let's change the Submit button to I'm going to make a warning.</p>
<p>Okay, and in here I'm going to put to send to, and then whoever we are highlighting from the team members is going to be the person to receive this email.</p>
<p>So that is going to be quite cool.</p>
<p>And another thing we can do is use the rich text editor.</p>
<p>So now we get a whole rich text editor component, this is pretty cool.</p>
<p>Okay, imagine trying to code that out yourself.</p>
<p>And in here, we're going to put Hi, and we're just going to put the selected user from our table.  </p>
<p>So that's it.</p>
<p>That's what we have built so far.</p>
<p>Now let's get to linking all of this up.</p>
<p>So for this tutorial, I'm going to use a render.com lynda.com is essentially a way for us to host our database in a non paid way.</p>
<p>Okay, so please go ahead and just go to the dashboard, sign up, go to the dashboard.</p>
<p>And we are going to sign in, I'm going to just choose to sign in with Google.</p>
<p>Okay, so please choose your own way of signing in, it's up to you.</p>
<p>And once he actually need to click on New, and it's a Postgres database that we are going to be create that is hosted on render, I'm going to call this pager duty as that's what I'm creating this database for, I'm happy for the database to be randomly generated, as well as the user name.</p>
<p>And we're just going to select the free tier and click create database.</p>
<p>Okay, so it really was that easy that is creating.</p>
<p>In the meantime, I'm gonna go here and go to resources.</p>
<p>So let's go ahead and create a new resource is going to be a Postgres SQL database, I'm just going to call it pager duty, as that will be easy for us to find the host.  </p>
<p>Well, that is this.</p>
<p>So I'm just going to copy this host name, like so.</p>
<p>But then I'm also going to  </p>
<p>have to append  </p>
<p>dot Oregon  </p>
<p>postgres.render.com.</p>
<p>So just make sure to append that keep the port as 5432.</p>
<p>As you see here, the database name is pager duty.</p>
<p>So let's put that in here.</p>
<p>And as authentication, we're going to use the database username, which is this, and the database password.</p>
<p>So here's the password, please go ahead and use your own because this one will not work after this tutorial is.</p>
<p>So there we go.</p>
<p>I'm going to click connect using SSL.</p>
<p>And let's just test this works.</p>
<p>And connection is a success.</p>
<p>This is because we left the address, IP address as everywhere, okay.</p>
<p>But if you wanted to, you know, keep it kind of private, you might consider adding these IP addresses.</p>
<p>So great.</p>
<p>Let's create a resource.  </p>
<p>Let's go back to the resources.</p>
<p>And now I'm just going to shut that down and refresh this.</p>
<p>So it loads the latest resources.</p>
<p>And I'm going to choose pager duty as my resource name, that's just the one I made.</p>
<p>Okay, so I'm actually going to use this UI in order to add the actual tables that I'm going to use for this project.</p>
<p>Okay, so I'm going to just go ahead and create my first table.</p>
<p>So I'm going to use the Create Table command.</p>
<p>And I'm going to create a table of incidents.</p>
<p>Okay, so get up your parenthesis and let's define what's going to go on our table.</p>
<p>Well, I'm going to put an ID, which is going to take an integer value, I'm going to put an urgency level which is just going to take our chart 30.</p>
<p>So 30 characters, essentially, I'm going to have triggered Is this a triggered event? Well, it's yes or no.</p>
<p>So I'm gonna have a Boolean for this.  </p>
<p>Next, I'm also gonna have acknowledged, this will also be a Boolean,  </p>
<p>as well as resolved, which is also going to be a Boolean.</p>
<p>Next we're going to have a description of the actual incident itself.</p>
<p>So that's going to take  </p>
<p>bar char.</p>
<p>And I'm going to actually limit that to 30.</p>
<p>Okay, we don't want them to be too long, and then assigned to, and this is going to take an integer.</p>
<p>In fact, it's going to take the ID of a an employee.</p>
<p>And I'm also going to have a created  </p>
<p>date, which is gonna take a date.</p>
<p>So that's what my table looks like, don't forget to finish it off with some semi colons.</p>
<p>Let's check if this works, I'm going to click Save and Run.</p>
<p>And great, that seems to have worked, we haven't got any errors.</p>
<p>And we're gonna call this playground for now, as I'm essentially using this as a little mini playground to add our tables.</p>
<p>Okay, so that was one thing that we have done, you can keep this in here, if you wish.</p>
<p>In fact, I'm just going to comment that out.</p>
<p>Because I'm going to create a new table to create  </p>
<p>table team, this is going to take our team.</p>
<p>So what's our team going to be made up of? Well, each team member is going to have an ID, which I'm going to say has to be an integer, they're going to have a first name, which I'm going to take as virtuoso characters, let's just do 30.</p>
<p>Again, a last name, which I'm going to be super strict about this too.</p>
<p>Of course, you don't have to have 30, I'm just choosing to for now.</p>
<p>And an email, var char binders put 225 wide dots, okay, you can have higher character numbers, if you wish.</p>
<p>Next, I'm going to have a phone number of your team mate, which is going to be an integer value, and Avatar, which is actually going to be a URL to an image on the internet.</p>
<p>And then we're going to have on call to check if they're on court or not.</p>
<p>And this is going to be a boolean value, as well as a shift,  </p>
<p>start.</p>
<p>And that's going to be a date,  </p>
<p>a shift and value, which is also going to be a date, and incidents that they are working on, which is going to be text, but it's also going to be an array of texts.</p>
<p>So that's how I would write that.</p>
<p>Great.</p>
<p>And just finish off some semicolons.</p>
<p>And hit save and run.</p>
<p>So there we go, we have just added our table for team members.</p>
<p>So I'm just going to comment that out.</p>
<p>I'm going to go ahead and just add two incidents just to start off with just so we have stuff to play around with.</p>
<p>So I'm going to insert into the table called incidents.  </p>
<p>And we're going to insert an ID and urgency value a triggered value, an AK, knowledge value, resolved value or description assigned to  </p>
<p>making sure to spell it exactly the same as we did up here.  </p>
<p>created date.</p>
<p>And that should be it.</p>
<p>So those are the values we want to insert into let's actually get the values.</p>
<p>So the value for the ID, I'm going to put incident ID number one, we're going to put urgency as the string of high as well as the triggered is going to be true.</p>
<p>Technology is going to be true and resolved is going to be false.</p>
<p>So we've put in Boolean values for those.</p>
<p>And now I'm just going to put in a description of this incident, it's going to be DevOps escalation.  </p>
<p>So just like that, it's going to be assigned to the employee with ID 201.</p>
<p>And then we're just going to put in a date that this was created at, I'm going to put a date in the past, okay, because, you know, we don't want to put everything as today's date.</p>
<p>So there we go.</p>
<p>Okay, and don't forget to finish the off with a semi colon.</p>
<p>Okay, so there is our semicolon, and just hit save and run.  </p>
<p>Great.</p>
<p>And that has run successfully.</p>
<p>So that is one incidence, let's just add a another one.</p>
<p>This one is going to have the value of two.</p>
<p>Let's put the  </p>
<p>urgency as low.</p>
<p>Let's add true, maybe false false, this time for triggered, acknowledged and resolved.</p>
<p>This one can be called Security ops.  </p>
<p>S escalation making sure to spell security correctly.</p>
<p>It's going to be assigned to use a 201 as well.</p>
<p>And let's put this as maybe one day  </p>
<p>earlier.</p>
<p>I'm click Save and Run.  </p>
<p>Great.</p>
<p>And I'm just going to comment that out.</p>
<p>I am now just going to insert into team so I'm only going to insert one person into here i  </p>
<p>Pick, that will be fine, we need to get the ID, we also get the first name of the person we are inserting, and the last name making sure this will exactly the same as we have in this table.</p>
<p>Next up is email.  </p>
<p>Next up is the phone number.</p>
<p>Next up is the Avatar.</p>
<p>And I'm going to have on call.  </p>
<p>It's up shift  </p>
<p>starts  </p>
<p>shift and, and the incidence attached to them.</p>
<p>Once again, just making sure that everything is spelled correctly, because next we're going to put in the values.</p>
<p>And the value of this, well, this is going to be team number 201, the name is going to be Ania.</p>
<p>And their last name is going to be Kubo.</p>
<p>Now my email address, I'm just going to put as the string of Ania at Free Code camp.org.  </p>
<p>The phone number, I'm just going to put a fake phone number for now.  </p>
<p>And as my avatar Well, I'm just going to use my avatar from Free Code Camp.</p>
<p>So please make sure to take an image that you know is unlikely to be taken offline, or alternatively store them yourself on imager.com.</p>
<p>Or you can put them on an external database such as AWS, for example.  </p>
<p>So I'm just going to copy this image address.</p>
<p>And I'm just going to paste it in like so.</p>
<p>Now after the avatar, I'm just going to specify if this person is on call, I'm going to put true.</p>
<p>And then I'm going to also  </p>
<p>put a made up start shift time, a made up and shift time as a string,  </p>
<p>and then an array.</p>
<p>So we can literally just put the word array like so.</p>
<p>And then I'm going to put in dev ops, s collation.  </p>
<p>And security ops, a skull escalation as the incidents assigned to this user.</p>
<p>Okay, don't forget to end it with some semicolons, hit save and run it say integer out of range, that's fine, I'll just change the format of the folder.  </p>
<p>And now I'm going to add in that Twilio phone number making sure to add 001 at the front and hit Run and safe.</p>
<p>Now it does say that integer out of range, which is kind of strange, I don't think this should be out of range.</p>
<p>But that is fine, we might have to make this a string instead.</p>
<p>So let's go ahead and make that a string, I'm actually going to just comment this out, we're going to drop the table DROP TABLE team.</p>
<p>So essentially, we're going to delete it, because we have to change the data type of the phone number.</p>
<p>So make sure to drop that.</p>
<p>And now I can create a table again.</p>
<p>So this is good for anyone who perhaps made an error in the first place.</p>
<p>And I'm just going to change this to var char as well.</p>
<p>So that will not take a string.</p>
<p>So save that and run it to create this table again, with the number of being an email.</p>
<p>And now we can insert into Tim, after you have commented this out.</p>
<p>So save and run.  </p>
<p>And great.</p>
<p>We have now added our first team member, so I'm just going to comment this out for now.  </p>
<p>Okay, so that was our playground.</p>
<p>And now I'm going to get some data that we can feed in into our first table of all the incidents.</p>
<p>So let's create a new resource.</p>
<p>And let's make sure that the result is pager duty.</p>
<p>I'm just going to do select all from the table incidents.</p>
<p>And click Save and Run.</p>
<p>Let's see what that looks like.</p>
<p>And indeed, we get two incidents back.</p>
<p>I'm going to rename this get incidents as that is essentially what this query does.</p>
<p>And now in here, instead of adding data manually, I can get incidents and get the data of those incidents so that it feeds in there and it's automatically mapped out to these lovely table rows.</p>
<p>What is cool is if you want to see the whole data object that comes back, you can all you have to do is go to state and get the incidents and there's the data and you will see all that information as  </p>
<p>The arrays that we can use.</p>
<p>Okay, so that's just in this tab on the left here, I'm just going to minimize that left panel now.</p>
<p>So that's what I have done.</p>
<p>Let's carry on working on this.</p>
<p>So another cool thing we can do is actually adjust which kind of columns do you want to see or edit or anything like that.</p>
<p>So for example, I can choose to hide the description if I want.</p>
<p>So I'm just going to select the whole table.</p>
<p>And I'm going to choose to hide the description by pressing on this little irate here, I'm also going to hide the assigned to, I'm going to keep the created date.</p>
<p>And I can even if I want, add a new column, add a custom column.</p>
<p>And this column is going to help me delete incidents if I want.</p>
<p>So at the moment, I'm not really going to put anything in here apart from just decide that it's going to be a button.</p>
<p>Okay, that is my button.</p>
<p>And the value of this is just going to say, delete.</p>
<p>So there we go, we can of course, make the columns smaller if we wish, and just in general, play around with this a little bit better.</p>
<p>So I'm happy with this table.</p>
<p>Let's now change the value of this hard coded zero.</p>
<p>And I can do so easily, I can literally get the incidence data.</p>
<p>And I can look inside to see how many of the objects triggered are true, right.</p>
<p>So for this, I'm going to use the filter method.</p>
<p>And I'm going to look through each item, okay, by essentially filtering through each one, and used AI as the representation of each item that we are filtering.</p>
<p>And if i equals true, I'm going to get the length of that array.</p>
<p>So as you will see here, two incidents are triggered.</p>
<p>And if we look here, indeed, two incidents are triggered.</p>
<p>So if we do the same for acknowledge, we should get one acknowledged incident.</p>
<p>So let's try again, we'll use the two curly braces as that is how we do things in retool in order to get values from the queries.</p>
<p>So the query we wrote is called Get incidents, right.</p>
<p>So I'm essentially getting the name of whatever we called this query and getting the data from it.</p>
<p>And I am going to this time filter by the acknowledged, so I'm going to get acknowledged and use the filter method on it to essentially filter and if I or each item in that array equals true, I'm going to get the whole arrays length.</p>
<p>And indeed, we get a one.</p>
<p>So once again, curly braces, in order to access data from the queries, the query I wanted to access is the get incidents query, you can see all the information that comes back here.</p>
<p>And again, if you really want to see the whole object, we can look in here.</p>
<p>So I've got incidents I've got into the data object, then I've gone into the acknowledged array, and I filtered out anything that is equal to true and got the length of that array.  </p>
<p>Got it? Cool.  </p>
<p>Okay, so to do your open incidents, we're going to have to do something else, because we're going to actually have to get the user.</p>
<p>Okay, so perhaps let's do that next.</p>
<p>Now, who was the user? Well, I think the user should be whoever's on call, right.</p>
<p>So let's go ahead and create a new resource query, I'm going to call this get user.</p>
<p>And I'm going to select all from team this time.</p>
<p>So the table of team were on call equals true, I'm going to save and run this.</p>
<p>So there we go, it comes back with one user and at all points, we really should only have one person on call, right.</p>
<p>So that is a kind of rule that should be done in the backend, this isn't a front end thing.</p>
<p>So we can just assume that this will come back with one user at all times.</p>
<p>So that is going to be a user.</p>
<p>And I'm going to filter incidents by the user.</p>
<p>So let's try another resource.</p>
<p>I'm going to write filter incidents.  </p>
<p>And I'm going to select all, from incidents.  </p>
<p>were assigned  </p>
<p>to equals and then I'm just gonna get the user data.</p>
<p>So this is our get user query.</p>
<p>We're going to get the ID but only going to get the first ID from the array because we are assuming there is only one so that should be fine.</p>
<p>And don't forget your semicolons at the end, so  </p>
<p>I'm just going to save and run that.</p>
<p>And making sure to spell incidents there correctly.</p>
<p>Just run that again.</p>
<p>Okay, so two items come back.</p>
<p>This is because indeed, two items are assigned to me and your Kuba because my user ID is 201.</p>
<p>Great.</p>
<p>Once we get to adding more incidents in here, you will see how that changes.</p>
<p>So now that means I can use my filter incidents query to dynamically update these.</p>
<p>So once again, use our curly braces, filter incidence is the query I want to use, I want to get the data from it.</p>
<p>And I want to get the triggered.  </p>
<p>array.</p>
<p>And I want to filter it based on if I have the item in here in this array equals true, and I want to get its length.</p>
<p>Okay, so to will be triggered, that is correct, because I'm just going on my open incidents.</p>
<p>And other moment, I'm the only user in here.</p>
<p>So once again, you guessed it, we're gonna get the curly braces, we're gonna get the filtered incidents.</p>
<p>So let's get the filter incidents data.</p>
<p>And this time, we're going to get the acknowledged array and filter it by looping over i and if i equals true, we're going to get to the length of this array at the end.</p>
<p>Wonderful.</p>
<p>So this is all looking good.</p>
<p>We can now actually also fill out this because we aren't technically getting the user.</p>
<p>So we can use this query, let's run it again and see what it looks like to find out who's on call, right? So I can use my curly braces, to go into the get user query and get the data and get the first name that comes back to us.</p>
<p>Okay, so I'm going to get the first name.</p>
<p>And I could also put the second name, I've literally just put a space there, this will work, I'm going to get user data.  </p>
<p>Last Name, great.</p>
<p>And same for the avatar, well, I'm going to get user data,  </p>
<p>Avatar at this time, and just go into the first item of that array.</p>
<p>Same for the email.</p>
<p>So on the label here, I'm just going to get into get user data to the email  </p>
<p>and get the first item from the array.  </p>
<p>In fact, maybe we should have that on the actual caption of the label.</p>
<p>And here, we will just have the first name again.  </p>
<p>So I'm just going to take all of this  </p>
<p>and whack it onto the label here.  </p>
<p>We can also essentially map out what the tags are.</p>
<p>And here we're going to get the incidents, right, so let's get rid of that curly braces, get user data incidents.</p>
<p>And we're gonna have to go into the first item, that array.</p>
<p>And there we go.</p>
<p>So these incidents have now been mapped out onto here.</p>
<p>Great.</p>
<p>Let's also do the same for here.</p>
<p>So I'm just going to show all the incidents, I'm going to get the user data incidents.  </p>
<p>And actually, if I treat this as an element, we can actually just put the array in here like so.</p>
<p>Okay, so that's something I have done.  </p>
<p>Now, from here, I'm just gonna get into the user again data and get the  </p>
<p>shift start.  </p>
<p>And once again, on here, I'm going to get user data, this time shift, and great.</p>
<p>So we've populated all of that base of the get user query.</p>
<p>How cool is that? Next up was work on adding an incident.</p>
<p>So for this, I'm actually going to create a modal that's going to pop up.</p>
<p>So in fact, let's go ahead and just put that up here.</p>
<p>And I'm just going to delete this button.</p>
<p>So delete it, and I'm going to change the text of this to be add incident.  </p>
<p>Let's also change it to be green.</p>
<p>So I'm just going to minimize that for now.</p>
<p>As well as the state.</p>
<p>The accent background is going to be green.</p>
<p>We're just going to make it a little bit bigger.  </p>
<p>Okay, so that is our modal.  </p>
<p>And here I'm just going to put in some tags.</p>
<p>So I'm going to drag in some texts, and this is going to say add incident.</p>
<p>I'm going to make this an h2 element, add  </p>
<p>incident  </p>
<p>Just like so  </p>
<p>let's also put a divider, I'm going to put that in right down here.  </p>
<p>And then I'm going to create a number input.</p>
<p>So one specifically to put in numbers.  </p>
<p>So just like that with a label in incident ID.  </p>
<p>Great.</p>
<p>Next, I'm going to create a drop down.</p>
<p>So I'm going to have this select drop down, just like so.</p>
<p>And I'm going to put the value of the label as urgency.</p>
<p>And I'm going to hard code my options, option one is going to be high, option two is going to be medium.</p>
<p>And option three is of course going to be low.</p>
<p>So those are my options.</p>
<p>And if you leave the label as empty, it will just take the value as the default.</p>
<p>And I'm happy with that.  </p>
<p>Next, I'm just going to create some  </p>
<p>switches.</p>
<p>So this is because I'm going to be dealing with Boolean values, so I'm fine with this, I'm going to have one for triggered.  </p>
<p>Let's also have one for acknowledged.  </p>
<p>And finally, let's have one for resolved.  </p>
<p>Okay, so those are my three options right? There.</p>
<p>You can of course, start them up as much as you want.  </p>
<p>Next, we're going to have a description input.</p>
<p>So for this, I think we should just have some text.</p>
<p>So I'm going to put in a text input.</p>
<p>This is why I kept the description short, Max 30 characters, because you know, that's not a lot to work with.</p>
<p>And we can also cap this as well.</p>
<p>So we can cap the value.  </p>
<p>You can have a max length of 30, just like so.</p>
<p>And let's go ahead and put description  </p>
<p>here.  </p>
<p>And finally, let's have one more select drop down.</p>
<p>So select.</p>
<p>And here, we're going to select who this is assigned to.</p>
<p>So I'm going to put assigned to.  </p>
<p>And instead of having options, I'm going to map out the options, this time, the data source for this, well, we're going to have to get all the team members, right, so let's go ahead and write a new query, I'm going to call this get team and we're gonna get all the team members.</p>
<p>And the query for this is select all from Team and run it.</p>
<p>So that's going to be the data source, get team.</p>
<p>And that's going to auto populate for you.</p>
<p>And great, this is fantastic is exactly what I want.  </p>
<p>Okay, so now we can actually choose which team member to select in such a nice way.</p>
<p>Okay, and whichever one we select, it's actually going to pick out the ID of that item.</p>
<p>So if I select me, the value of this, when it's selected will be 201.</p>
<p>So readable, and then practical.</p>
<p>Great.</p>
<p>And finally, I'm just going to put in a button,  </p>
<p>that's going to submit this modal.  </p>
<p>So I'm just going to put Summit, and we can actually make the height of this Moodle dynamic based on what's inside.</p>
<p>So I can do hug content, and then that will happen.</p>
<p>Okay, wonderful.</p>
<p>Now let's write a query for adding an incident.</p>
<p>So add incident, just like so making sure the resource is pager duty.</p>
<p>And once again, we're essentially going to use this piece of code.</p>
<p>So I'm just going to copy all of that, because that is the code that we will need in order to add a new incident, of course when it's uncommented out.</p>
<p>So that's what we are going to insert into the table incidents.</p>
<p>However, this time the values will not be hard coded.</p>
<p>They will in fact be taken from here.</p>
<p>So I'm going to use my curly braces to access this component which is called number input one number input one and get its value.</p>
<p>If you hover over that you will see that zero well if I change this that will change and you will see the value is now two  </p>
<p>Okay, so as literally getting the value of that input, this second one is the urgency, right.</p>
<p>And this component is called select one.</p>
<p>So I'm going to gret select one and get its value once more.</p>
<p>So if I choose high, that will now be high.</p>
<p>Wonderful.</p>
<p>So hopefully that's kind of making sense.</p>
<p>Next, we're going to go on these switches.</p>
<p>So that is going to be switch one, two, and three.</p>
<p>So once again, I'm just gonna use my curly braces to get switch ones value, switch, two's value, as long as these are of course in the correct order.</p>
<p>And then switch threes value switch, three, value.</p>
<p>And finally, I'm just going to minimize this, we need to get the description which is text input one, and the Select to value.</p>
<p>So description is going to go here.</p>
<p>And that is text input value one, I believe, yes, text input one and select two.  </p>
<p>So select to value, and we're just going to get today's date.</p>
<p>So I'm going to use the new date object from JavaScript to do that, once again, make sure it's in curly braces, and just call it.</p>
<p>So that's what I've done, I'm essentially getting the values of all these inputs, let's have a go at filling this out.</p>
<p>So let's go Incident Number three, urgency, high triggered, acknowledge, not resolved description, well, I can go ahead and put anything I want, let's put  </p>
<p>deployment.  </p>
<p>And assigned to what I'm the only one here right now.</p>
<p>So great.</p>
<p>And now this button, well, we need to trigger the Add incident query we just wrote.</p>
<p>So I'm going to add an event listener control query that's been generated for me at incidents because I'm currently on that query.</p>
<p>And we're going to trigger that.</p>
<p>Okay, that looks great to me.</p>
<p>So now, before I submit this, I'm going to run this query with all these values filled out, you will see the values that we are essentially going to put, they've all been now filled out.  </p>
<p>And if this works, well on success actually want to do a few things, right, I want to get all the incidents again.</p>
<p>So the freshest incidents, and I also want the modal to close.</p>
<p>So I'm going to control a component to this time.</p>
<p>And that component is the modal.</p>
<p>So let's go ahead and find it.</p>
<p>And I want it to close, we can also add some other fun things like confetti if we want.</p>
<p>So confetti, let's go ahead and do that.</p>
<p>And save.</p>
<p>So I haven't run this because I want to trigger it by pressing this button.</p>
<p>So I'm going to click on here,  </p>
<p>we have got an error, let's debug.  </p>
<p>It would seem I just put a extra  </p>
<p>string, I didn't delete those strings properly.</p>
<p>So now let's save.</p>
<p>Let's try it again.</p>
<p>And hit submit.</p>
<p>And great that has worked.</p>
<p>And tada, we have gotten the fresh incidents again.</p>
<p>And that has updated our table.</p>
<p>So this is looking great.</p>
<p>And you will see that this has been updated to  </p>
<p>oh, this has not been updated.</p>
<p>That's because we of course also need to run the filter incidents method as well.</p>
<p>So on success of adding an incident, let's add the gets filtered incidents also.</p>
<p>Okay, so there we go.</p>
<p>Now I've just run that again.</p>
<p>So we would have added the same stuff again, because as you will see, that information is still there.</p>
<p>So now let's work on deleting that third one, okay, because we don't want it but at least we know this is working.</p>
<p>And this is working and everything is updating.</p>
<p>So now let's work on it deleting an incident.</p>
<p>So this one I'm going to call delete incident.  </p>
<p>The resource is going to be pager duty, and all I'm going to do is delete from incidents where ID equals and then this is table one selected row.  </p>
<p>Data it so this for the either it is going to cause a problem.</p>
<p>This is because you know, if we essentially delete, this one is going to look at the ID and it's going to delete both of these.</p>
<p>So just keep in mind that whenever you add an incident that Id needs to be unique.</p>
<p>So save that and onsuccess of this well once again, I'm going to essentially get all the incidents  </p>
<p>and run  </p>
<p>The filter incidents query once more.</p>
<p>So let's save that.</p>
<p>And now let's hook up this button to that.</p>
<p>So let's find that custom button that we made an on click, we're going to run a query, and that query is delete incident.</p>
<p>Okay, so that's what we want to happen.</p>
<p>Now let's try it out.</p>
<p>I'm going to click Delete incident.</p>
<p>And that should delete and it's deleted all of these incidents, and it's gotten all the incidents again, so we can view them in the table.</p>
<p>And it's updated these based on the filter incident query and the get incident query.</p>
<p>Wonderful.</p>
<p>So I'm really happy with this.</p>
<p>Let's carry on.</p>
<p>Okay, now it's time to move on to the team members page.</p>
<p>So in here, let's go ahead and use the get team query in order to populate this table.</p>
<p>So once again, I will use my curly braces, get the get team query and the data from it.</p>
<p>And that should automatically update my table right here.</p>
<p>Again, we can choose which columns to hide, so I'm going to hide the avatar column, this time, I'm going to hide the phone number, I'm also going to hide the start shift and the end shift as well as the incidence, okay, and I'm going to add a custom column once again, to be able to delete that user.</p>
<p>So this is going to be a button.</p>
<p>So let's go ahead and do that.  </p>
<p>So there we go, there is a button that says, delete after we update the value.</p>
<p>So great.</p>
<p>Before we go deleting anything, let's populate this container with this selected user, as well as add functionality to add a new team member.</p>
<p>So to do this, I'm actually going to rename this component, as I mentioned, this is something you can do.</p>
<p>So I'm just going to call it team table.</p>
<p>Okay.</p>
<p>And that just makes it more reasonable for when I use this table.</p>
<p>So the team table, so I can literally use my curly braces, to access the team table component  </p>
<p>and get the selected row data.</p>
<p>And I could just grab the first name, if I wish of that selected row.</p>
<p>And of course, I could do so for the last name too.</p>
<p>So once again, that's going to Bacardi braces.</p>
<p>And I'm just going to use 10 tables selected row data.</p>
<p>Last Name, this time.  </p>
<p>And Wonderful.</p>
<p>Great, we can also use the avatar that I have hidden from the table, but it still does exist.</p>
<p>So if I want to just go ahead and grab an image,  </p>
<p>just like so.  </p>
<p>And now I can go into the team table on small selection of data and get the avatar as that exists.</p>
<p>On here, even though it's a deleted column.</p>
<p>Once again, if you want to have a look at the objects for this, just have a look in here look at get team data.</p>
<p>And we're getting the selected row data, right avatar.  </p>
<p>And that is the URL that I am getting.  </p>
<p>Great.</p>
<p>And now let's populate these other components.</p>
<p>So here once again, I would delete all that would use the team table selected row data to get the array of incidents that will be mapped out onto these tags.</p>
<p>And you know what to do this should be easy.</p>
<p>Now once again, let's go into the team table selected row data and get the shift start time.</p>
<p>And then also get this shift and time.</p>
<p>So team table, select data,  </p>
<p>shift and is what I want.</p>
<p>And here I'm just going to display the email address.</p>
<p>So once again, Team table selected or data dot email.</p>
<p>Wonderful.</p>
<p>And that was it very painless.</p>
<p>Now let's get to adding a new team member.</p>
<p>So for this, I'm actually going to use the modal components.</p>
<p>I'm just going to put that in here for now and it's delete this button.  </p>
<p>So I'm just going to delete that and move this over.</p>
<p>So move over the modal button  </p>
<p>just to here.</p>
<p>It's going to make it a little bit bigger.</p>
<p>And of course we can style it up.</p>
<p>I'm just going to make everything green.</p>
<p>And then it's changed the font of this to be add new team member.  </p>
<p>Great.</p>
<p>So now let's work on the modal  </p>
<p>So for this, I'm just going to drag in some text, let's make this an h3 element.</p>
<p>And it's gonna say add a new team member.  </p>
<p>Just like that.</p>
<p>And I'm just going to drag it out, I'm also going to have another divisor just like we did previously.</p>
<p>So I can put that divide it in right here.  </p>
<p>And then let's add some inputs.</p>
<p>So I'm going to put a number input.  </p>
<p>And this is going to say, Team Member ID.</p>
<p>So let's change the text of the label to say, Team Member ID.</p>
<p>Let's also just have a text input, this is going to be for the first name.</p>
<p>And we're gonna also have one for the last name, too.</p>
<p>So let's change this to say, first name.  </p>
<p>And I'm literally going to copy and paste.</p>
<p>So the second one shows up.</p>
<p>So I can change this to last name, we're also going to have email.</p>
<p>So there is a special email input we can use.</p>
<p>So there we go, I'm just going to drag that in like so and put it here.  </p>
<p>Maybe let's put some two dots there, just so we can make it look the same.</p>
<p>And then we're going to have a phone number.</p>
<p>Well, this is actually a text input as we know, because we changed that to be a text input.</p>
<p>So I'm just going to put phone number like  </p>
<p>that.  </p>
<p>And it's also an avatar image.</p>
<p>Well, this is just going to be a URL to something on the internet, as we mentioned, for now, of course, you don't have to have it like that, you can link this up to an AWS database if you wish in order to store images.</p>
<p>Next, we need to have a checkbox to allow us to check if someone's on call or not.</p>
<p>Okay, so let's go ahead and put on call here, and then someone can choose to select that or not select that, that'll be up to them.</p>
<p>We also have a date picker.</p>
<p>So I'm going to go ahead and just put in  </p>
<p>this date picker right here.  </p>
<p>And by default, that will show you today's date, which you can then change.</p>
<p>And I'm going to go ahead and change this label to start dates.  </p>
<p>And let's make an end date, one, two, so end date end date of each shift.</p>
<p>And finally, we're also going to have a select by a multiple select, okay, because we can pick multiple  </p>
<p>operations assigned to one user.</p>
<p>So I'm just going to change this to operations.</p>
<p>And let's Harker the options, the first one is going to be email ops, escalation,  </p>
<p>just like that.</p>
<p>The second one is going to be security ops, escalation.  </p>
<p>And the third one is going to be DevOps escalation.</p>
<p>So I'm just going to put dev ops, escalation just like that.</p>
<p>Okay, so now people can select multiple ones, which is quite cool.</p>
<p>And then finally, let's have a submit button.</p>
<p>So I'm just gonna get rid of that and find a button so that people can submit these answers.</p>
<p>So there we go.</p>
<p>And it's changes to say, submit.</p>
<p>So wonderful, I think that is looking good.</p>
<p>Now let's get to writing a query for this.</p>
<p>So this is going to be add team member.</p>
<p>So let's change this say add team member, just make sure that's the correct resource.</p>
<p>And then we are also going to insert into so this is where our playground comes in handy, we're going to insert into team, it's going to grab all of that  </p>
<p>team member, just paste it in uncomment that out and just change the values.</p>
<p>So the value of this will be whatever the number input two is.</p>
<p>So let's grab the number input to value.</p>
<p>This will be text input two values, so just get rid of that input, text input two value, and then we have text input three value.</p>
<p>So text input three value  </p>
<p>will also have email one, so get rid of that string, input email, one value.  </p>
<p>We also then have the phone number, which is text input four.</p>
<p>So let's go ahead and put text input for value.</p>
<p>Then we have the avatar, right, which will be text input five value.</p>
<p>So let's get rid of that text input five value.</p>
<p>And then we have a checkbox.</p>
<p>So the checkbox is checkbox one and then we update one do  </p>
<p>To add multi select one, so that's really easy to remember, check box value one, this will be date one.  </p>
<p>So date, one value, this will be date to value.  </p>
<p>And this will be the multi select options or multi, select one value.</p>
<p>Great.</p>
<p>So now let's save this, I'm actually going to also add some things to do on success because on success, we want to, essentially we get the team, right, so that will happen onsuccess.</p>
<p>And then let's also have some confetti, because why not, let's also close the modal.</p>
<p>So we're going to this time control a component and find the second modal that we made.</p>
<p>So just scroll down for modal two, and then we want to close it.</p>
<p>Okay.</p>
<p>So that's what we want to do on success.</p>
<p>So now I'm going to save that and hook up the Submit button to run that query.</p>
<p>So let's just add an event handler, control query add team member.</p>
<p>So wonderful, let's try it out.</p>
<p>I'm going to put employee to to I'm going to put Beau Carnes and as an email, I'm just gonna go well, at Free Code camp.org.</p>
<p>And as a phone number, I'm just going to make this one up.</p>
<p>Okay, just like that.  </p>
<p>And as an avatar image, I'm just going to get one off the internet Akbar.  </p>
<p>Okay, so I'm going to copy the image address.  </p>
<p>And just paste that in, he's not going to be on call.</p>
<p>And let's put his start date as the 11th.</p>
<p>And the end date as the 14th.</p>
<p>And the operations he's going to be assigned to?  </p>
<p>Well, let's have a look.  </p>
<p>I think there's just an email ops, and security DevOps as well.</p>
<p>So now let's hit submit.  </p>
<p>And great bow has been added.</p>
<p>This is looking wonderful.  </p>
<p>And finally, let's add a way to actually delete a team member.</p>
<p>So I'm going to actually do is write a new query.</p>
<p>So let's add a new query resource query, this is going to be delete team number.  </p>
<p>Make sure the resource is correct.</p>
<p>And we're going to delete from  </p>
<p>team  </p>
<p>where the ID equals the team table selected row data ID, right, because we're here, if we're selecting this one, and we want to delete it, we just want to make sure that it's the correct ID.</p>
<p>And that's how we're going to delete that team member.</p>
<p>So on success, we're, of course going to have to rerun the query to get all team members.</p>
<p>So get team and save.</p>
<p>And now we just need to hook up this button.</p>
<p>So let's find that button.</p>
<p>And on click, we want to run a query.</p>
<p>And that query is to delete a team member.  </p>
<p>Okay, so there we go.</p>
<p>And Shall we try it out?  </p>
<p>Let's do it.</p>
<p>I'm going to delete this team member.  </p>
<p>And wonderful, so that we have it.</p>
<p>And I'm just going to Riad him.</p>
<p>Luckily, all of this is still here, as I have not cleared that.</p>
<p>I've done that on purpose, just so it's easy for us for this tutorial, I'm just going to hit submit.</p>
<p>So there's both again.  </p>
<p>Now for the email composer, well, it'll be quite nice to have whoever I'm highlighting, have their first name show up.</p>
<p>So that's what I'm going to do.</p>
<p>I'm going to you once again, use the team table, select your data to get the first name of that person.</p>
<p>Okay, so there we go.</p>
<p>It says Hi, Bo.</p>
<p>So this is the default value of our whole rich text editor and put a comma there too.</p>
<p>And here as well, we can show who was sending this to.</p>
<p>So I can use team table selected or data came out this time.</p>
<p>So great.</p>
<p>At the moment, it's showing Bo, the device selects myself, of course, this updates, but this updates to and then I can write an email and send it but of course for that we need to hook this up.</p>
<p>Well, I'm also going to do though, is whenever we send an email, I'm going to also send a text notification.</p>
<p>So that's two resources we need to add.</p>
<p>Let's go ahead and do it.</p>
<p>So I'm going to go to Resources.  </p>
<p>And let's just hook up our Twilio that  </p>
<p>We signed up to before.</p>
<p>So please go ahead and find Twilio on here.  </p>
<p>That is Twilio.</p>
<p>And I'm going to do  </p>
<p>and is  </p>
<p>Twilio.  </p>
<p>Let's get the s ID account.</p>
<p>So here is my account s ID.  </p>
<p>Let's get the auth token.</p>
<p>So here is my auth token, I will be deleting this.</p>
<p>So please don't think that it will still work.</p>
<p>If you use it.  </p>
<p>I'm just going to select this as it's not allowing me to update the IP addresses.</p>
<p>And let's test the connection.</p>
<p>So that connection is successful.</p>
<p>And let's create a resource.</p>
<p>Wonderful.</p>
<p>So that is now done, I'm going to save these changes.</p>
<p>And let's go back to resources.</p>
<p>And this time, I'm going to create another resource.</p>
<p>This time it's going to be for emailing, I'm just going to scroll down to a previous one that I've made and reconfigure it.</p>
<p>So here are the settings that you need.</p>
<p>However, you do need to have a Google workspace, which is a paid workspace for this to work now.</p>
<p>So my Google workspace credentials are Ania at code with ania.com.</p>
<p>And then I'm just going to put in my password for that email.</p>
<p>Okay, so that's what you need to do, let's test the connection.</p>
<p>And then we're just going to have to allow less secure apps.</p>
<p>So just go to your admin console.</p>
<p>And I'm just going to search for a way to control access to less secure apps.</p>
<p>Okay, so that's here, we go to settings.</p>
<p>And then we go to access and data control and less secure apps.  </p>
<p>And I'm going to allow users to manage the less secure apps.</p>
<p>Great.</p>
<p>So that's for all users in my company.  </p>
<p>And that connection is a success.</p>
<p>So there we go, we have done it.</p>
<p>Great.</p>
<p>Now let's go back to our app is going to get rid of that.</p>
<p>So now let's hook this up, I'm just going to create a new query.</p>
<p>So let's create a new query resource query, this is going to be to send email.  </p>
<p>And this time the resource Well, it's going to be Gmail.  </p>
<p>And the from email  </p>
<p>is just going to be from Ania at  </p>
<p>code with  </p>
<p>ania.com.  </p>
<p>And the to email is essentially going to be whatever we are selecting right, so the selected row.</p>
<p>So let's go into the team table, select a row data email,  </p>
<p>where you can have a BBC or a CC, I'm not going to.</p>
<p>And this is just going to say email from pager duty clone.  </p>
<p>And the body.</p>
<p>Well, it's just going to be the body of our rich text editor.</p>
<p>So we're going to go into rich text editor one and get the value.</p>
<p>Okay, great.  </p>
<p>And onsuccess.</p>
<p>Let's also just have some confetti, and save that.</p>
<p>And now let's hook it up to the Form button.</p>
<p>So event handler on click, we're going to control the query sent email, because that is the query that we are on now.</p>
<p>So it's picked that up.</p>
<p>Another thing I want to do is actually just sent a text message that we got an email.</p>
<p>So there's gonna be another resource query send text.</p>
<p>And this time, this is going to be the resource Twilio that we just made.</p>
<p>So and use Twilio.</p>
<p>Okay, so that's been pre configured for me.</p>
<p>And this is the operation we need to do.</p>
<p>It's a post request, this is the endpoint and it's populated my account Sid, that is good, too.</p>
<p>Well, who we're going to be sending this to.</p>
<p>I'm going to put in my own phone number in a bit to test this out.</p>
<p>But of course, usually you would go into the team table selected row data and get the phone number right.</p>
<p>So just like that, so let's go ahead and select someone.</p>
<p>So I am selected so that should be bringing up my phone number.  </p>
<p>The body I'm just going to put you have an email notification from pager duty clan, and from well from is going to be my Twilio phone number which is actually in fact this you can get it from Twilio as well.</p>
<p>If you don't have a from here, please go ahead and add a parameter by clicking here.</p>
<p>Okay, great.</p>
<p>So that's what we want to do and let's just add some confetti for when this saves  </p>
<p>And I'm just going to attach that to this bottom two.</p>
<p>So send a text trigger.</p>
<p>Okay, so both those things will happen.  </p>
<p>Now,  </p>
<p>because of course, this number doesn't really exist, I am going to put in my phone number.</p>
<p>So please look away.  </p>
<p>And let's say this untested out.  </p>
<p>And great.</p>
<p>That has worked, I should have received a text.</p>
<p>So let's check it out.</p>
<p>Well, first off, I'm just going to run this and see if that has worked.</p>
<p>And let's see bug.</p>
<p>Okay, just to make sure that you have your email the same one as you are using in the app.</p>
<p>And that should work.</p>
<p>Let's check the Gmail account.</p>
<p>So I'm just going to sign in and check out if I got an email.</p>
<p>And I did.</p>
<p>Okay, here's my email.</p>
<p>Of course, it does come as HTML.</p>
<p>So we're probably gonna have to format that a little bit better.</p>
<p>But at least it's sending something.</p>
<p>So there we have it.</p>
<p>We have completed our pager duty app.</p>
<p>Here is in all its glory.</p>
<p>It works.</p>
<p>The Twilio API works.</p>
<p>The SMTP API works.</p>
<p>We are using a Postgres database, we can add incidents, team members, delete team members, delete incidents.</p>
<p>This is looking glorious.</p>
<p>I hope this tutorial was useful.</p>
<p>And I hope you had fun while learning and I'll see you soon</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Add Websocket Real Time Communication to Your SaaS App ]]>
                </title>
                <description>
                    <![CDATA[ In this conference talk made for Tech Fest 2021, Steven Lemmo talks to you about building SaaS Applications with WebSockets. Here is a full breakdown of what he will cover in this 40 minute talk: The reason for this talk Why Websockets? Websockets m... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-add-websocket-real-time-communication-to-your-saas-app/</link>
                <guid isPermaLink="false">66b0a8abb30dd4d00547bbeb</guid>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ websocket ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ania kubow ]]>
                </dc:creator>
                <pubDate>Wed, 24 Nov 2021 23:50:04 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/11/websockets.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this conference talk made for Tech Fest 2021, Steven Lemmo talks to you about building SaaS Applications with WebSockets.</p>
<p>Here is a full breakdown of what he will cover in this 40 minute talk:</p>
<ul>
<li>The reason for this talk</li>
<li>Why Websockets?</li>
<li>Websockets minimal API example</li>
<li>Rest Web Services</li>
<li>Multiple Listeners</li>
<li>Full Duplex</li>
<li>Websockets time-line</li>
<li>TypeScript</li>
<li>Vendor Tools</li>
<li>Auction IDL Definition</li>
</ul>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/K2O7RyU9clc" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p>This article was written by Ania Kubow in support of the conference talk made by Steven Lemmo.</p>
<figure><a class="kg-bookmark-container" href="https://www.youtube.com/channel/UC5DNytAJ6_FISueUfzZCVsw"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Code with Ania Kubów</div><div class="kg-bookmark-description">Hello everyone. This channel is run by Ania Kubow. In this channel, I will be teaching you JavaScript,React, HTML, CSS, React-native, Node.js and so much more! A little bit about me:My background is in the financial markets, where I worked as a derivates broker our of University. After starting m…</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://www.youtube.com/s/desktop/6b151e52/img/favicon_144.png" width="144" height="144" alt="favicon_144" loading="lazy"><span class="kg-bookmark-publisher">YouTube</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://yt3.ggpht.com/ytc/AAUvwnjSRt8sIbeM7P--pHoUDh67sDhaNTCMF_XiNOCvUw=s900-c-k-c0x00ffffff-no-rj" width="900" height="900" alt="AAUvwnjSRt8sIbeM7P--pHoUDh67sDhaNTCMF_XiNOCvUw=s900-c-k-c0x00ffffff-no-rj" loading="lazy"></div></a></figure>

 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How I Went from Hackathons to CTO of a 20 Person SaaS Company in 3 Years ]]>
                </title>
                <description>
                    <![CDATA[ By Yacine Mahdid In this article I will share the story of how I became CTO of a software as a service (SaaS) company. It all started about 3 years ago when I was going to hackathons for fun.  At the end of the article you can find some ]]>
                </description>
                <link>https://www.freecodecamp.org/news/from-hackathon-to-cto-in-3-years/</link>
                <guid isPermaLink="false">66d46171b3016bf139028d9a</guid>
                
                    <category>
                        <![CDATA[ Entrepreneurship ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hackathons ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Personal growth   ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ self-improvement  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[  Startup Lessons ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 11 Feb 2021 19:30:01 +0000</pubDate>
                <media:content url="https://cdn-media-2.freecodecamp.org/w1280/601702fe0a2838549dcbc125.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Yacine Mahdid</p>
<p>In this article I will share the story of how I became CTO of a software as a service (SaaS) company. It all started about 3 years ago when I was going to hackathons for fun. </p>
<p>At the end of the article you can find some tips and advice I would give to aspiring entrepreneurs as well as some reading recommendations.</p>
<p>It was – and still – is a wild ride!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-8.png" alt="The GRAD4 team in 2020" width="600" height="400" loading="lazy">
<em>Our company team (we now have 8 more employees!)</em></p>
<p>Although I have a good knowledge of entrepreneurship, don't just take my advice at face value. Learn from books, from other entrepreneurs, and from your environment in order to triangulate how best to act in a given situation.</p>
<h2 id="heading-my-first-hackathon-which-i-lost">My First Hackathon – Which I Lost</h2>
<p>It might sound off, but I had no interest whatsoever in entrepreneurship or business 3 years ago. I was more of a researcher in spirit than an entrepreneur. </p>
<p>From studying <a target="_blank" href="https://onlinelibrary.wiley.com/doi/full/10.1111/jnc.14473">memory at the molecular level</a>, to <a target="_blank" href="https://www.frontiersin.org/articles/10.3389/fpsyg.2021.612681/full">helping schizophrenic patients learn better</a>, or <a target="_blank" href="https://pubmed.ncbi.nlm.nih.gov/33376599/">predicting consciousness recovery in patients with traumatic brain injuries</a>, my mind was deeply focused on the frontiers of science.</p>
<p>However, I had a deep passion for programming and for finding ways of building tools that didn't exist before. </p>
<p>Like the time I built a <a target="_blank" href="https://mjm.mcgill.ca/article/view/129">brain computer interface that was compatible with a Transcranial Direct Current Stimulation electroencephalographic headset</a> (that is, it stimulates the brain by giving shocks and lets you control a kind of ping pong game). I just love to build stuff.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/graphics.png" alt="A very simple brain computer interface with a white ball dropping down to a red or green tile" width="600" height="400" loading="lazy">
<em>Cutting edge graphic from my brain computer interface</em></p>
<p>So three years ago, my brother wanted to find a new job as a software developer. We figured out that we could attend hackathons to help him get noticed by recruiters. </p>
<p>It was my first hackathon so I was pretty excited to spend two days building something. Our team was composed of myself, my brother, his friend, and one more member (who didn't do a lot, but was still there).</p>
<p>It was two days of heavy coding, and at the end we came up with a pretty cool algorithm to pattern match people with jobs given some characteristics. </p>
<p>On top of that, I met nice people that, like me, love to build stuff. We ended up among the finalists and came in fourth during the final presentation. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/coding_away.jpg" alt="3 student working in a computer in a crowded space" width="600" height="400" loading="lazy">
<em>Here I am trying to figure out why the clustering is not working</em></p>
<h3 id="heading-what-i-learned-from-my-first-hackathon">What I learned From My First Hackathon</h3>
<p>It was a bit of a bummer not to win, but I realized something important then: except for the participants, no one really cared what I coded during these two days. </p>
<p>It was mostly the presentation of what we wanted to build that mattered. We should have spent a bit more time making a very compelling PowerPoint instead of focusing on the development of an algorithm!</p>
<p>It was a fun learning experience, but my brother didn't get a job out of it – so we were back at square 1.</p>
<h2 id="heading-my-second-hackathon-in-which-we-won-a-special-prize">My Second Hackathon – in Which We Won a Special Prize</h2>
<p>I was ready to take a small break from hackathons for a while. However, my brother decided to sign us up anyway. So his friend and I to go to another hackathon right after the first one. I would have refused as I had other stuff to do, but he had already paid the sign up fees.</p>
<p>This hackathon was a bit bigger than the previous one and there were still a bunch of recruiters there. We figure that this could be our second shot at getting him a job. </p>
<p>The theme of this hackathon was open data and environment. So we were in a good position to make something cool with my background in machine learning.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/hackqc.jpg" alt="People waiting in a room for a speech at HackQC" width="600" height="400" loading="lazy">
<em>A lot more people where present at HackQC which was nice!</em></p>
<p>Knowing that the presentation was everything, we decided to be a bit more strategic with this hackathon. We combed all the different prizes we could win and selected the one with seemingly the least competition. The project would deal with the prediction of combined sewage overflow. </p>
<p>We then split up our team of four (another friend of my brother's joined in) so that half of us were working on the coding and the other half on the presentation.</p>
<p>The name of our team was “Égout Quebec” which translates to Sewage Quebec (branding wasn’t our strength). Here is the excerpt from our <a target="_blank" href="https://devpost.com/software/debordementhackqc18">competition log on DevPost</a> in French:</p>
<blockquote>
<p>ÉGOUT QUEBEC permet de prédire le débordement des égouts et d’en avertir les amateurs d’activités aquatiques. Ainsi les personnes désirant aller faire des activités aquatiques pourrons éviter les zones polluées par les déversements d’eau usée. De plus, des conseils seront dispensés aux gens des différents quartier, de façon à réduire les risques de débordement.   </p>
<p>ÉGOUT QUEBEC utilise une technologie basée sur une intelligence artificielle. Celle-ci peut déterminer lorsque les égouts déborderont. Ses analyses et calculs sont basés sur des données disponibles sur le site de donnée Québec. Finalement, les différentes villes auront avantage à utiliser ÉGOUT QUEBEC.   </p>
<p>Grâce aux prédictions et aux analyses de la plateforme, il sera possible de concentrer les ressources de la ville aux endroits les plus problématique.</p>
</blockquote>
<p>All this basically translates to <em>“we found a way to solve the problem using a random forest + a bunch of data”</em>. </p>
<p>We actually ended up winning a $1000 prize, which was huge at that time! We even got a nice glass trophy in the shape of a water droplet. It was less challenging than the previous hackathon because we had a better plan for what we needed to do. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/winner.png" alt="Four logo of the project receiving a prize" width="600" height="400" loading="lazy">
<em>Here we are at the podium (still technically fourth place though).</em></p>
<h3 id="heading-what-i-learned-from-my-second-hackathon">What I learned From My Second Hackathon</h3>
<p>This hackathon made me realize that presentation was indeed important. But I also discovered that focusing on problems that people don't find interesting is a good way of increasing your chance of coming out on top!</p>
<p>Winning felt good and I was ready to take my share of the $1000 prize and call it a day...</p>
<h2 id="heading-my-third-and-last-hackathon-which-we-kinda-lost-again">My Third (and Last) Hackathon – Which We Kinda Lost Again</h2>
<p>However, the $1000 prize came with an automatic fast-track to the semi-final of yet another hackathon! This one had higher stakes with a $25,000 prize. </p>
<p>We met up at a coffee shop to figure out if we wanted to split up the money or to use it in order to take our project to the next level.</p>
<p>After looking at the other participants' projects and the rules of the competition, we decided that we actually had a good shot to at least get to the finals. This would unlock some more funding which we could use at will and would give us a nice trip in the summer to a lake in Ontario.</p>
<p>This hackathon wasn't like the others in the sense that it ran for the whole year. It was more of a take-home hackathon, which was great for me since I didn't have to do that insane two-day time crunch routine over a weekend.</p>
<p>We changed our name to EGC Labs and managed to get a website together to advertise our sewer overflow prediction solution (which was working surprisingly well). We then went to the semi-finals competition in Ontario, and to our amazement we moved on to the final!</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-16.png" alt="People learning they moved to the semi-final of Aquahacking" width="600" height="400" loading="lazy">
<em>Here we are as finalists! I didn't go all the way to Ontario for the semi-finals, though.</em></p>
<p>This was great! We had a very good shot at winning more than a small stipend, and maybe starting a business out of this thing. To help us bootstrap ourselves, Aquahacking generously gave us $2000 and École de technologie supérieure (ÉTS) gave us $1000 for travel expanses.</p>
<p>However, we still didn't have any customers for our solution, which was a bit problematic. </p>
<p>We tried to contact a bunch of cities in Quebec in order to see if they would be interested in our sewage overflow application. We had some pretty advanced discussions with a few cities, however their process was so slow that we got very near to the finals without any concrete validation. </p>
<p>The final was a 5 minute presentation in front of a big audience with some investors present.</p>
<p>Things got a bit more tricky when we got closer and closer to the presentation date. The rest of my team started to become less and less responsive. I ended up not being able to reach my brother's friend who had joined at the HackQC competition. </p>
<p>This quickly followed by my brother losing all interest in the competition and focusing on other stuff. We were left with myself and my brother's other friend (Félix) who joined in the very beginning. </p>
<p>He was also starting to slip out of the picture because nothing seemed to be moving forward, as I was the only one left coding. I wasn't very interested in that project either, however one thing I hate is half-backed projects (even more when they are public).</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-17.png" alt="The scene at Aquahacking with Felix presenting and Yacine doing nothing" width="600" height="400" loading="lazy">
<em>Our faces were plastered everywhere</em></p>
<p>I felt that it would have been a waste to stop so close to a conclusion. So I took it upon myself to jump start the project by coding the app's user interface, getting some branding going, and creating our business cards for the event. </p>
<p>As things started to pick up Félix started to become more and more involved (which was a blessing because he was the one pitching!).</p>
<p>We were still finalizing the PowerPoint the night before the presentation in the hotel room. After all this hard work we had something pretty solid (by my standards at the time, anyway). We've ended up winning fourth place in the competition:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-9.png" alt="Yacine and Felix receiving the fourth prize at Aquahacking" width="600" height="400" loading="lazy">
<em>Us receiving the 4th place trophy (again) with no money whatsoever associated with it haha</em></p>
<p>We went from a team of 4 to 2 people and were able to get that far. However, it was exhausting since I had to do so many different things that were not in my core zone of competency. </p>
<h3 id="heading-what-i-learned-from-my-last-hackathon">What I learned From My Last Hackathon</h3>
<p>I learned two very important lessons during that adventure: </p>
<ul>
<li>Having motivated teammates is the most important asset in any endeavor. If we had everyone pulling their weight we would have had more chance to win!</li>
<li>For a startup, having clients is the ultimate measure of success. We did everything right, except making sure that we had paying customers. In the end this is what kept us at 4th place.</li>
</ul>
<p>We won a big $0 in that part of the competition, and once again I thought that was it. I had coded some cool Flask applications, went to the final of a big hackathon, and learned some valuable lessons. Until I realized...</p>
<h2 id="heading-how-i-learned-about-entrepreneurship-at-an-incubator">How I Learned About Entrepreneurship at An Incubator</h2>
<p>Being part of the finalists of Aquahacking automatically gets you a spot in one of the provincial incubators for startups, which happened to be <a target="_blank" href="https://centech.co/">Centech</a> for us in Québec.</p>
<p>I didn't really know what an incubator was (except in a biological sense), so I wasn't particularly excited. We had to make a pitch for our "startup" to the incubator panels even though we had a reserved spot. I let Félix do it without me, as I had other stuff to focus on.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-18.png" alt="The inside of the Centech incubator" width="600" height="400" loading="lazy">
<em>Centech is inside an old planetarium so it has that funky circular shape. It's a pretty cool place.</em></p>
<p>At first I didn't go too often to the entrepreneurial classes and I was starting to think that this whole hackathon-thing was becoming a huge time-sink. However, I stuck to it because Félix was very motivated by the whole experience.</p>
<p>So I decided to give it a serious shot. I started attending the entrepreneurial classes (which were amazing), I did the homework, and I tried really hard to make our sewage company work. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/lotofpeople.jpg" alt="A lot of people in the stairs of Centech" width="600" height="400" loading="lazy">
<em>Lots of entrepreneurs in our 2018 cohort, try to find me!</em></p>
<p>Frankly speaking, it was amazing. It was genuinely fun to try and make something novel. And being mentored by people that had built incredible products that helped millions of people was incredible. </p>
<p>The class that I was in was also very motivating, as we had people from all backgrounds and oddly enough most of them were graduate students, like I was! </p>
<p>I really got the bug for entrepreneurship. It was like doing research, except I could directly see the results of my experiments in a matter of days. It was like combining the pleasure of discovery I had while doing an experiment with the freedom of programming for a side project.</p>
<p>However, after reading the books and learning the material, I realized that we'd done everything upside-down for our budding startup. We had built the technology before even validating that there was a sustainable business out of it (silly us!).</p>
<p>We were ready for our very first pivot, which in entrepreneurial jargon means tweaking your business in a significant way in order to not crash and burn.</p>
<h2 id="heading-how-we-made-our-first-pivot-after-learning-that-our-startup-idea-was-doomed">How We Made Our First Pivot After Learning that Our Startup Idea Was Doomed</h2>
<p>Our combined sewage overflow predictor was working great. However, no one was willing to spend money on it. </p>
<p>We were targeting cities in order for them to be able to proactively do something about the overflow of raw sewage into their rivers (which, by the way, is a worse and more frequent problem than you think). But these cities had such a long cycle to sell to that we would never be able to grow this business.</p>
<p>One thing that we realized during this project was that the majority of the data that was available for the competitions were in a very bad shape. It took us many hours of cleaning and we had to develop some very custom toolsets in order to make the cleaning efficient.</p>
<p>We had also talked to a few of the judges at Aquahacking and what they were most interested in was the portion of our business that related to data cleaning.</p>
<p>So that was it! We would become a data cleaning company. For about three weeks we were frenetically searching for customers, making plans on how to make this type of business work, and coding outlier detection algorithms.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-299.png" alt="Image" width="600" height="400" loading="lazy">
<em>This would have been us inside the datasets for the rest of our lives: Photo by [Unsplash](https://unsplash.com/@mkjr</em>?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit"&gt;mkjr</em> / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<p>However, after meeting with our mentors at Centech, we realized that what we were building was a service company. Almost no data is similar, especially when it comes from different sources. </p>
<p>The amount of domain expertise we would have to gain in order to be efficient in one industry would not necessarily transfer easily to another industry. There was a lot of manual work involved in this and all our ideas for making it more automated were failing.</p>
<p>Now, there is nothing wrong with making a service company. It's a great type of company if you are very interested in your industry. But we did some amount of introspection and we realized that we weren't really passionate about data cleaning. </p>
<p>We were passionate about automating work and improving efficiency through technology. Building a data cleaning company where most of the work would be manual and not very efficient wasn't very exciting for us. </p>
<p>We were ready to make our second pivot, which would be our largest so far.</p>
<h2 id="heading-how-we-made-our-second-pivot">How We Made Our Second Pivot</h2>
<p>During the Centech program we had met two other entrepreneurs that had a problem that was complementary to ours. They had a lot of validation from potential clients whose problem could be solved by technology. However, they didn't have the technical skills to make the technology required to power their business.</p>
<p>I had helped them out a couple of times in order to get them started with building their web application since I'd already done something similar for EGC Labs. </p>
<p>Helping out other entrepreneurs without expecting something in return is very common in this type of environment. The more I helped them, the more we realized that we could get way more done if we were a team of 4 instead of two teams of 2.</p>
<p>We went for lunch to discuss what a potential "merger" of our two ideas would look like. After discussing for a week, we decided to ditch the EGC Labs project completely, as it had the smallest chance of success. Instead we would join forces with their idea, which was called GRAD4.</p>
<p>That was the second pivot for Félix and me. It meant completely dropping a project that was leading nowhere and joining another one that we were much more excited about.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/pitching.jpg" alt="the CEO of the company GRAD4 presenting at some pitch competition" width="600" height="400" loading="lazy">
<em>Félix pitching our new company instead of the data cleaning one</em></p>
<p>During the next weeks, we were absolutely crushing it. We did everything right. We officially incorporated our company. We made a few plans as to what the actual application would need in terms of functionality so it would deliver value to our potential customers. However, we didn't wait for me to complete it to start selling it!</p>
<p>The other two co-founders were already on the road talking to people who'd showed interested. They were gathering checks for a 1 year subscription to our platform, which didn't exist yet. The $500 yearly subscription would start when we launched the product and we were very upfront that we were in the process of building it. </p>
<p>Getting people interested in your project is one thing, but getting paying customers before building a SaaS app is the holy grail of business validation. By doing so we were able to get about 15 checks that would help finance the development of the product. </p>
<p>We also won a $1500 elevator pitch competition around the same time because Félix was very focused on practicing his pitch.</p>
<p>All of these small wins compounded during the program and we ended winning a $15,000 prize at the end of the incubator called the Unicorn prize. </p>
<p>This was great, but more importantly we secured a spot in the next stage of the incubator which was the Propulsion program. This would secure us an office and some additional perks that would help our company succeed.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/office_1.jpg" alt="The first office of the company GRAD4 in a basement" width="600" height="400" loading="lazy">
<em>Cozy office in the basement of Centech. It was nice, but there was lots of noise because of pipes. You can see a wild cofounder working through the night.</em></p>
<h3 id="heading-what-i-learned-from-my-first-startup-incubator">What I Learned From My First Startup Incubator</h3>
<p>During that part of our young company's adventure I learned a few important lessons:</p>
<ul>
<li>Clinging onto an idea because it was yours even though all analysis tells you it's a bad one is usually a bad idea.</li>
<li>A larger founding team is more productive than a smaller one. It also allows for cheaper labor as no one is getting paid at the start.</li>
<li>Asking customers to pay for an in-progress idea is not as difficult as you may think. If the pain is big enough for the customers, they are usually very supportive of having someone fix that mess. Even if the probability of success is low.</li>
<li>The technology part of the business is not that important.</li>
</ul>
<h3 id="heading-we-have-money-now-what">We Have Money, Now What?</h3>
<p>We now had gathered a fair amount of money and we had multiple options in front of us:</p>
<ul>
<li>Either we keep the money and pay ourselves.</li>
<li>We keep the money and don't use it in case we fail to make the app and we need to reimburse people.</li>
<li>We use the money to hire people to speed up the development.</li>
</ul>
<p>The first idea was wasteful, and it would decrease the probability that our company would succeed. The second option would just let our funds sit idle. So we decided to go for the third option.</p>
<h2 id="heading-how-we-scaled-our-startup-past-the-founding-team">How We Scaled Our Startup Past the Founding Team</h2>
<p>During that summer of 2019, we made our first two official hires to help me with the development of the platform. They were software engineering interns from the ÉTS (École de Technologie Supérieure). One intern would be focused on the backend side while the other would be focused on the frontend side.</p>
<p>Our product was a simple CRUD application that would allow buyers who needed metal parts manufactured and suppliers of metal parts to find each other. </p>
<p>It was basically a sort of marketplace where buyers would create what is called a <em>request for quote</em> and the suppliers would create a <em>quote</em> to say how much they could build the part for. Pretty simple!</p>
<p>The technology we choose to use was the following:</p>
<ul>
<li>Django + Django REST API for the backend.</li>
<li>React + Redux for the frontend.</li>
<li>Bootstrap for the styling.</li>
<li>Heroku to host both applications.</li>
<li>GitHub for remote source control.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-19.png" alt="A picture of a development stack feature Django and React" width="600" height="400" loading="lazy">
<em>This was our setup at first. Credit: https://blog.usejournal.com/react-on-django-getting-started-f30de8d23504</em></p>
<p>Choosing the technology was up to me as the CTO, and I honestly decided to go with what I thought would be better in the long run. I was already familiar with Flask, however I knew a bit of Django too. Seeing that a lot of the functionalities I needed were already pre-built into apps made me lean toward it.</p>
<p>I chose React on the frontend side because I had played around with it 6 months prior and found that it was such an easier way to build applications than the traditional way I was used to.</p>
<p>However, in retrospect I think I would have greatly simplified the stack. I would have only used Flask for the following reasons:</p>
<ul>
<li>I was more familiar with Flask than with Django.</li>
<li>We didn't need to have a separate frontend, as the application we needed to build was very simple. Simple templates would have been enough for a first proof of concept.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-20.png" alt="Flask Logo and Title" width="600" height="400" loading="lazy">
<em>Just this would have been more than enough</em></p>
<p>However, we went with this tech and we learned a great deal in the process.</p>
<h3 id="heading-what-i-learned-during-my-first-months-as-cto">What I Learned During My First Months as CTO</h3>
<p>One lesson I learned from this first foray into making an application customers would actually use is that overthinking scalability is oftentimes useless. </p>
<p>Working with what you are already comfortable with and delivering something as soon as possible is much more useful as you learn more rapidly. This is actually what is most important in a startup. </p>
<p>The more learning you can do (about the business, what customers want, how to talk to them) the more probable it is that the next thing you try will work.</p>
<p>After a few iterations, we were finally ready to launch our closed beta with the customers that had already paid for our service.</p>
<h2 id="heading-how-we-did-our-first-product-launch-closed-beta">How We Did Our First Product Launch (Closed Beta)</h2>
<p>We were able to make the first version of the application at the end of the summer of 2019 and launched it for our users (1 month later than we promised). It wasn't pretty and it was barely working. </p>
<p>We had to babysit our users throughout the whole process and the buyer section of the application wasn't usable. We had to do the work manually for all our buyers while the suppliers were able to create quotes. </p>
<p>What was great, though, was that we continued to be able to get checks from people who were interested which helped fuel development. We made a few mistakes on the hiring side, though. We hired a friend of one of the founders who, although experienced, was a jerk to the other more junior developers. </p>
<p>This was the first firing I had to do, and I'm glad I did it. Creating an enjoyable work environment is much more important than technical prowess, because at the end no one wants to work in a bad environment for long. </p>
<p>Having people stick around for the long run in a startup environment is crucial for the company's success. I believe it is one of the reasons our startup is still alive. </p>
<p>As development was chugging along we realized that we had to structure the company a bit more than what we were doing. Thanks to being next to so many startups and successful companies at Centech, we could learn from each other. </p>
<p>During that year, we focused on getting financing going (this is a super important part of business and should be prioritized). We also focused on getting some sort of marketing going, making actual designs that made sense instead of winging it, and making sure our team was staffed by full-time people instead of only interns.</p>
<p>We also moved from the basement of Centech to an office space with windows on the second floor which was actually pretty nice! We maxed out the capacity pretty quickly, though, and the founders had to start working in the dining area of the building so that the employees had more room.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/lotofpeopleagain.jpg" alt="A very very crowded office with the GRAD4 team" width="600" height="400" loading="lazy">
<em>This was way too crowded in retrospect, but at least the view was nice</em></p>
<h3 id="heading-what-i-learned-from-my-first-launch">What I Learned From My First Launch</h3>
<p>I learned a great many lessons after our first launch:</p>
<p><strong>Don't do an official launch.</strong> It's useless. We put so much pressure upon ourselves to impress the paying customer with a pompous launch, and we were afraid that they would be mad at us if we didn't deliver.   </p>
<p>The truth is they didn't really care. When we sent an apology email saying that we had to delay the launch no one complained. I'm pretty sure they were actually amazed that we were able to get something up and running so quickly.</p>
<p><strong>Even if you have paying customers for your SaaS application, prioritize doing manual work over building an application</strong>. In retrospect, I would have made a simple form where the buyer could upload a Zip file containing the request for a quote which would be sent to the founders' email address.   </p>
<p>We would then send out this request manually to the manufacturers we knew would be interested in this type of work. That would have delivered value way faster to our users and released the pressure on development.   </p>
<p>It would also have validated a lot of hypothesis we had that would have accelerated the development work.</p>
<p><strong>Stay focused on very few things and make sure that this focus is explicitly written down somewhere.</strong> At some point I was building a blockchain-powered smart request for quote because it was hyped up by one of the co-founders who knew someone working with blockchain technology. That was a solid waste of time and I'm glad I killed that project soon after.</p>
<p>We were starting to pick up steam and we finally had something that we could show the world. It still wasn't pretty, however customers saw the value in what we were building and we knew better what was creating value and what was not. </p>
<p>We were ready to open our beta to increase the amount of users in our application!</p>
<h2 id="heading-how-we-opened-up-our-product-to-more-users">How We Opened Up Our Product to More Users</h2>
<p>After tweaking the platform using feedback from our early users, we now had a better understanding of what exactly we needed to build. We were now ready to increase the amount of people using our platform and start to increase the output of the sales (which the founders did).</p>
<p>This put more pressure on our application and we started to experience downtime. We knew that we had to improve the way we were building this product if we wanted to scale to more users. </p>
<p>We asked an experienced software architect, now part of the team full time (thanks <a target="_blank" href="https://www.linkedin.com/in/karimbesbes/">Karim</a>!), to help us out. This really gave us a solid direction to follow. Here is what we changed:</p>
<ul>
<li>We moved to AWS to have more control over the cloud environment.</li>
<li>We moved to Gitlab to have an easier-to-use CI/CD environment.</li>
<li>We added automated testing to our application.</li>
<li>We started to migrate toward Material-UI instead of Bootstrap.</li>
</ul>
<p>After improving the way we worked, we now had a fully working CI/CD setup and a more robust application that was rigorously tested. The amount of downtime we experienced was drastically reduced and developers were more confident about the changes they were making. It was a more enjoyable development experience, too.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-21.png" alt="Gitlab CI CD pipeline" width="600" height="400" loading="lazy">
<em>Gitlab is a very enjoyable work environment and reduces a lot of the complexities with lots of tooling. Credit: https://about.gitlab.com/</em></p>
<h3 id="heading-what-i-learned-from-my-first-open-beta">What I Learned From My First Open Beta</h3>
<p>During that period, I learned that the minimal viable product (MVP) phase of the product is necessary. But when transitioning out of it you shouldn't hesitate to ask for expert help.</p>
<p>We now had sales giving us more clients (which were still a bit difficult to get, but coming along at a constant pace) and the application provided more value than before.</p>
<p>We were ready to ask for our first real investment!</p>
<h2 id="heading-how-we-got-our-first-real-investment">How We Got Our First Real Investment</h2>
<p>We applied for a funding from <em><a target="_blank" href="https://blog.frontrow.ventures/">Front Row Ventures</a></em> which is a venture capital fund entirely managed by students and which only invests in student-led startups across Canada. </p>
<p>We met with the people from Front Row, and explained what we were doing and where we were going with all of this.</p>
<p>We ended up having to do a 25 minute pitch in front of a full panel of students who were asking solid questions about the business and the technology. </p>
<p>This was great because we knew that if we got the funding we would not only have the cash, but also new connections that could be there to answer questions and make strategic introductions for us.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-22.png" alt="The team of 20 people from Front Row Venture" width="600" height="400" loading="lazy">
<em>It was great, some of these people went to the same university as I did!</em></p>
<p>Well, <a target="_blank" href="https://blog.frontrow.ventures/behind-the-deal-grad4-4f0e92b2a547">we ended up getting the funding and the people at Front Row were very helpful in many aspects of the company</a>! </p>
<h3 id="heading-what-i-learned-from-getting-our-first-funding">What I Learned From Getting Our First Funding</h3>
<p>This was another important lesson I learned during that time: when choosing a funding partner, it's not only about the funding. It's also about how much help they can provide. </p>
<p>Front Row ended up opening many more doors for us and ensured that their investment had all the possible tools to succeed.</p>
<h2 id="heading-what-our-day-to-day-routine-looked-like">What Our Day to Day Routine Looked Like</h2>
<p>Everything was going pretty well for us on the business side. However, the pace we were following was a bit worrisome. We were an archetypal startup where employees would come at the office from 9 to 5 and where the founders worked 80 hours a week. </p>
<p>Meeting on the weekends at the office to plan stuff out and clear up more tasks was routine. </p>
<p>I wasn't too happy with this because this type of work isn't sustainable in the long run – and I started to see signs of burn out in the others. We also weren't very structured, the documentation was poor, and we had almost no processes clearly mapped. </p>
<p>It was at that time that I started to read more about how to structure a company properly in order to maximize our chances of success (see readings at the end).</p>
<p>Then, out of nowhere, we started to hear about the possibility of a pandemic hitting our Canadian shores...</p>
<h2 id="heading-how-we-reorganized-when-covid-19-hit">How We Reorganized When COVID-19 Hit</h2>
<p>Some employees were very afraid of the virus and of getting sick. There were no cases in Canada yet, but some of the employees had family abroad in areas more advanced in the pandemic's course. </p>
<p>We started to think about what we could do and we realized that we didn't absolutely need to have people come in the office to do their work. All of the work could be done remotely just fine – it was just the culture that we'd set up that required people to show up from 9 to 5 every day of the week. </p>
<p>This type of schedule was directly taken from what we were all familiar with, but we realized that we didn't have to do like all the other companies we'd worked for. </p>
<p>We decided that we would allow everyone that wasn't comfortable going into the office to stay home. I personally decided to not show up to the office and most of our employees followed suit.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-300.png" alt="Image" width="600" height="400" loading="lazy">
_As long as people had a the tools they needed, they didn't need to go into the office: Photo by [Unsplash](https://unsplash.com/@xps?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;XPS / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<p>This was great because we soon realized that a lot of the shortcomings of our company's structure were hidden by the fact that all employees were available in the office every day. Everyone was always trying to setup a Zoom meeting at random hours or calling at any time of the day to ask for operational tasks status. </p>
<p>We also realized that people had a hard time finding where all the information was, and many different people asked the same thing many times.</p>
<p>I decided to read a bit more about how to structure remote work efficiently so we read the <a target="_blank" href="https://about.gitlab.com/resources/ebook-remote-playbook/">remote playbook from Gitlab</a> and implemented some changes:</p>
<ul>
<li>We removed the silos between the different teams by making all communication public and via written messages.</li>
<li>We organized the documentation so that people would put their files in a shared drive instead of sending them on the instant messaging app.</li>
<li>We instated a Kanban methodology for all departments, not just the people working on the technology since tasks weren't properly tracked.</li>
<li>We reduced synchronous meetings by only scheduling those that were necessary and by having the results of the meetings communicated in one form or another.</li>
</ul>
<p>This helped a lot! However, it took time before it really took hold. People were still having private messaging discussions when everyone could benefit from what was said. Synchronous meetings were still the default for a lot of people to share information. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/Executives.png" alt="The executive team of the company GRAD4" width="600" height="400" loading="lazy">
<em>Here are the co-founders trying to keep 2m distance in our first pandemic meetup (and photo shoot)</em></p>
<p>But it got better. By putting a lot of effort into making remote-work work, we were drastically improving our company's productivity to a level way above that pre-pandemic. </p>
<p>In a sense it was helpful that we were forced to experience remote work (as we were in full lockdown for a while) because it gave us the necessary buy-in from people that were skeptical that this was possible. </p>
<p>There was no other choice but to go all-in to remote work, otherwise the company would've just ground to a halt. We had employees to pay, so waiting the pandemic out wasn't an option.</p>
<p>In the business sense, we were very fortunate as our platform was useful for the manufacturing industry which couldn't do business in person anymore. </p>
<p>Some startups that we knew of weren't so lucky and their whole business was now impossible to operate within a very short time frame. Most of them had to close shop or do some radical pivot.</p>
<h3 id="heading-what-i-learned-from-pivoting-to-a-remote-company-structure">What I Learned From Pivoting to a Remote Company Structure</h3>
<p>The first few months of the pandemic taught me some very important lessons:</p>
<p><strong>Starting a startup is very, very, very risky</strong>. If you had started a ridesharing startup in 2019 and things were going fantastic for you, you would still have had to stop that business once the pandemic hit in 2020.   </p>
<p>No matter how prepared you are, there are always risks and unforeseen events happening on a daily basis.</p>
<p><strong>Working on the company is more valuable than working for the company as a founder</strong>. What I mean by that is spending time structuring the company and making adjustments to how people work in order to increase productivity is invaluable.   </p>
<p>You can't expect employees to do that on top of doing their regular work. It's up to the founders to set up a structure that make sense and to always be improving it.</p>
<p><strong>Remote work will improve your team's productivity compared to in person work (when possible) only if you take the time to make it work</strong>. Trying to reproduce an in-office way of working remotely will lead to an obvious decrease in productivity. Drastic changes needs to be made in order for this type of work to be useful and it requires a leap of faith.</p>
<p>We were still growing during that time as we were hiring more people to sustain our rapid pace. We also made a major pivot in how we were generating revenue by ditching our subscription-based model to a transaction fee model. This allowed our sales team to rapidly increase the amount of companies we enrolled in our application.</p>
<p>Our first version of the application started to show signs of not being optimally adapted given all the new information we'd collected from our customers. So we were working on revamping it with an improved design and user experience with a dedicated UI/UX team.</p>
<h2 id="heading-the-improved-version-of-the-application">The Improved Version of the Application</h2>
<p>We launched the revamped version of our app (this time without a hard deadline) and the reception was great. This is when I started to feel like we actually launched a real product and not a MVP to validate a need.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/platform.gif" alt="A screen with a 3D part moving in the GRAD4 Application" width="600" height="400" loading="lazy">
<em>You could now see the part directly on the platform (we've used <a target="_blank" href="https://xeogl.org/">xeogl</a> for that)!</em></p>
<p>The sales of our product increased dramatically as the user interface was great and the experience made sense to prospective users. We went on to do some more major refactoring and clearing up technical debt. </p>
<p>We knew that the added load on our platform meant that we needed to have something cleaner to work with. This was important as technical debt will always creep up and you can't just always build features.</p>
<p>We were slowly emerging from the bootstrapping phase of the company to more serious territory and it felt great. We made some more hires in the customer care and the marketing side. We improved month after month how the company operated. </p>
<p>Around that time, we decided to implement two systems that would improve our productivity:</p>
<p><strong>Objective and Key Result system:</strong> This really switched our whole mindset from working long hours to working on objective attainment. By having concrete goals that would improve how the company was doing, it allowed everyone to focus on what really mattered.   </p>
<p>It also allowed us to stop tracking when people were working or when they were taking vacations. As long as the objectives were worked on, it didn't really matter what the employees' workflows were (as long as they were not overworking).</p>
<p><strong>EOS System from Traction:</strong> This system was a very good foundational system to ensure that everyone stayed aligned. It really improved how our meetings were structured and laid the foundations for the vision of the company.   </p>
<p>I used to scoff at the thought of having a vision or core values. But after reading the book and implementing it, I cannot understand how we got as far as we got without one.   </p>
<p>This aligned everyone to a level that we didn't think possible and allowed us founders to make better decisions for the company.</p>
<p>It now felt that we were really running at a solid pace. Every month we had major improvements or good news in the company. The whole startup thing felt easier and was more enjoyable. </p>
<h3 id="heading-what-i-learned-from-my-second-product-launch">What I Learned From My Second Product Launch</h3>
<p>I learned another good set of lessons during the time of the launch:</p>
<p><strong>Your job as a founder is to "elevate and delegate".</strong> At a certain point, if you are still working on the minutiae of the work you are wasting resources. </p>
<p>It is usually way more cost effective to hire someone else that is more competent than you to do the operational work and to move on to another position that doesn't have staffing.   </p>
<p>By de-risking and laying the groundwork for a section of the company that is weak you are ensuring that it's worthwhile to hire someone and that this person has something to start with. This is priceless.</p>
<p><strong>No one can work 80 hours a week on different job types effectively.</strong> We realized this when we were looking at what the weakest points of the company were. It was always the spot where someone that had 3 different hats was working because there wasn't enough time to do quality work.   </p>
<p>If you are working on 3 different positions as a founder and you work 80 hours per week, it is the equivalent of working as a tired part time worker. The documentation will be poor, the process will be non-existent, and mistakes will start to show up.   </p>
<p>As soon as we saw someone work more than 40h a week it was a big red flag that we needed to distribute the load onto someone else.</p>
<p><strong>Part-time and volunteer workers are usually a waste of time.</strong> Once we were picking up more speed, we found ourselves constantly waiting for the part-time worker or the volunteer to finish their part of the work. This was holding us back so we made it a policy to not hire part time workers again.   </p>
<p>It's different for interns though, as they have a well contained work arrangement. For instance, we currently have 3 interns working on various machine learning projects as part of their PhD. This is perfect because the work that is given to them is well balanced and we know what to expect. </p>
<p>Around the time of the new platform release, we started our first accelerator programs.</p>
<h2 id="heading-the-next-ai-and-ecofuel-accelerators">The NEXT AI and EcoFuel Accelerators</h2>
<p>I didn't really know what an accelerator was at that time. However by being in two I quickly understood the difference between an accelerator and an incubator. </p>
<p>An accelerator's job is to give tools to an already up and running startup to accelerate their growth. On the other hand, an incubator is where startups usually begin.</p>
<p>One thing that is very useful in accelerators is that they provide startups they select with funding. By doing the NEXTAI and EcoFuel accelerators we were able to get $100,000 in total funding!</p>
<p>It was way more fast-paced than the incubator we'd been in. We had virtual classes with incredible entrepreneurs and got technical classes with researchers in machine learning such as Yoshua Bengio. </p>
<p>We also got to meet other amazing tech entrepreneurs living similar challenges in trying to get a startup to scale during a pandemic.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/nextai.jpeg" alt="The CEO and CTO of the Company GRAD4 at NEXT AI Demo Day" width="600" height="400" loading="lazy">
<em>Here we are keeping the 2m distance for a photo shoot.</em></p>
<p>It's at that time that we officially founded the more "research-y" side of the company that dealt with making AI models and working on the data we collected. We staffed that section of the company with PhD students and graduate master's students (and me!). </p>
<p>It's also at that time that we decided to raise our seed round of venture capital (VC) funding. We made this decision because we knew that we had something great, however we were in the type of business where we needed to scale fast to deliver the most value to our customers. </p>
<p>The more people use the platform, the more valuable it is for the people using it. Therefore, getting funding to crank up the sales and marketing is a must. We put our CEO on that full time as it is indeed a full-time job.</p>
<p>Around the time when NEXT AI was ending, we had one of our first major financial drawbacks that would forever change the way we saw failure in our company. </p>
<p>We thought that, just like at Centech, we were in a very good position to get the financing that comes at the very end of the program. However, we were in the finalists but not in the top 3 winner startup.</p>
<p>This was a major blow as we thought we were very solid during the whole program. </p>
<p>After gathering more information about why we weren't shortlisted, we realized that there was a major communication gap between what the organizers of the program thought we were doing and what we were doing. </p>
<p>They didn't realize how advanced our product was and were not aware of all the cool AI modules we were building to complement our offering.</p>
<h3 id="heading-what-i-learned-from-our-first-startup-accelerator">What I Learned From Our First Startup Accelerator</h3>
<p>This is where we realized a very, very important lesson:</p>
<p><strong>Other people's perception of your startup is as (if not more) important than what you are actually doing.</strong> We couldn't blame the program organizer for having the wrong perception about our company when they weren't even aware of most of what was happening with our company in the first place. </p>
<p>There was so much cool stuff happening everyday, but the number of things we publicly showed was thin in comparison.   </p>
<p>This was problematic and could seriously handicap ourselves in the future. This is when I made the decision to celebrate the wins of the company publicly.   </p>
<p>Every release of the product, I would publicly acknowledge my team's work on our social media. If we were passing major sales milestones, I would make a statement out of that. A major advance in the AI research of the company would be public within the week that it happened.  </p>
<p>I used to hate posting on social media as it felt like I was bragging, but it was one of the most important change we've made so far. By celebrating our wins publicly, we've increased by a lot the likelihood of good opportunities coming our way. This is now a vital part of the company that we leverage every day.</p>
<p>With our new understanding of how to best promote the company and improve our probability of success, we applied to two other bigger accelerators (Creative Destruction Lab and MaRS) and got accepted!</p>
<h2 id="heading-the-current-state-of-the-company">The Current State of the Company</h2>
<p>This last week of February was the best week the company has had, by far. User activity is way up and the amount of people involved in our project hasn't stopped growing. We are also in the final stretch of closing our financing round, which will allow us to accelerate our growth by a large margin. </p>
<p>However, it's not like it's all good and there is nothing to do now. One of the curses of starting to learn about a topic and making improvements in the way you work is realizing how much there is still to improve. </p>
<p>My company-improvement list on Trello never gets any shorter. Every time I finish a book, I have twenty new improvement ideas and every time I talk to a mentor, I have a dozen more. Even when I finish the implementation of one improvement, I have three more that spawn.</p>
<p>It's very comforting, though, to know that all of the work you put to improve yourself and your startup pays off. Also, realizing that you have created a company that can provide for other human beings is amazing. </p>
<p>I've made a lot of efforts to shed our old prototypical startup way of thinking. This means the following:</p>
<h3 id="heading-there-are-unlimited-vacations-for-all-employees">There Are Unlimited Vacations for All Employees</h3>
<p>This basically means that we don't track anyone's time off. The only time I'm talking to an employee regarding vacation is when I feel they don't take enough time off.</p>
<h3 id="heading-there-are-no-working-hours">There Are No Working Hours.</h3>
<p>If an employee wants to work early morning or late evening, I don't really care (because I do too). As long as the objectives are reasonably met, there is nothing worthwhile to track. </p>
<h3 id="heading-there-is-no-bragging-about-the-amount-of-work-someone-piles-up-allowed">There is No Bragging about the Amount of Work Someone Piles Up Allowed.</h3>
<p>It doesn't matter. Only objectives matter. If someone is working an unusual amount of hours, we flag that to the human relations department and we initiate a proposal to find more personnel.</p>
<h3 id="heading-we-invest-in-our-employees">We Invest in Our Employees</h3>
<p>We invest in them by buying books, courses, conference tickets, certifications or finding them mentors. Learning is a crucial part of the company's culture and it is heavily promoted. It's also very great for me because I can buy all the books I want on Amazon!</p>
<p>In short, I'm trying to build a company which I would have enjoyed working for. This is one of the guiding principles behind the choices I've made along the way and it really created something that I'm proud of.</p>
<h2 id="heading-my-advice-to-aspiring-saas-entrepreneurs">My Advice to Aspiring SaaS Entrepreneurs</h2>
<p>So that's how I ended up becoming the CTO of my company that has 20+ employees, all from doing random hackathons. It's been a wild ride and still is, but I would not trade this for any other job.</p>
<p>I want to conclude this section with a list of advice and tips for tech entrepreneurs (SaaS in particular) that I've learned along the way. I broke it down into subsections for convenience.</p>
<h2 id="heading-general-product-advice">General Product Advice</h2>
<h3 id="heading-your-product-is-way-less-important-than-you-think">Your Product is Way Less Important Than You Think</h3>
<p>The value the customer can get from your product is what matters. If you can deliver the same amount of value to your customer in a simpler way than having a full blown application, do it. </p>
<p>You will learn faster and you will be able to create more value for your customers in return. At some point though, the only way to keep increasing the value you can deliver is to have a good application. At that point you should have a very good idea about what is important to put in it.</p>
<h3 id="heading-validate-the-need-for-the-product-before-thinking-about-a-solution">Validate the Need for the Product Before Thinking About a Solution</h3>
<p>Spending time and effort on an application that your customers don't care about is the biggest waste of time you can have. </p>
<p>Validate with them every step of the way to see whether or not what you are doing is useful. Even at the expense of development time. </p>
<h3 id="heading-the-tech-stack-you-choose-is-less-important-than-you-think">The Tech Stack you Choose is Less Important Than you Think</h3>
<p>The most important concern is if you have enough knowledge to build something with the tech you choose, and if you can safely hire people to work with it. </p>
<p>Spending time finding the most optimal stack to work with is oftentimes pointless.</p>
<h3 id="heading-do-not-overcomplicate-your-application-at-first">Do not Overcomplicate Your Application at First</h3>
<p>Start with a good old monolith and gradually refactor it when needed. The monolith architecture will work like a charm for longer than you think! </p>
<h3 id="heading-put-the-minimum-amount-of-features-in-your-app-to-generate-the-maximum-amount-of-values-for-your-customers">Put the Minimum Amount of Features in Your App to Generate the Maximum Amount of Values for Your Customers</h3>
<p>The fewer features you have, the less maintenance, fewer bugs, and less technical debt you will accumulate. If a feature is not used by your users, kill it and scrub it from your code base.</p>
<h3 id="heading-talk-to-your-customers">Talk to Your Customers</h3>
<p>Spend as much time as possible with them and really learn from them. The knowledge you will gain will be a major competitive edge and will allow you to always deliver value to them!</p>
<h3 id="heading-at-some-point-cicd-and-a-good-suite-of-tests-is-a-lifesaver">At Some Point CI/CD and a Good Suite of Tests is a Lifesaver.</h3>
<p>Not having to fiddle around with deployment and not having to worry as much that you introduced a regression in your code is liberating. </p>
<p>It allows you to become more productive and have a better understanding of the whole code base when you have to read test errors.</p>
<h3 id="heading-monitoring-is-super-important-and-should-be-implemented-as-soon-as-possible">Monitoring is Super Important and Should be Implemented as Soon as Possible</h3>
<p>Being able to know what is being used, what is the state of the application, and if there are potential problems is a must. </p>
<p>Not having monitoring tools is like driving in a forest road at night with your sunglasses on. It's a bit weird and generally not a safe way to get wherever you want to go.</p>
<h3 id="heading-do-not-outsource-your-core-competency">DO NOT OUTSOURCE YOUR CORE COMPETENCY.</h3>
<p>This advice is in all caps because I'm yelling it. If the core of your business is making a web app, make sure that you have everything you need in-house to make a web app. </p>
<p>Relying on outsourcing firms that don't have direct access to your customers or your reality is a sure way to mess the whole thing up. It's therefore extra important that you define clearly what is your core competency in order to not outsource it.</p>
<h2 id="heading-artificial-intelligence-advice">Artificial Intelligence Advice</h2>
<h3 id="heading-ai-is-great-but-delivering-value-to-your-customers-is-better">AI is Great, but Delivering Value to Your Customers is Better</h3>
<p>If you don't need AI to deliver value to your customers, don't put AI in what you give them. It will slow you down big time. However, if you validated that you indeed need some sort of AI to provide value to your customers, make that a top priority for your company.</p>
<h3 id="heading-ensure-that-you-are-collecting-the-right-data-for-your-ai">Ensure That You are Collecting the Right Data for Your AI</h3>
<p>This is especially important if you are working with partnering organizations as they often have no idea what is good data for a given problem. You need to figure out if the data is great for the problem you are tackling before getting more of it.</p>
<h3 id="heading-start-with-a-linear-regression-and-work-your-way-to-that-deep-neural-network-with-thousands-of-layers">Start With a Linear Regression and Work Your Way to That Deep Neural Network with Thousands of Layers</h3>
<p>Even if you have tasks for which you have enough data to attempt larger models, start with the simple ones. It will allow for rapid feedback on your data and will help you secure some baseline performance that can be used as benchmarks for the larger models.</p>
<h3 id="heading-iteratively-improve-your-ai-system-and-dont-wait-until-everything-is-perfect-to-launch">Iteratively Improve your AI system and Don't Wait Until Everything is Perfect to Launch.</h3>
<p>It's fine to label an AI system as Beta and start experimenting at a larger scale with users. This applies to any product you build, but I feel like this is worth mentioning again in the AI context as it is often forgotten.</p>
<h2 id="heading-company-advice">Company Advice</h2>
<h3 id="heading-join-an-incubator">Join an Incubator</h3>
<p>The amount of coaching you will get – even from the judges before being accepted – is very important. They've seen thousands of startup ideas and they will be able to give you some very valuable advice. </p>
<p>Incubators are oftentimes paid by the government for every startup that they get in their program, which means it's a win-win situation for everyone.</p>
<h3 id="heading-join-an-accelerator-after-joining-an-incubator">Join an Accelerator After Joining an Incubator</h3>
<p>This gives you additional resources and allows you to get very good coaching on very specialized parts of your business (like machine learning or financing). It is also a very good way to network!</p>
<h3 id="heading-setup-your-core-values-and-your-vision-for-your-company">Setup Your Core Values and Your Vision for Your Company</h3>
<p>Having a vision is like a superpower. As soon as someone throws in that blockchain idea for the nth time you can throw it right back by saying that it doesn't fit the vision. </p>
<p>If someone has a rotten attitude you can easily show your core values publicly and correct course. The hiring and firing is much easier when all of this is setup and understood by the whole company.</p>
<h3 id="heading-document-your-companys-processes-as-soon-as-possible">Document Your Company's Processes as Soon as Possible</h3>
<p>You will be surprised by how much processes you have even at an early stage. You will also be surprised by how little everyone is aware of it (including yourself!). </p>
<p>By documenting these processes you will be in very good shape to start improving and refining them to increase everyone productivity.</p>
<h3 id="heading-you-most-likely-dont-need-an-office">You Most Likely Don't Need an Office</h3>
<p>If you are building a SaaS product, your stuff will most likely live on the cloud and your offerings will be purely software-based. Learn about how to setup a remote work environment efficiently and save on the office cost early on!</p>
<h3 id="heading-meetings-are-less-important-than-you-think">Meetings are Less Important Than You Think</h3>
<p>Face to face synchronous meetings are not that useful. I've found that most of the time, just having a Google document that says what problem you want to fix in the meeting and distributing it to people you want to meet with is 99% of the job. </p>
<p>You will get a few comments on what to change, 3-4 asynchronous back-and-forth discussions, and <em>voilà</em>! Another problem fixed.</p>
<h3 id="heading-meetings-are-sometimes-necessary">Meetings are Sometimes Necessary</h3>
<p>No meetings whatsoever are not possible, though (I'm a hardcore asynchronous guy and even I need to admit that). If after sending that document you have 30 comments and you get to a stalemate kind of situation, it's usually time to ring the meeting bell and address the point of contention synchronously. </p>
<p>Most of the time, it's a matter of miscommunication. Having this synchronous back and forth allows the issue(s) to be resolved more efficiently.</p>
<h3 id="heading-make-most-stuff-in-your-company-public-to-all-employees">Make Most Stuff in Your Company Public to All Employees</h3>
<p>If something that is work related doesn't have to be private to a specific set of people, it should be public. By having the opportunity to jump in someone else's operational discussion, you can provide much needed feedback that will save lots of time. </p>
<p>Also, by having this whole bank of general knowledge available to everyone, you ensure that people are all aware about what is going on in the other departments.</p>
<h3 id="heading-make-sure-that-the-private-stuff-stays-private">Make Sure that the Private Stuff Stays Private</h3>
<p>This goes both for security-related sensitive material and for private employee matters. If an employee tells you something personal, do not break that trust.</p>
<h3 id="heading-continuously-improve-your-companys-structure">Continuously Improve Your Company's Structure</h3>
<p>A company is an ever-growing organism. The structure that is best for today won't necessarily be the best in a month's time. It needs to be continuously tweaked and improved in order to maximize the work that the people working on it can output.</p>
<h3 id="heading-beware-of-working-with-large-entities-like-cities-multinationals-or-governments">Beware of Working with Large Entities Like Cities, Multinationals, or Governments</h3>
<p>These are slow and could end up suffocating your company. They will book meetings upon meetings to move the project forward by inches. Even if they pay you a lot, the cycle of learning you can do with them is so long that you will not have improved by much. </p>
<p>Working with smaller entities allows for more direct feedback. And if you can gather enough of them you can have a much more robust business. Resting on a thousand small pillars is more stable than resting on two huge ones.</p>
<h2 id="heading-employee-advice">Employee Advice:</h2>
<h3 id="heading-dont-hire-jerks-just-because-of-their-technical-skills">Don't Hire Jerks Just Because of their Technical Skills</h3>
<p>It's a big no-no. If you think about it this way, a person will stifle the productivity of everyone by souring the cultural soup. </p>
<p>If people are dreading going to work because of that one person, you will end up with more problems than what this person can fix with their code.</p>
<h3 id="heading-cultural-fit-is-not-an-option">Cultural Fit is not an Option</h3>
<p>Clearly check if the person has the technical abilities that you are looking for. However, check just as rigorously if the person as the right personality for your company. </p>
<p>Having someone clash with the company or not upholding one of your core values will do more harm than good.</p>
<h3 id="heading-team-fit-is-not-an-option">Team Fit is not an Option</h3>
<p>Make the teams an integral part of the hiring process. You will be surprised by how picky the team is and how rigorous they are in the hiring process. </p>
<p>It happened quite often that the person we were interviewing passed the technical interview and the cultural one, but didn't pass the team interview. </p>
<p>The rationale for rejecting a participant from the team was always valid and we couldn't believe we didn't catch it earlier in the process.</p>
<h3 id="heading-neurodiversity-increases-productivity">Neurodiversity Increases Productivity</h3>
<p>You have to resist the urge to hire people that think exactly like you if you want to have a truly productive company. </p>
<p>By having people from different backgrounds, you will increase the chance of finding creative ways out of problems and you will reduce your blind spots by a lot. </p>
<h3 id="heading-dont-try-to-fit-a-good-profile-in-the-company-find-a-good-profile-for-a-need">Don't Try to Fit a Good Profile in the Company, Find a Good Profile for a Need</h3>
<p>Always start by assessing what is your most urgent need and then find the best person to fill that position. By starting the other way, you will bloat your company with people that don't truly create value.</p>
<h3 id="heading-you-will-have-to-fire-people-and-its-for-the-best">You Will Have to Fire People and it's for the Best</h3>
<p>I've had to let a few people in the company go, and every time it was better for all parties. However, do it with respect. If you did all the work to bring someone on your ship, you should do all the work to bring someone out of it. </p>
<p>This means ensuring that this person understands why it's not a fit, that you gave enough warning signs, and making sure that this person has the support they need once they are moving away from the company.</p>
<h3 id="heading-make-sure-that-your-employees-are-genuinely-happy">Make Sure That Your Employees are Genuinely Happy</h3>
<p>If someone doesn't feel good, talk to them and help them out. Wish them a happy birthday. Say thank you when they do something great. Coach them when they want to grow. Debug them when they make a mistake. </p>
<p>Having happy employees is one of your most valuable currencies as a startup and what makes working in one such a great experience.</p>
<h2 id="heading-personal-advice">Personal Advice:</h2>
<h3 id="heading-make-sure-that-the-founders-or-executives-dont-kill-each-other-and-the-company">Make Sure that the Founders or Executives don't Kill Each Other – and the Company</h3>
<p>What do you get when you have a toxic startup culture of working insane work hours coupled with financial stress and customer problems? A good recipe for company failure. </p>
<p>Make sure to always reserve at least one hour per week where you don't talk about the company, but just check on each other and try to mend personal issues in the open.</p>
<h3 id="heading-repeat-after-me-its-a-marathon-not-a-sprint">Repeat After me: It's 👏 a 👏 Marathon 👏 not 👏 a 👏 Sprint</h3>
<p>Insane working conditions cannot last. It's not <strong>if</strong> you will burn out, it's <strong>when</strong>. </p>
<p>If you cannot envision keeping the pace of work you currently have for the rest of your life, change it before it's too late. </p>
<p>I've seen too many startups crumble suddenly because of people thinking they can sustain having no time off forever. </p>
<h3 id="heading-leave-room-for-your-personal-growth">Leave Room for Your Personal Growth</h3>
<p>Learn about that one topic that is completely left field for your startup and enjoy it. Go ahead and network with people for your own benefit, it's okay. The more you grow as a person the higher the potential growth for your company.</p>
<h3 id="heading-leave-room-to-just-chill-out">Leave Room to Just Chill Out</h3>
<p>Even if you enjoy working on your startup don't neglect the other aspects of your life. </p>
<p>It's fine to have other friends outside of work and it's fine to just unplug for a while. If you can't do that you have serious issue to fix in your company.</p>
<h3 id="heading-have-fun">Have Fun</h3>
<p>It's genuinely fun to build a company from the ground up. Enjoy the time working on that nasty bug that put the whole EC2 instance down. Enjoy your time calling this one customer that has nothing good to say about what you do. </p>
<p>Enjoy all of the little problems that will pave the way of your company. Because a startup can only do two thing: <strong>Die</strong>, in which case you will look back at those days with fond memory. <strong>Grow</strong>, in which case it will start to become something bigger than you and gain a personality of its own!</p>
<h2 id="heading-useful-saas-entrepreneurship-reading">Useful (SaaS) Entrepreneurship Reading</h2>
<h3 id="heading-the-lean-startup-how-todays-entrepreneurs-use-continuous-innovation-to-create-radically-successful-businesses">The Lean Startup: How Today’s Entrepreneurs Use Continuous Innovation to Create Radically Successful Businesses:</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-48.png" alt="The Lean Startup by Eric Ries" width="600" height="400" loading="lazy"></p>
<p>This book is a very simple read and taught me that cycling through hypothesis/learning is way more important than doing the most perfect thing right off the bat. </p>
<p>As a technical person that <strong>loves</strong> technology, I couldn’t understand why the tech was not at the forefront of every business discussion. </p>
<p>This book, with the clear example of bold tests that were done with real users, showed me exactly why a focus on building a product before understanding what the users will think of the product is a bad idea.</p>
<h3 id="heading-traction-get-a-grip-on-your-business">Traction: Get a Grip on Your Business</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-37.png" alt="Traction by Gino Wickman" width="600" height="400" loading="lazy"></p>
<p>This book helped me make sense of how to structure our company once it has scaled past the founders. I’m at something like the sixth read cover to cover. </p>
<p>There is a lot of useful information and practical guidelines to use in order to really get a solid structure that make sense for the next growth phase. </p>
<p>It also helped create a sense of calm when thinking about the future because it increases your awareness of what will come in the future.</p>
<h3 id="heading-measure-what-matters-how-google-bono-and-the-gates-foundation-rock-the-world-with-okrs">Measure What Matters: How Google, Bono, and the Gates Foundation Rock the World with OKRs</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-38.png" alt="Measure What Matter by John Doerr" width="600" height="400" loading="lazy"></p>
<p>I read this book before reading Traction, however there are a lot of similarities between the OKR goal structure and the Rock goal structure from Traction. The basic idea is that you have limited time to work on goals/projects, so work on the most impactful ones and ditch the rest. </p>
<p>The idea of simply not thinking about the low priority objectives really creates a sense of space in your head. Knowing exactly what to focus on and having the liberty to think about how to get there also helped create an ultra-collaborative structure.  </p>
<p>I use the OKR system in my personal life too. It really helps me reassure myself that I’m on the right path and allow me to say no to opportunities that pop up throughout year that are not aligned with my objectives.</p>
<h3 id="heading-peopleware-productive-projects-and-teams">Peopleware: Productive Projects and Teams</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-39.png" alt="Peopleware by Tom DeMarco &amp; Timothy Lister" width="600" height="400" loading="lazy"></p>
<p>This was a very enjoyable read. It talks about a facet of software engineering that is often not taken into consideration, which is the people factor. I absolutely love the straight to the point organic writing style that the authors use. </p>
<p>Lots of examples are given and there is a significant supplementation of statistics along their argumentation that really help gauge what non conventional changes to implement.</p>
<h3 id="heading-drive-the-surprising-truth-about-what-motivates-us">DRIVE: The surprising truth about what motivates us</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-40.png" alt="Drive by Daniel H. Pink" width="600" height="400" loading="lazy"></p>
<p>Drive is very closely related to Peopleware in the subject it addresses. Both of them help in figuring out how to create a work environment that is purposeful and that drive people to give their fullest. </p>
<p>I’ve learned a great deal about how much “carrot and stick” kind of reward/punishment comes into play in the traditional workplace and how it's not the optimal way to increase motivation. </p>
<p>It also allowed me to understand how I can push myself to accomplish my goals in a purposeful manner without having to bribe and trick myself.</p>
<h3 id="heading-effective-devops-building-a-culture-of-collaboration-affinity-and-tooling-at-scale">Effective DevOps: Building a Culture of Collaboration, Affinity, and Tooling at Scale</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-41.png" alt="Effective DevOps by Jennifer Davis &amp; Ryn Daniels" width="600" height="400" loading="lazy"></p>
<p>This book is an extensive introduction to DevOps culture and is a good handbook to keep to consult when you're unsure about a certain aspect or situation. </p>
<p>It was the book that introduced me in more depth to that way of thinking and got me to really understand it more than on the surface level. It had some very neat examples of how all of the DevOps concepts tie up in the real world.  </p>
<p>However, it’s quite a lengthy book. It is meant to be consulted in a non-linear fashion. I recommend keeping a copy at hand if you manage a technological team to get some ideas about what to do in a given situation.</p>
<h3 id="heading-the-phoenix-project-a-novel-about-it-devops-and-helping-your-business-win">The Phoenix Project: A Novel About IT, DevOps, and Helping Your Business Win</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-42.png" alt="The Phoenix Project by Gene Kim, Kevin Behr and George Spafford" width="600" height="400" loading="lazy"></p>
<p>I read the Phoenix Project a while after having read Effective DevOps. Effective DevOps gave me a deeper understanding of the movement, but it’s the Phoenix Project that really made everything “click”. </p>
<p>It’s a novel, but explained in such an organic way that it could have been a biography. I read the whole thing in 2 days over the summer as I was very engaged with the protagonist's struggle with inefficient process and “impossible” goals to meet. </p>
<p>After reading it I felt way more confident that the changes I was making to my organization were the right ones.  </p>
<p>If I had one book to give to a non-tech manager to make them understand how to make a tech department fail and how to make it thrive, it would be this one.</p>
<h3 id="heading-designing-data-intensive-applications">Designing Data-Intensive Applications</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-43.png" alt="Designing Data-Intensive Applications by Martin Kleppmann" width="600" height="400" loading="lazy"></p>
<p>This book was so densely packed with information gained from working with very difficult problems that you probably need to re-read it from time to time while you also work on difficult problems. </p>
<p>I’ve learned a lot, both in the inner design of the behemoth of the internet and how much these behemoths were built by facing a constant stream of problems. </p>
<p>The sheer amount of tradeoffs, learning, and ambiguity that takes place in systems at huge scale was staggering. It helped me prepare and better react when I hit various problems in my tiny (in comparison) systems I’d been building.  </p>
<p>Likewise, this is the kind of book that should be read periodically while building something that is in the process of scaling.</p>
<h3 id="heading-forge-your-future-with-open-source-build-your-skills-build-your-network-build-the-future-of-technology">Forge Your Future with Open Source: Build Your Skills. Build Your Network. Build the Future of Technology</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-44.png" alt="Forge Your Future with Open Source by VM Brasseur" width="600" height="400" loading="lazy"></p>
<p>This book is one that really helped me better structure our remote company so we could hit our business objectives and help our employees feel productive and happy. </p>
<p>I drew a lot of inspiration from how open source projects were structured and made quite a lot of changes in that sense. It also helped me understand and appreciate a bit more about how open source projects work.</p>
<h3 id="heading-principles-by-ray-dalio">Principles by Ray Dalio</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-46.png" alt="Principles by Ray Dalio" width="600" height="400" loading="lazy"></p>
<p>This is an incredible book with an insane amount of tips from a successful entrepreneur in the financial sector. </p>
<p>The amount of useful content in there is staggering and will require multiple reads in order to extract it all. If you are looking for new ideas to make your organization more efficient, better at problem solving, or stimulate growth, it's a must!</p>
<h3 id="heading-delivering-happiness-a-path-to-profits-passion-and-purpose">Delivering Happiness: A Path to Profits, Passion, and Purpose</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-47.png" alt="Deliverin Happiness by Tony Hsieh" width="600" height="400" loading="lazy"></p>
<p>A very beautiful book by the late CEO of Zappos. It's a humble book filled with good learning and takeaways by Tony Hsieh in his entrepreneurial journey. </p>
<p>The most important part here is the focus on making sure that the culture was right, as he had two main company successes in his career: One with LinkExchange that had no focus on the culture and another one with Zappos which was heavily invested in it. </p>
<p>The latter is arguably the stronger business.</p>
<h2 id="heading-reaching-out">Reaching Out</h2>
<p>If you are interested in learning more about my company, you can check our <a target="_blank" href="https://grad4.com/en/">website</a>. If you have questions feel free to add me on <a target="_blank" href="https://www.linkedin.com/in/yacine-mahdid-809425163/">LinkedIn</a> or <a target="_blank" href="https://twitter.com/CodeThisCodeTh1">Twitter</a> to chat :) </p>
<p>I hope this was helpful!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How I Accidentally Built an API Business ]]>
                </title>
                <description>
                    <![CDATA[ By Wenbin Fang In this article, I’ll share my journey of building an API business, the technology behind it, and how to build your own API business in the future. First, a little bit about the business I've built: Listen Notes is a podcast search eng... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-i-accidentally-built-an-api-business/</link>
                <guid isPermaLink="false">66d461734bc8f441cb6df835</guid>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Entrepreneurship ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Wed, 09 Dec 2020 22:02:15 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/12/3c36ff70b8ab4d25aa85bfa567007087.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Wenbin Fang</p>
<p>In this article, I’ll share my journey of building an API business, the technology behind it, and how to build your own API business in the future.</p>
<p>First, a little bit about the business I've built: <a target="_blank" href="https://www.listennotes.com/">Listen Notes</a> is a podcast search engine that allows people to search <a target="_blank" href="https://www.listennotes.com/podcast-stats/">nearly two million podcasts and more than 89 million episodes</a> by people or topics. We also provide a <a target="_blank" href="https://www.listennotes.com/api/">podcast API</a> for developers to use, which is called Listen API. It has become a core part of our business.</p>
<h2 id="heading-an-accidental-api-business"><strong>An accidental API business</strong></h2>
<p>I left my previous failed startup in September 2017. After a few days of tinkering, I picked up one of my fledgling side projects to polish the UI a bit. </p>
<p>That side project was <a target="_blank" href="https://www.listennotes.com/">Listen Notes</a>, a podcast search engine website, which was just a single page React JS app running on three $10/month DigitalOcean droplets.</p>
<p>Little did I know a few years ago that my small, neglected side project would turn into the helpful business it has blossomed into.</p>
<p><img src="https://production.listennotes.com/web/image/37454d6afb7b458ca58ae4e5873ddbbd.png" alt="Image" width="600" height="400" loading="lazy">
<em>An early version of Listen Notes</em></p>
<p>I continued to work on Listen Notes full-time and incorporated Listen Notes as a Delaware C-Corp in October 2017. One of my goals was to experience as many facets of business as possible, rather than just writing code behind the scenes.</p>
<p>My initial plan was as follows: (Don’t laugh at me!)</p>
<ul>
<li>Build a podcast search engine website and make some money from advertising, just like Google. Simple!</li>
<li>If this Listen Notes thing doesn’t work in two or three months, then I’ll run out of cash, and I’ll go into credit card debt to keep going for one more month or so. If it still doesn’t work, then I’ll have to find a full-time job. Although Jeff Bezos’ parents invested $300,000 in early Amazon and Mark Zuckerberg’s parents loaned $100,000 to early Facebook, not every family is able to casually toss six figures of cash at web projects.</li>
</ul>
<p>Then something happened.</p>
<p>On November 20, 2017, I got an email from the developer of a new podcast app, who asked if Listen Notes provided an API. He wanted to be able to search episodes in his app, but he didn’t want to build the entire backend. </p>
<p>I asked a few questions (for example, how would the endpoints look, what data fields did he need, how much was he willing to pay…). I got his answers. Everything was in an email thread within a couple days.</p>
<p>On November 30, 2017, I quickly implemented three endpoints (<em>GET /search, GET /podcasts/{id}, and GET /episodes/{id}</em>), which were basically three <a target="_blank" href="https://docs.djangoproject.com/en/3.1/topics/http/views/">Django views</a>. </p>
<p>I Googled “API gateway” or something like that and found a service called <a target="_blank" href="https://konghq.com/blog/mashape-has-a-new-homepage/">Mashape</a>, which was an API marketplace that handled payment, user management, and API documentation. </p>
<p>So I put my three endpoints on Mashape and created two plans there: FREE and PRO. I emailed the developer back to tell him the API was ready to use.</p>
<p><img src="https://production.listennotes.com/web/image/125d913b8ad14bbd99fbc7c1cfe49e04.png" alt="Image" width="600" height="400" loading="lazy">
<em>The email thread that prompted me to build Listen API</em></p>
<p>Then nothing happened. The podcast app developer didn’t use our API and instead phased out their project.</p>
<p>Eventually, I moved on to primarily focus on the development of listennotes.com. The API was basically in self-driving mode on the open web. Anyone who happened to discover our API could sign up, without talking to any human beings.</p>
<p>On January 14, 2018, I got my first paying user. A few more paying users arrived that same year.</p>
<p><img src="https://production.listennotes.com/web/image/1cf8ad68f0c345318c9c64b3f370764b.png" alt="Image" width="600" height="400" loading="lazy">
<em>The email notification I received for my first paying user</em></p>
<p>Wait, what is RapidAPI? Well, Mashape was acquired by a startup named RapidAPI. They didn’t rebrand Mashape to RapidAPI completely until mid-2018. Startups typically don’t do things in a clean and methodical way, which is totally understandable.</p>
<p>Then something happened.</p>
<p>There was an outage on the RapidAPI end on November 29, 2018. </p>
<p><img src="https://production.listennotes.com/web/image/4d1a713f41dd465b9e57fa4e34be4208.png" alt="Image" width="600" height="400" loading="lazy">
<em>The email I sent to people in RapidAPI when the outage happened</em></p>
<p>RapidAPI had performed a big backend upgrade around that time. As an engineer, I totally understand that outages happen, especially when making huge changes in the backend. But I felt helpless because their customer support didn’t reply to my email. Phone call didn't work, as expected.</p>
<p>Usually their customer support was very responsive. Perhaps it was the holiday season and people were on vacation. </p>
<p>So I used hunter.io to find work emails of individual RapidAPI employees, the CEO, as well as the CTO. The issue was finally resolved, many hours later. In other words, our API was completely unusable during those down hours. I felt very sorry for our paying users.</p>
<p>Then around mid-February 2019, RapidAPI had billing problems and failed to pay us a few thousand bucks. Our paying users paid RapidAPI first. RapidAPI took a 20% cut. Then they paid the remaining 80% (minus PayPal fees) to us. </p>
<p>After several back-and-forth emails and phone calls, we finally got our payment. It’s understandable. Again, startups make mistakes.</p>
<p>In late February 2019, I decided to build our own RapidAPI replacement, for a few reasons:</p>
<ul>
<li>Our API revenue became nontrivial. The 20% cut from RapidAPI was a bit too much for us.</li>
<li>We wanted API requests to hit our own servers directly, thus lowering latency for our users.</li>
<li>I didn’t want to feel helpless when RapidAPI had outages. Overall they did a good job running the service. But I wanted to control my own destiny.</li>
<li>I wanted to contact my API users directly. Using RapidAPI, API providers like me didn’t have access to our users’ email addresses. It’s understandable. It’s like the “Uber for X” companies that don’t want workers and customers to bypass them and strike deals under the table. Marketplaces don’t want users to skip the middleman’s commission fees.</li>
</ul>
<p>In addition, I vowed to do two things really well for our new API system:</p>
<ul>
<li>We must provide great customer service to our paying users.</li>
<li>We will give customers a very stable &amp; reliable backend service.</li>
</ul>
<p>After 30 days of hard work, <a target="_blank" href="https://www.listennotes.com/blog/listen-api-v2-simple-pricing-same-endpoints-39/">we launched Listen API v2</a> on March 27, 2019. The legacy API hosted on RapidAPI became Listen API v1, a version we won’t add new features to but don’t want to shut down because some apps are still using it as of December 2020!</p>
<p>We continue to improve our new Listen API v2 by adding new endpoints, new data fields, improving operational efficiency, as well as spiffing up the user dashboard and our internal tools.</p>
<p>Things are picking up speed gradually. I’ve been happy since then.</p>
<p>So, that’s the journey of Listen API so far.</p>
<p><em>Note: Although we decided to move on from RapidAPI, I still think it’s a great service. Startups all make mistakes in the early stage. They fix things and continue to improve their service, which is great!</em></p>
<h2 id="heading-the-technology-behind-listen-api"><strong>The technology behind Listen API</strong></h2>
<p>Developers can use our API to search podcasts and fetch detailed podcast-episode metadata. To make this whole thing work, we need to make sure a few core components are in place.</p>
<p><img src="https://production.listennotes.com/academy/image/3c36ff70b8ab4d25aa85bfa567007087.png" alt="Image" width="600" height="400" loading="lazy">
<em>Listen API's main components and the technologies used</em></p>
<h3 id="heading-datastore-and-search-engine"><strong>Datastore and search engine</strong></h3>
<p>This is a shared component with our website. Therefore, I didn’t need to change anything in the datastore and search engine when building our API infrastructure.</p>
<p>We use Postgres as our main data store (for example, for podcast metadata, user accounts, and so on), and Elasticsearch as the search engine.</p>
<p>I wrote an old blog post with the <a target="_blank" href="https://www.listennotes.com/blog/the-boring-technology-behind-a-one-person-23/">details of the entire tech stack</a>.</p>
<h3 id="heading-internal-tools-and-processes"><strong>Internal tools and processes</strong></h3>
<p>If you’ve worked at any web companies, you probably know what I’m referring to here.</p>
<p>It’s rare for an Internet business to be 100% automatic. A company always needs to build tons of internal tools and set up manual processes to keep the service functional. That’s why companies like <a target="_blank" href="https://www.bloomberg.com/news/articles/2020-10-20/retool-nears-1-billion-valuation-with-funding-from-sequoia">Retool have such a high valuation</a> nowadays.</p>
<p>Companies are investing big money in internal tools that are invisible to end users:</p>
<p><img src="https://production.listennotes.com/web/image/e448df5503934491b251a2a85b815686.png" alt="Image" width="600" height="400" loading="lazy">
<em>Percentage of team's time spent on internal tools. Credits: <a target="_blank" href="https://retool.com/blog/state-of-internal-tools-2020/">Retool</a></em></p>
<p>To start our API business, we needed to build (at least) two types of internal tools:</p>
<ul>
<li><strong>For data operations</strong>: We needed the ability to keep the podcast metadata up-to-date, fix corrupted metadata, plus review and approve any changes made by users.<br>Additionally, we required a framework that handled new, rare edge cases of corrupted podcast data along the way. To some degree, building a software product means handling tons of edge cases for a very long period of time (like, years), rather than launching new features every day.</li>
<li><strong>For user operations</strong>: We required the ability to suspend a bad user’s account, as well as immediately look up all information related to a specific user who contacted us for a specific issue.<br>Plus, we had to be able to quickly evaluate if “it’s our fault” (server-side errors) or “it’s their fault” (client-side errors) when users complained.</li>
</ul>
<p>Internal tools are used by employees inside the company. Some of those tools are fully automated, such as cron jobs that perform scheduled tasks. But many tools should be used manually by human employees, for example when inputting a user’s ID number and clicking a button.</p>
<p>Most of our internal tools have ugly web UIs, with default <a target="_blank" href="https://getbootstrap.com/">Bootstrap</a> styling :) </p>
<p><img src="https://production.listennotes.com/web/image/f5c69dcc39a041bdbb230bcc25b3a36c.png" alt="Image" width="600" height="400" loading="lazy">
<em>A portion of our internal tool’s UI that allows us to suspend an API user’s account.</em></p>
<p>Fortunately, our API shares many internal tools with the website. So we didn’t need to build too many new things here.</p>
<h3 id="heading-the-analytics-and-billing-system"><strong>The analytics and billing system</strong></h3>
<p>The pricing model of an API is typically usage-based. Check out some real world examples:</p>
<ul>
<li><a target="_blank" href="https://www.twilio.com/pricing">https://www.twilio.com/pricing</a></li>
<li><a target="_blank" href="https://sendgrid.com/pricing/">https://sendgrid.com/pricing/</a></li>
<li><a target="_blank" href="https://cloud.google.com/maps-platform/pricing/">https://cloud.google.com/maps-platform/pricing/</a></li>
<li><a target="_blank" href="https://www.microsoft.com/en-us/bing/apis/pricing">https://www.microsoft.com/en-us/bing/apis/pricing</a></li>
</ul>
<p>It’s a must to track how many requests a user uses in real-time. We use Redis to keep track of such stats and periodically dump into Postgres for persistent storage.</p>
<p>What happens if our Redis has an outage? We might temporarily lose some tracking stats. In this case, we have an internal tool to sync stats from raw Nginx logs.</p>
<p>We have to change billing plans without affecting existing users. For example, if we raise prices, existing users should still enjoy the benefit of the old plans. If it’s not done right, it’s easy to have inconsistent states across the board, and angry users getting charged the wrong billing plan!</p>
<p>Payment failures, a very common occurrence, must be handled gracefully. We can’t just suspend users right away. We need to be able to notify ourselves that “this user failed to pay” and notify the user that “you failed to pay.” </p>
<p>After a few retries, we suspend users manually — well, we could’ve automated this last step. But we don’t suspend users often nowadays, so it’s okay to do so manually. There’s no need to make everything perfect (at least for now).</p>
<p>We have a dashboard (God’s view) to see how many requests each individual user uses in the current billing cycle. And we are able to review raw logs for each user from a web UI, without manually pulling log files from S3.</p>
<p>Stripe and PayPal (via Braintree) are our payment processors. Most of our international users use PayPal.</p>
<p>Finally, putting all of these factors together, we can calculate the actual amount of money that a user should pay us in real-time, based on their usage. We run async tasks via <a target="_blank" href="https://docs.celeryproject.org/en/stable/getting-started/introduction.html">Celery</a> to charge due bills.</p>
<p>What happens if a user unsubscribes in the middle of a billing cycle? We charge them prorated rates, based on time and usage. Users don’t need to pay a full month’s fee in those instances.</p>
<h3 id="heading-api-servers"><strong>API Servers</strong></h3>
<p>We run Django apps to serve API requests. Each endpoint is a simple Django view. A Django middleware verifies if a request is legit, then generates a log or rejects the request right away.</p>
<p>We cache response data per API key + unique URL in Redis. In general, <a target="_blank" href="https://www.listennotesstatus.com/">our API performance is pretty good</a>.</p>
<p>We use Nginx as a load balancer and provision multiple API servers. It’s straightforward to do rolling deployment here, with a bunch of sanity checks to ensure the API is functioning. </p>
<p>Generally speaking, the easy and robust deployment process increases my confidence to make incremental code changes often and to deploy frequently.</p>
<p>An API endpoint is RESTful and returns a JSON response, pretty standard nowadays.</p>
<h3 id="heading-user-dashboard-and-api-docs"><strong>User Dashboard and API Docs</strong></h3>
<p>Each API user can access a <a target="_blank" href="https://www.listennotes.com/api/dashboard/">dashboard</a> on our website to learn the amount of requests they’ve used in the current billing cycle and view recent raw logs. They can also update payment methods, create or reset new API keys, set up webhooks, and add coworkers to the same API account.</p>
<p><img src="https://production.listennotes.com/web/image/77749e815d7741a4a66980282870e25f.png" alt="Image" width="600" height="400" loading="lazy">
<em>Listen API's user dashboard</em></p>
<p><a target="_blank" href="https://www.listennotes.com/api/docs/">API Docs</a> is probably the most important UI for an API business. Therefore, many API companies employ a whole team of full-time engineers to build and maintain “merely” the API Docs page(s).</p>
<p>An API Docs page is not simply a full page of English words. It must show code snippets for different programming languages. </p>
<p>Users have to be able to run your code example directly from the page. You are required to design a repeatable process (no matter if it's automatic or manual) to keep the documentation in sync with your code. There are plenty of nuances.</p>
<p>We spent a lot of time and energy building and iterating multiple versions of <a target="_blank" href="https://www.listennotes.com/api/docs/">our API Docs page</a>. Following is the end result:</p>
<p><img src="https://production.listennotes.com/web/image/0170ea52dec748038632db1bd3444812.png" alt="Image" width="600" height="400" loading="lazy">
<em><a target="_blank" href="https://www.listennotes.com/api/docs/">Listen API Docs Page</a></em></p>
<p>Initially, we tried a few open source solutions for the API documentation. It’s quite time-consuming to understand an open source project well enough to customize it. Ultimately, we decided that it would be faster to build the page from scratch rather than customizing an open source solution built by others.</p>
<p>Our API Docs page is basically a React JS single page app.</p>
<p>We codify all endpoints, response data schema, and example response in an <a target="_blank" href="https://listen-api.listennotes.com/api/v2/openapi.yaml">OpenAPI spec</a>. The React JS app of the API Docs page reads from our OpenAPI spec directly.</p>
<p>The side effect of using OpenAPI is that we can easily integrate with tools like <a target="_blank" href="https://www.postman.com/">Postman</a>, because <a target="_blank" href="https://en.wikipedia.org/wiki/OpenAPI_Specification">OpenAPI</a> is a (relatively) widely adopted standard for API documentation nowadays.</p>
<h2 id="heading-why-listen-api-works"><strong>Why Listen API works</strong></h2>
<p>Listen API has been a nice business for me so far.</p>
<p>But don’t expect me to share revenue numbers publicly :)</p>
<p>Some companies are doing this <a target="_blank" href="https://www.google.com/search?q=open+startup">open startup</a> thing, sharing every single business metric to the public, which is great.</p>
<p>But we shouldn’t blame the majority of companies (including my small company Listen Notes, Inc.) who don’t want to share business metrics publicly.</p>
<p>Not everyone is comfortable being naked in public, literally or figuratively.</p>
<p>Similarly, there’s lots of business advice (or cliches) that you don’t have to follow.</p>
<ul>
<li>You don’t have to find a cofounder - having a horrible cofounder is way worse than not having one.</li>
<li>You don't have to reveal your revenue to public or do any "open startup" thing. No pressure. Don't feel guilty if you are not doing what other cool kids are doing. You run your own company. You make your own decisions.</li>
<li>You don’t have to do XYZ that a Twitter VC philosopher urges you to do in a fortune-cookie-like tweet.</li>
<li>You don't have to be 100% bootstrap nor 100% VC-backed. Many things are not completely one way or the other. Usually, there's middle ground.</li>
<li>...and the list goes on.</li>
</ul>
<p>The bottom line is, not one is absolutely wrong or absolutely correct. Each individual's vision/knowledge is limited. Each person's preferences might be very different.</p>
<p>An API business may be too obscure to most people in the world, but I like my API business very much. People from big companies (like Apple, Amazon, or Microsoft) may examine my business and deem it “cute”. But I would consider it a success for me personally. </p>
<p>And success is relative. The key is to bring happiness to customers (by saving them time and money and helping them solve problems), myself (a professional achievement), and my family (by keeping the fridge full).</p>
<p>So why does the Listen API work?</p>
<h3 id="heading-demand-and-mvp"><strong>Demand and MVP</strong></h3>
<p>I didn’t build a solution to find problems. It was the problem (a podcast app that wanted to add search functionality) that found us—and we built a very simple solution at first.</p>
<p>We didn’t spend months launching the API. We spent a couple of hours. It costs at least $100 per hour to hire a not-so-bad engineer in San Francisco, so the cost of launching this API MVP was approximately $200. Even if it were $2,000, I'd still think it was worthwhile.</p>
<p>Two reasons why we were able to launch an MVP quickly:</p>
<ul>
<li>The heavy lifting part of building a podcast database, search engine, and data operations tool was already done, because of our podcast search engine website.</li>
<li>Mashape / RapidAPI existed to provide a plug-and-play solution for us to manage users and create paid plans without writing code on our end.</li>
</ul>
<p>However, in hindsight, it’s actually very common for a commercial search engine to license their tech (via API or other ways). Some examples:</p>
<ul>
<li>Yahoo Search was powered by Google circa 2000, and is powered by Bing today.</li>
<li>In the early days, Baidu's only business model was to put a web search on some Chinese portal sites</li>
<li>Today, Bing provides <a target="_blank" href="https://www.microsoft.com/en-us/bing/apis/bing-web-search-api">a bunch of search APIs</a>.</li>
</ul>
<p>By launching an MVP fast, we were able to get feedback early, especially after getting the first paying user only a month or so after launch.</p>
<h3 id="heading-good-documentation"><strong>Good documentation</strong></h3>
<p>User feedback proves that our <a target="_blank" href="https://www.listennotes.com/api/docs/">API Docs page</a> plays an important role in customers' decisions to use our API. There must be a reason for API companies to employ a whole team of engineers “only” to maintain their documentation pages.</p>
<p>Great documentation builds trust.</p>
<h3 id="heading-stable-backend-service"><strong>Stable backend service</strong></h3>
<p>Stability is the essential base of an API business’ <a target="_blank" href="https://en.wikipedia.org/wiki/Maslow%27s_hierarchy_of_needs">Maslow’s hierarchy of needs</a>. If an API is not stable at all (for example, it has frequent outages or runs very very slowly), it can't be used.</p>
<p>However, it’s boring to perform work to improve backend stability. Most tasks to stabilize backend services are preventive, including extensive monitoring and alerting, the process to deploy code with confidence, end-to-end regression tests, and so on.</p>
<p>No news is good news.</p>
<p>No outages are great news.</p>
<p>We use Statuspage.io to hook up our Datadog metrics to build a status page: listennotesstatus.com.  </p>
<p><img src="https://production.listennotes.com/web/image/8928e10cdf454a25b7b2c13ff513fbfe.png" alt="Image" width="600" height="400" loading="lazy">
<em><a target="_blank" href="https://www.listennotesstatus.com/">System status page of Listen Notes</a></em></p>
<p>Here’s hoping that the status page will convince our prospective users to try out our API :)</p>
<h3 id="heading-excellent-customer-service"><strong>Excellent customer service</strong></h3>
<p>We are all customers of someone else’s products and services. We have all been frustrated with poor customer service at some point in our lives. It’s obvious that great customer service goes a long way — <a target="_blank" href="https://en.wikipedia.org/wiki/Tony_Hsieh">RIP, Tony</a>.</p>
<p>Many people are likely not aware that <a target="_blank" href="https://aws.amazon.com/premiumsupport/pricing/">you have to pay AWS big money to access better customer service</a>!</p>
<p>Our customers don’t only pay us for using our API, an online service. They also pay for being able to get high-quality customer assistance from real human beings. In our case, it’s me, the person who built this thing.</p>
<p>I use <a target="_blank" href="https://superhuman.com/">Superhuman</a> to process emails promptly and efficiently. And I’ve got a ton of prewritten email templates to handle the most popular customer support tickets. Oftentimes I can reply to an email within 5 seconds, using CMD + K to select an email template.</p>
<h3 id="heading-invest-in-internal-tools-and-processes"><strong>Invest in internal tools and processes</strong></h3>
<p>For knowledge work, it’s possible that one single person (or a tiny team) can create 10x, 100x, or even 1,000x more value than a big team.</p>
<p>Let’s look at an extreme example: book publishing. It’s (almost) impossible to hire 10,000 good writers to collaborate on one book together and hope it’s “better” cohesively than Harry Potter, written by a single author.</p>
<p>JK Rowling, a single person, created way more value (in terms of measurable dollar amount and unmeasurable happiness, good times) than most companies with hundreds of employees in the world.</p>
<p>Eventually, the software business would grow in a similar way.</p>
<p><a target="_blank" href="https://www.dailymail.co.uk/news/article-2127343/Facebook-buys-Instagram-13-employees-share-100m-CEO-Kevin-Systrom-set-make-400m.html">We already witnessed the 13-employee Instagram get acquired for $1B in 2012</a>. When will we see a $1B+ software/internet company with 5 or fewer employees achieve the same feat?</p>
<p>Great internal tools and processes provide leverage to enable a tiny team to be super-efficient. This is easy to understand. We human beings already built a lot of tools to greatly extend our physical/mental limits, for example bikes and cars (versus walking), computers (versus manual calculation), and so on.</p>
<p>Given that it’s (almost) impossible to 100% automate an Internet business, we have to improve the efficiency of manual operations. It’s a great investment to increase human operators’ productivity.</p>
<h2 id="heading-tidbits-of-running-listen-api-as-a-business">Tidbits of running Listen API as a business</h2>
<p>Here are some things I didn’t know before…</p>
<h3 id="heading-anyone-can-sign-up-gt-submit-your-application-first"><strong>Anyone can sign up =&gt; Submit your application first</strong></h3>
<p>A few years ago, I noticed that certain APIs required me to submit an application first, describing my use case, before giving me an API key.</p>
<p>I didn’t understand the rationale back then.</p>
<p>After running my own API business, now I understand.</p>
<p>The Internet is huge. The world is gargantuan. There are good people and bad people. If the API you provide is useful, some folks will try to abuse your API.</p>
<p>That’s what happened when we initially allowed anyone to create an API account. We were seeing users creating dozens of accounts in order to get around the free quota limit.</p>
<p>Today, we require people to submit an application first. We get a notification via Slack. Then we use our internal tool to review and approve or reject the application. The applicant receives an automatic email. On our end, it’s two or three clicks to finish all these operations.</p>
<p>To assist our review process, we use a bunch of heuristics:</p>
<p>Did this user previously create multiple accounts?</p>
<p>Is this IP address a well-known spammer discoverable via stopforumspam.com? (hint: there's an API for that)</p>
<p>And so on…</p>
<p>Again, we are seeing new edge cases from time to time. Yet we are also learning how to handle those unique cases.</p>
<h3 id="heading-ideal-customers-and-interesting-customers"><strong>Ideal customers and interesting customers</strong></h3>
<p>Our best customers are mostly startup founders who have been in business for quite some time. </p>
<p>They can make decisions on their own. They understand the value we provide. They have the power to finalize purchase decisions. And they are competent enough to read our documentation autonomously and ask very few questions — or they don’t even talk to us at all.</p>
<p>On the other hand, people from well-funded VC-backed startups or huge companies (some of the biggest companies in the world) oftentimes ask for a discount or free trial, which we don’t have. Why? I don’t have a good answer here.</p>
<p>Of course, there are always exceptions.</p>
<h3 id="heading-dev-shops-and-coding-bootcamps"><strong>Dev shops and coding bootcamps</strong></h3>
<p>Many of our users hire freelancers or dev shops overseas to build apps and websites.</p>
<p>Generally speaking, developers from dev shops are not as good as in-house developers. Although not 100% true, the chance is quite high.</p>
<p>In essence, a bunch of my customer support replies are to teach Computer Science 101 . Sometimes they sent code snippets in PHP (or a language that I don’t know) to ask us to debug it via email.</p>
<p>I understand that some of those developers from dev shops are fresh out of coding bootcamps (or the dev shop itself is a coding bootcamp). Most of the time I will Google for them and send them a StackOverflow link or something like that. But occasionally, if I was in a bad mood, I would not reply to the “help me debug my PHP code” emails from FREE users who don’t pay us.</p>
<p>Also, quite a few coding bootcamps use our API to teach students how to write code, which is great. In real-world web projects, you can’t avoid using third-party REST APIs. Teaching new programmers how to talk to a REST API is necessary.</p>
<h3 id="heading-api-is-a-slow-business"><strong>API is a slow business</strong></h3>
<p>Usually it’ll take a user a few months to start paying us.</p>
<p>They need to add a big product feature or even build an entire app first. Then they need to do some marketing and get some traction. Finally, they pay, or they give up and shut down the app.</p>
<p>We definitely should think about how to help our users build product features fast.</p>
<p><a target="_blank" href="https://stripe.com/">Stripe</a> is doing a great job in this area. They built a lot of nice UI components that developers can directly use without writing tons of code, like <a target="_blank" href="https://stripe.com/payments/checkout">Checkout</a>.</p>
<h3 id="heading-api-is-a-stable-business"><strong>API is a stable business</strong></h3>
<p>Our churn rate is quite low. People spend many months building an app using our API, so it’s unlikely that they’ll switch to something else overnight.</p>
<p>I’m happy with that fact.</p>
<p>Meanwhile, I’m also very bullish on all the other API businesses out there, like Stripe, Plaid, and Twilio. (This isn’t investment advice, but look at the stock <a target="_blank" href="https://finance.yahoo.com/quote/TWLO/?guccounter=1&amp;guce_referrer=aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8&amp;guce_referrer_sig=AQAAAD_FoGY9a1EMiBkUZnYb_ByV8xNHfzcUKtujgYNthliWl55I0UWnIhIDivMvPxpFu5Fzuuyn1fh9lCU4p3tRZmjFFIJIxEKdx4Jlnp5U1Bb_HD4AZRMH3pri07JrBsKu6LqPk4M1ruR5QQefUPmS0Mg9-3R54fpr7AzYBnutkxbK">TWLO</a>.)</p>
<h3 id="heading-start-with-whales-then-diversify"><strong>Start with whales, then diversify</strong></h3>
<p>At the early stage, there might be a few user “whales” who account for a big portion or even most of the revenue.</p>
<p>Don’t panic.</p>
<p>Having revenue is still better than not having revenue at all.</p>
<p>We are not in a position to be picky at the early stage. We can diversify along the way.</p>
<p>I like reading <a target="_blank" href="http://www.investopedia.com/terms/s/sec-form-s-1.asp">S-1</a>s.</p>
<p>It’s not uncommon to see some SaaS or API companies with a few whales when they went public. If they lost one or two such whales, their revenue would drop 10%, or even 20%+ immediately! Well, they are already a public company. No need to worry about them. They know what to do next.</p>
<h3 id="heading-pricing-is-a-work-in-progress"><strong>Pricing is a work-in-progress</strong></h3>
<p>We are always experimenting with new pricing. Similar to building software projects in general, pricing is always a work in progress.</p>
<p>We allow old users to stick to the lower pricing they obtained when they signed up. Any future price changes won’t affect existing paying users. </p>
<p>I know that select pricing experts would warn me that I leave money on the table by this practice. But I feel thankful for customers who stand by us for so long. I want them to enjoy the low pricing as a benefit.</p>
<p>By the way, <a target="_blank" href="https://www.profitwell.com/">ProfitWell</a> has great resources regarding pricing.</p>
<h3 id="heading-haters-irrelevant-critiques"><strong>Haters / irrelevant critiques</strong></h3>
<p>You may have seen this theory: <a target="_blank" href="https://www.google.com/search?q=When+you+have+haters%2C+you%27re+doing+something+right">When you have haters, you’re doing something right</a>.</p>
<p>There’s a similar quote from <a target="_blank" href="https://en.wikipedia.org/wiki/Zeng_Guofan">Zeng Guofan</a> (one of the most important military leaders and politicians in the 19th century China):</p>
<p>不招人妒者皆庸才. “If no one envies you, then you are incompetent.”</p>
<p>Side note: You can find Zeng Guofan’s wisdom inside many airport bookstores in China. He would have been a great Twitter user and beat those Twitter VC philosophers if he were born in our time - it's hard to beat a historical Chinese figure in the game of fortune cookie :)</p>
<p>If your project is visible on the Internet and gets a bit of traction, some people will hate you for no particular reason.</p>
<p>Once you offer a paid service, you’ll never provide a price that is low enough to make everyone in the world happy. No, $1.00 USD is not cheap at all in many places in the world. People who are not your target users will complain about your pricing.</p>
<p>From my experience, it’s safe to ignore most critics, advice-givers, and suggestions from non-users. Sometimes people try to compare two things with similar names. </p>
<p>For example, if you search “podcast API” on Google, you’ll find a few other APIs with “podcast API” in their names. However, if you spend a few minutes skimming the documentation, you’ll find obvious differences. It’s like comparing two people with the same first name and family name who are two completely different individuals after all.</p>
<p>The only critiques or suggestions I care about are mostly from our users. I can see their API usage. I know they are expressing meaningful facts. So I listen to them.</p>
<h2 id="heading-so-are-you-interested-building-an-api-business"><strong>So are you interested building an API business?</strong></h2>
<p>Nowadays, the “passion economy” or “creator economy” is hot.</p>
<p>Who are creators? Writers, podcasters, streamers…</p>
<p>Don’t forget that software developers are also creators!</p>
<p>If you already run a website or have some interesting data, you may start an API business as well.</p>
<p>Thanks for reading this long article :) Let me know what you think: wenbin@listennotes.com. And you can <a target="_blank" href="https://www.listennotes.com/blog/how-i-accidentally-built-an-api-business-46/">read more of my posts on my blog</a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The SaaS Handbook – How to Build Your First Software-as-a-Service Product Step-By-Step ]]>
                </title>
                <description>
                    <![CDATA[ In this extensive write-up, I'll cover how all the main pieces came together for the first SaaS I ever launched. From implementing favicon to deploying to a cloud platform, I will share everything I learned. I'll also share extensive code snippets, b... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-your-first-saas/</link>
                <guid isPermaLink="false">66bb908dd2bda3e4315491d4</guid>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ kyw ]]>
                </dc:creator>
                <pubDate>Tue, 30 Jun 2020 15:52:15 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/The-SaaS-Handbook-Book-Cover--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this extensive write-up, I'll cover how all the main pieces came together for the first SaaS I ever launched.</p>
<p>From implementing favicon to deploying to a cloud platform, I will share everything I learned. I'll also share extensive code snippets, best practices, lessons, guides, and key resources. </p>
<p>I hope something here will be useful to you. Thanks for reading. ❤️</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#findingideas">Finding Ideas</a></li>
<li><a href="#thestack">The Stack</a></li>
<li><a href="#repo">Repo</a><ul>
<li><a href="#startfullstackdevelopmentlocally">Start full-stack development locally</a></li>
</ul>
</li>
<li><a href="#client">Client</a><ul>
<li><a href="#npmscriptclient">Npm Script</a></li>
<li><a href="#environmentvariables">Environment Variables</a></li>
<li><a href="#webpackbabel">Webpack &amp; Babel</a></li>
<li><a href="#webperformance">Web Performance</a></li>
<li><a href="#differentialserving">Differential Serving</a></li>
<li><a href="#fonts">Fonts</a></li>
<li><a href="#icons">Icons</a></li>
<li><a href="#favicon">Favicon</a></li>
<li><a href="#apicalls">API Calls</a></li>
<li><a href="#testproductionbuildlocally">Test Production Build Locally</a></li>
<li><a href="#security">Security</a></li>
</ul>
</li>
<li><a href="#design">Design</a><ul>
<li><a href="#modularscale">Modular Scale</a></li>
<li><a href="#colors">Colors</a></li>
<li><a href="#cssreset">CSS Reset</a></li>
<li><a href="#astylingpractice">A Styling Practice</a></li>
<li><a href="#layout">Layout</a></li>
</ul>
</li>
<li><a href="#server">Server</a><ul>
<li><a href="#filestructure">File Structure</a></li>
<li><a href="#npmscriptserver">Npm Script</a></li>
<li><a href="#database">Database</a></li>
<li><a href="#creatingtableschemas">Creating Table Schemas</a></li>
<li><a href="#droppingadatabase">Dropping a database</a></li>
<li><a href="#craftingsqlqueries">Crafting SQL Queries</a></li>
<li><a href="#redis">Redis</a></li>
<li><a href="#errorhandlinglogging">Error Handling &amp; Logging</a></li>
<li><a href="#permalinkforurlsharing">Permalink for URL Sharing</a></li>
</ul>
</li>
<li><a href="#userauthenticationsystem">User Authentication System</a></li>
<li><a href="#email">Email</a><ul>
<li><a href="#implementation">Implementation</a></li>
<li><a href="#emailtemplates">Email Templates</a></li>
<li><a href="#howtogetoneofthosehiexamplecom">How to get one of those <code><strong>hi@example.com</strong></code></a></li>
</ul>
</li>
<li><a href="#tenancy">Tenancy</a></li>
<li><a href="#domainname">Domain Name</a><ul>
<li><a href="#howtogetoneofthoseappexamplecom">How to get one of those <code><strong>app.example.com</strong></code></a></li>
</ul>
</li>
<li><a href="#deployment">Deployment</a><ul>
<li><a href="#deploynodejs">Deploy Nodejs</a></li>
<li><a href="#deploypostgresql">Deploy Postgresql</a></li>
<li><a href="#setupschemasinproductiondatabase">Setup schemas in production database</a></li>
<li><a href="#deployredis">Deploy Redis</a></li>
<li><a href="#filestorage">File Storage</a></li>
<li><a href="#deploynewchangesinbackend">Deploy New Changes in Back-end</a></li>
</ul>
</li>
<li><a href="#hostingyourspa">Hosting Your SPA</a><ul>
<li><a href="#deploynewchangesinfrontend">Deploy New Changes in Front-end</a></li>
</ul>
</li>
<li><a href="#richtexteditor">Rich-text Editor</a></li>
<li><a href="#cors">CORS</a></li>
<li><a href="#paymentsubscription">Payment &amp; Subscription</a></li>
<li><a href="#landingpage">Landing Page</a></li>
<li><a href="#termsandconditions">Terms and Conditions</a></li>
<li><a href="#marketing">Marketing</a></li>
<li><a href="#wellbeing">Well-being</a></li>
</ul>
<h2 id="heading-introduction">Introduction</h2>
<p>I switched careers to web development back in 2013. I did it for two reasons. </p>
<p>First, I noticed I could get lost in building customer-facing products among all the colors and endless possibilities for interactivity. So while being reminded of the trite "<em>Find a job you enjoy doing, and you will never have to work a day in your life</em>", I thought "<em>Why not make this a job?</em>" </p>
<p>And second, I wanted to make something of myself, having spent my teenage years inspired by Web 2.0 (Digg.com circa 2005 opened the world for me!). The plan was to work on the latter while working in the former.</p>
<p>Turns out, though, that the job and the 'JavaScript fatigue' ensued and wholly consumed me. It also didn't help that I was reckless in my pursuit of my ambition, after being influenced by the rhetoric from 'Silicon Valley'. I read Paul Graham's <em>Hackers &amp; Painters</em> and Peter Thiel's <em>Zero to One</em>. I thought, I'm properly fired up! I'm hustling. I can do this too!</p>
<p>But nope, I couldn't. At least not alone. I was always beat after work. I couldn't find a team that shared my dreams and values. </p>
<p>So meanwhile, I rinsed and repeated less than half-baked projects in my free time. I was chronically anxious and depressed. I mellowed out as the years went by. And I began to cultivate a personal philosophy on entrepreneurship and technology that aligned better with my personality and life circumstances – until September 2019.</p>
<p>The fog in the path ahead finally cleared up. I got pretty good at it – the job then became less taxing, and I'd reined in my 'Javascript fatigue'. For the longest time, I had the mental energy, time, and the mindset that allowed me to see through a side project. And that time, I started small. I believed I had this!</p>
<p>I was wrong.</p>
<p>Since I had been a front-end developer for my entire career, I could only go as far as naming the things that I imagined I would need – a 'server', a 'database', an 'authentication' system, a 'host', a 'domain name', but <em>how</em>... <em>where</em>... and <em>what</em>...<em>I..I don..I don't even</em>... ?</p>
<p>Now, I knew my life would have been easier if I'd decided to use one of those abstract tools like 'create-react-app', 'firebase SDK', 'ORM', and 'one-click-deployment' services. The ode of '<em>Don't reinvent the wheel. Iterate fast</em>'. </p>
<p>But there were a few qualifications I wanted my decisions to meet:</p>
<ul>
<li>No vendor lock-in — This ruled out using the Firebase SDK all over my codebase. This included 'create-react-app', because ejecting it forced me to inherit and maintain its massive tooling infrastructure.</li>
<li>Simple &amp; Minimalistic — Cut having to learn new opinionated syntax and patterns. This ruled out 1) Project generators that output complex architecture and layers of boilerplate codes, 2) Using third-party libraries such as 'knex.js' or the 'sequelize' ORM.</li>
<li>Pay-as-you-need — I wanted to keep my operating cost proportional to the usage level. This ruled out services such as 'one-click-deployment'.</li>
</ul>
<p>To be fair, I had the following things going for me:</p>
<ul>
<li>I was building a simple SaaS.</li>
<li>I was not anxious to scale, dominate, disrupt etc.</li>
<li>I was still holding my day job.</li>
<li>I had accepted my odds of failure. ?</li>
</ul>
<p>Also keep in mind that:</p>
<ul>
<li>This was a one-man show—design, development, maintenance, marketing, etc.</li>
<li>I'm not a 10x rockstar full-stack programmer.</li>
</ul>
<p><strong>Most importantly</strong>, I wanted to follow through with a guiding principle: Building things <a target="_blank" href="https://alistapart.com/article/responsible-javascript-part-1/"><em>responsibly</em></a>. Although, unsurprisingly, doing so had had a significant impact on my development speed, and it forced me to clarify my motivations:</p>
<ul>
<li>If I had to ship something as soon as possible, unless it was a matter of life and death, then I probably wasn't solving a unique and hard problem. In that case—assuming I was still at my day job and had zero debt— What was the rush?</li>
<li>And second-guessing from the ethical perspective: Was it even a problem that needed solving? What would be the second-order consequences if I solved it? Could my good intentions be better directed elsewhere?</li>
</ul>
<p>So what follows in this article is everything I've learned while developing the first project I ever launched called <strong>Sametable</strong> that helps <a target="_blank" href="https://www.sametable.app">managing your work in spreadsheets</a>. </p>
<p>Let's get to it.</p>
<h2 id="heading-finding-ideas">Finding Ideas</h2>
<p>Well, first of all, you need to know what you want to build. I used to lose sleep over this, thinking about and remixing ideas, hoping for eureka moments, until I started to look inward:</p>
<ul>
<li>Build things that solve problems that you encounter and piss you off frequently.</li>
<li>Solve the so-called 'pain points' or 'frictions'. Go outside, don't stop listening to people and learn from them.</li>
<li>Be an expert in your domain. Feel its pains. Maybe solve one of them. Seems to me lots of founders founded company related to their domain on which they have built their career and social network.</li>
</ul>
<h2 id="heading-the-stack">The Stack</h2>
<p>How your stack looks will depend on how you want to render your application. Here is a <a target="_blank" href="https://developers.google.com/web/updates/2019/02/rendering-on-the-web#wrapup">comprehensive</a> discussion about that, but in a nutshell:</p>
<ul>
<li><p><strong>Client-side rendering(CSR); SPA; JSON APIs</strong> —
This is perhaps the most popular approach. It's great for building interactive web applications. But <a target="_blank" href="https://macwright.org/2020/05/10/spa-fatigue.html">be aware</a> of its downsides and steps to mitigate them. This is the approach I took, so we will talk about it in a lot of detail.</p>
</li>
<li><p><strong>Hybrid CSR; Both client-side and server-side rendering(SSR)</strong> —
With this approach, you still build your SPA. But when a user requests your app, for example, the homepage, you render the homepage's component into its static HTML <strong>in your server</strong> and serve it to the user. Then at the user's browser, <a target="_blank" href="https://reactjs.org/docs/react-dom.html#hydrate">hydration</a> will happen so the whole thing becomes the intended SPA.</p>
</li>
</ul>
<p>The main benefits of this approach are that you get good SEO and users can see your stuff sooner (faster 'First Meaningful Paint'). </p>
<p>But there are downsides too. Apart from the extra maintenance costs, we will have to download the same payload twice—First, the HTML, and second, its Javascript counterpart for the 'hydration' which will exert significant work on the browser's main thread. This prolongs the 'First time to interactive', and hence diminishes the benefits gained from a faster 'First meaningful paint'.</p>
<p>  The technologies that are adopted for this approach are <a target="_blank" href="https://nextjs.org/">NextJs</a>, <a target="_blank" href="https://nuxtjs.org/">NuxtJs</a>, and <a target="_blank" href="https://www.gatsbyjs.org/">GatsbyJs</a>.</p>
<ul>
<li><p><strong>Server-side rendering and 'sprinkle ✨ it with Javascript'</strong>
— This was the old-school way of building on the web!—Use PHP to build your templates with data in your server, then bind events handlers to the DOM with jQuery in the browser. This approach might have been ill-suited to build the increasingly complex apps that businesses have asked for on the web, but some technologies have emerged to warrant a reconsideration:</p>
<ul>
<li><a target="_blank" href="https://stimulusjs.org/">https://stimulusjs.org/</a></li>
<li><a target="_blank" href="https://github.com/turbolinks/turbolinks">https://github.com/turbolinks/turbolinks</a></li>
<li><a target="_blank" href="https://github.com/phoenixframework/phoenix_live_view">https://github.com/phoenixframework/phoenix_live_view</a></li>
<li>For more, check out this <a target="_blank" href="https://mobile.twitter.com/nateberkopec/status/1260602209475198976">twitter thread</a></li>
</ul>
</li>
</ul>
<p>To be honest, if I was more patient with myself, I would have gone down this path. This approach is making a comeback in light of the excess of Javascript on this modern web.</p>
<p>The bottom line is: Pick any approach you are already proficient with. But be mindful of the associated downsides, and try to mitigate them before shipping to your users.</p>
<p>With that, here is the boring stack of Sametable:</p>
<h3 id="heading-front-end">Front-end</h3>
<ul>
<li>Webpack, Babel</li>
<li><strong>Preact</strong></li>
</ul>
<h3 id="heading-back-end">Back-end</h3>
<ul>
<li><strong>Node</strong> — API server with ExpressJS</li>
<li><strong>Postgresql</strong> — Database</li>
<li><strong>Redis</strong> — Store users' session data and cache queries' results.</li>
</ul>
<h3 id="heading-hosting">Hosting</h3>
<ul>
<li><strong>Google Cloud Platform</strong> — GAE for hosting Nodejs, GCE for hosting Redis.</li>
<li><strong>Firebase</strong> — For hosting my SPA.</li>
</ul>
<p><img src="https://i.imgur.com/CA88ijh.png" alt="Sametable architecture" width="600" height="400" loading="lazy"></p>
<h2 id="heading-repo">Repo</h2>
<p><a target="_blank" href="https://github.com/kilgarenone/boileroom">https://github.com/kilgarenone/boileroom</a></p>
<p>This repo contains the structure I'm using to develop my SaaS. I have one folder for the <strong>client</strong> stuff, and another for the <strong>server</strong> stuff:</p>
<pre><code class="lang-json">- client
    - src
      - components
      - index.html
      - index.js
    - package.json
    - webpack.config.js
    -.env
    -.env.development
- server
    - server.js
    - package.json
    - .env
- package.json
- .gitignore
- .eslintrc.js
- .prettierrc.js
- .stylelintrc.js
</code></pre>
<p>The file structure always aims to be flat, cohesive, and as linear to navigate as possible. Each 'component' is self-contained within a folder with all its constituent files (html|css|js). For example, in a 'Login' route folder:</p>
<pre><code class="lang-xml">- client
   - src
     - routes
       - Login
         - Login.js
         - Login.scss
         - Login.redux.js
</code></pre>
<p>I learned this from the <a target="_blank" href="https://angular.io/guide/styleguide#angular-coding-style-guide">Angular2 style guide</a> which has a lot of other good stuff you can take away. Highly recommended.</p>
<h3 id="heading-start-full-stack-development-locally">Start Full-stack Development Locally</h3>
<p>The <code>package.json</code> at the root has a <strong>npm script</strong> that I will run to boot up <strong>both</strong> my client and server to begin my local development:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"client"</span>: <span class="hljs-string">"cd client &amp;&amp; npm run dev"</span>,
    <span class="hljs-attr">"server"</span>: <span class="hljs-string">"cd server &amp;&amp; npm run dev"</span>,
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"npm-run-all --parallel server client"</span>
}
</code></pre>
<p>Run the following in a terminal at your project's root:</p>
<pre><code class="lang-json">npm run dev
</code></pre>
<h2 id="heading-client">Client</h2>
<pre><code class="lang-json">- client
    - src
      - components
      - index.html
      - index.js
    - package.json
    - webpack.config.js
    -.env
    -.env.development
</code></pre>
<p>The file structure of the 'client' is quite like that of the 'create-react-app'. The meat of your application code is inside the <code>src</code> folder in which there is a <code>components</code> folder for your functional React components; <code>index.html</code> is your <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/config/webpack.development.js#L41-L43">custom template</a> provided to the <a target="_blank" href="https://github.com/jantimon/html-webpack-plugin#options"><code>html-webpack-plugin</code></a>; <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/src/index.js"><code>index.js</code></a> is a file as a <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/config/webpack.common.js#L12-L15">entry point</a> to Webpack.</p>
<aside>
<p><strong>Note:</strong> I have since restructured my build environment to achieve <a href="#differential-serving">differential serving</a>. Webpack and babel have been organized differently, and npm scripts changed a bit. Everything else remains the same.</p>
</aside>

<h3 id="heading-npm-scriptclient">Npm Script(Client)</h3>
<p>The client's <code>package.json</code> file has two most important npm scripts: 1) <code>dev</code> to start development, 2) <code>build</code> to bundle for production.</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"cross-env NODE_ENV=development webpack-dev-server"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"cross-env NODE_ENV=production node_modules/.bin/webpack"</span>
}
</code></pre>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>It's a good practice to have a <code>.env</code> file where you define your sensitive values such as API keys and database credentials:</p>
<pre><code class="lang-json">SQL_PASSWORD=admin
STRIPE_API_KEY=<span class="hljs-number">1234567890</span>
</code></pre>
<p>A library called <a target="_blank" href="https://www.npmjs.com/package/dotenv">dotenv</a> is usually used to load these variables into our application code for consumption. However, in the context of Webpack, we will use <a target="_blank" href="https://www.npmjs.com/package/dotenv-webpack">dotenv_webpack</a> to do that during compile and build time <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/config/webpack.common.js#L33-L37">as shown here</a>. The variables will then be accessible in the <code>process.env</code> object in your codebase:</p>
<pre><code class="lang-js"><span class="hljs-comment">// payment.jsx</span>

<span class="hljs-keyword">if</span> (process.env.STRIPE_API_KEY) {
  <span class="hljs-comment">// do stuff</span>
}
</code></pre>
<h3 id="heading-webpack-amp-babel">Webpack &amp; Babel</h3>
<p>Webpack is used to lump all my UI components and its dependencies (npm libraries, files like images, fonts, SVG) into appropriate files like <code>.js</code>, <code>.css</code>, <code>.png</code> files. During the bundling, Webpack will run through my <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/config/webpack.production.js#L19-L57">babel config</a>, and, if necessary, transpiles the Javascript I have written to an older version(e.g. es5) to support my <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/package.json#L13-L27">targeted browsers</a>.</p>
<p>When Webpack has done its job, it will have generated one (or <a target="_blank" href="https://webpack.js.org/concepts/entry-points/#multi-page-application">several</a>) <code>.js</code> and <code>.css</code> files. Then by <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/config/webpack.production.js#L189-L202">using</a> a webpack plugin called <a target="_blank" href="https://github.com/jantimon/html-webpack-plugin">'html-webpack-plugin'</a>, references to those JS and CSS files are automatically (default behaviour) injected respectively as <code>&lt;script&gt;</code> and <code>&lt;link</code> in your <code>index.html</code>. Then when a user requests your app in a browser, the 'index.html' is fetched and parsed. When it sees <code>&lt;script&gt;</code> and <code>&lt;link&gt;</code>, it will fetch and execute the referenced assets, and finally your app is <a target="_blank" href="https://preactjs.com/guide/v10/api-reference/#render">rendered</a>(i.e. client-side rendering) in all its glories to the user.</p>
<p>If you are new to Webpack/Babel, I'd suggest learning them from their first principles to slowly build up your configuration instead of copy/pasting bits from the web. Nothing wrong with that, but I find it makes more sense doing it once I have the mental models of how things work.</p>
<p>I wrote about the basics here:</p>
<ul>
<li><strong><a target="_blank" href="https://medium.com/@kilgarenone/minimal-webpack-setup-a5f32c5f8960">Webpack</a></strong></li>
</ul>
<p>Once I understood the basics, I started <a target="_blank" href="https://github.com/nystudio107/annotated-webpack-4-config">referring to this resource</a> for more advanced configuration.</p>
<ul>
<li><strong><a target="_blank" href="https://medium.com/@kilgarenone/minimal-babel-setup-b12b563ee2ca">Babel</a></strong></li>
</ul>
<h3 id="heading-web-performance">Web Performance</h3>
<p>To put it simply, a web app that performs well is good for your <a target="_blank" href="https://developers.google.com/web/fundamentals/performance/why-performance-matters">users and business</a>.</p>
<p>Although web perf is a huge subject that's <a target="_blank" href="https://web.dev/fast/">well documented</a>, I would like to talk about few of the most impactful things I do for web perf (apart from <a target="_blank" href="https://images.guide/">optimizing the images</a> which can account for over 50% of a page's weight).</p>
<h4 id="heading-critical-rendering-path">Critical rendering path</h4>
<p>The goal of optimizing for the 'critical rendering path' in your page is to have it rendered and be interactive the soonest possible to your users. Let's do that.</p>
<p>We mentioned before that 'html-webpack-plugin' automatically injects references of all Webpack-generated <code>.js</code> and <code>.css</code> files for us in our <code>index.html</code>. But we don't want to do that now to have full control over their placement and applying the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content">resource hints</a>, both of which are a factor in how efficient a browser discovers and downloads them as chronicled <a target="_blank" href="https://timkadlec.com/remembers/2020-02-13-when-css-blocks/">in this article</a>.</p>
<p>Now, there are Webpack <a target="_blank" href="https://github.com/jantimon/html-webpack-plugin#plugins">plugins</a> that seem to help us in this respect, but:</p>
<ul>
<li>There was no intuitive way to control the ordering of my <code>&lt;script</code>. Well, there is <a target="_blank" href="https://github.com/jantimon/html-webpack-plugin/issues/140#issuecomment-376316414">this method</a>, but how about ordering among my <code>&lt;link&gt;</code> too?</li>
<li>There was no plugin that <code>preload</code> my CSS the way I wanted as we will see later. Well, there is <a target="_blank" href="https://github.com/GoogleChrome/preload-webpack-plugin">this</a> (no control over attributes), <a target="_blank" href="https://github.com/jantimon/resource-hints-webpack-plugin">this</a> (same), and <a target="_blank" href="https://github.com/numical/style-ext-html-webpack-plugin">this</a> (no clear support for MiniCssExtractPlugin).</li>
</ul>
<p>Even if I could've somehow hack them all together, I would have decided against it in a heartbeat if I'd known I could do it in an intuitive and controlled way. And I did.</p>
<p>So go ahead and disable the auto-injection:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// webpack.production.js</span>
<span class="hljs-attr">plugins</span>: [
  <span class="hljs-keyword">new</span> HtmlWebpackPlugin({
    <span class="hljs-attr">template</span>: settings.templatePath,
    <span class="hljs-attr">filename</span>: <span class="hljs-string">"index.html"</span>,
    <span class="hljs-attr">inject</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// we will inject ourselves</span>
    <span class="hljs-attr">mode</span>: process.env.NODE_ENV,
  }),
];
</code></pre>
<p>And knowing that we can grab Webpack-generated assets from the <a target="_blank" href="https://github.com/jantimon/html-webpack-plugin#writing-your-own-templates"><code>htmlWebpackPlugin.files</code></a> object inside our <code>index.html</code>:</p>
<pre><code class="lang-json"><span class="hljs-comment">// example of what you would see if you</span>
<span class="hljs-comment">// console.log(htmlWebpackPlugin.files)</span>

{
  <span class="hljs-attr">"publicPath"</span>: <span class="hljs-string">"/"</span>,
  <span class="hljs-attr">"js"</span>: [
    <span class="hljs-string">"/js/runtime.a201e1a.js"</span>,
    <span class="hljs-string">"/vendors~app.d8e8c.js"</span>,
    <span class="hljs-string">"/app.f8fb511.js"</span>,
    <span class="hljs-string">"/components.3811eb.js"</span>
  ],
  <span class="hljs-attr">"css"</span>: [<span class="hljs-string">"/app.5597.css"</span>, <span class="hljs-string">"/components.b49d382.css"</span>]
}
</code></pre>
<p>We inject our assets in <code>index.html</code> ourselves:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">%</span> <span class="hljs-attr">if</span> (<span class="hljs-attr">htmlWebpackPlugin.options.mode</span> === <span class="hljs-string">'production'</span>) { %&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span>
  <span class="hljs-attr">defer</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"&lt;%= htmlWebpackPlugin.files.js.filter(e =&gt; /^\/vendors/.test(e))[0] %&gt;"</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>
  <span class="hljs-attr">defer</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"&lt;%= htmlWebpackPlugin.files.js.filter(e =&gt; /^\/app/.test(e))[0] %&gt;"</span>
&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"&lt;%= htmlWebpackPlugin.files.css.filter(e =&gt; /app/.test(e))[0] %&gt;"</span>
/&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">%</span> } %&gt;</span>
</code></pre>
<p>Note:</p>
<ul>
<li>We only do this when building for production; we let <code>webpack-dev-server</code> injects for us during local development.</li>
<li><p>We apply the <code>defer</code> attribute on our <code>&lt;script&gt;</code> so that browser will fetch them <em>while</em> parsing our HTML, and only execute the JS once the HTML has been parsed.</p>
<figure>
<img src="https://i.imgur.com/cF7jPjB.png" alt="defer diagram" width="600" height="400" loading="lazy">
<figcaption><a href="https://hacks.mozilla.org/2017/09/building-the-dom-faster-speculative-parsing-async-defer-and-preload/">source</a></figcaption>
</figure>

</li>
</ul>
<h4 id="heading-inlining-css-and-js">Inlining CSS and JS</h4>
<p>If you <a target="_blank" href="https://web.dev/extract-critical-css/#overview-of-tools">managed</a> to separate your <em>critical</em> CSS or you have a tiny JS script, you might want to consider inlining them in <code>&lt;style&gt;</code> and <code>&lt;script&gt;</code>. </p>
<p>'Inlining' means placing corresponding raw content in HTML. This saves network trips, although not being able to cache them is a concern worth factoring in.</p>
<p>Let's inline the <code>runtime.js</code> generated by Webpack as suggested <a target="_blank" href="https://developers.google.com/web/fundamentals/performance/webpack/use-long-term-caching#inline_webpack_runtime_to_save_an_extra_http_request">here</a>. Back in the <code>index.html</code> above, add this snippet:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- more &lt;link&gt; and &lt;script&gt; --&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  &lt;%= compilation.assets[htmlWebpackPlugin.files.js.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> <span class="hljs-regexp">/runtime/</span>.test(e))[<span class="hljs-number">0</span>].substr(htmlWebpackPlugin.files.publicPath.length)].source() %&gt;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>The key was the <code>compilation.assets[&lt;ASSET_FILE_NAME&gt;].source()</code>:</p>
<blockquote>
<ul>
<li>compilation: the webpack <a target="_blank" href="https://webpack.js.org/api/compilation-object/">compilation object</a>. This can be used, for example, to get the contents of processed assets and inline them directly in the page, through <code>compilation.assets[...].source()</code> (see <a target="_blank" href="https://github.com/jantimon/html-webpack-plugin/blob/master/examples/inline/template.pug">the inline template example</a>). (<a target="_blank" href="https://github.com/jantimon/html-webpack-plugin#writing-your-own-templates">source</a>)</li>
</ul>
</blockquote>
<p>You can use this method to inline your critical CSS too:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="xml">
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span> <span class="hljs-attr">compilation.assets</span>[<span class="hljs-attr">htmlwebpackplugin.files.css.filter</span>(<span class="hljs-attr">e</span> =&gt;</span> /app/.test(e)) [0].substr(htmlWebpackPlugin.files.publicPath.length) ].source() %&gt;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>For non-critical CSS, you can consider 'preload' them.</p>
<h4 id="heading-preload-non-critical-css">Preload non-critical CSS</h4>
<p>In short:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"/path/to/my.css"</span>
  <span class="hljs-attr">media</span>=<span class="hljs-string">"print"</span>
  <span class="hljs-attr">onload</span>=<span class="hljs-string">"this.media='all'"</span>
/&gt;</span>
</code></pre>
<p><a target="_blank" href="https://timkadlec.com/remembers/2020-02-13-when-css-blocks/">source</a></p>
<p>But let's see how to do this with Webpack.</p>
<p>So I have my non-critical CSS contained in a CSS file, which I specify as its own entry point in Webpack:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// webpack.config.js</span>
<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">entry</span>: {
    <span class="hljs-attr">app</span>: <span class="hljs-string">"index.js"</span>,
    <span class="hljs-attr">components</span>: path.resolve(__dirname, <span class="hljs-string">"../src/css/components.scss"</span>),
  },
};
</code></pre>
<p>Finally, I inject it above my critical CSS:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- Preloading non-critical CSS --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span>
  <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
  <span class="hljs-attr">href</span>=<span class="hljs-string">"&lt;%= htmlWebpackPlugin.files.css.filter(e =&gt; /components/.test(e))[0] %&gt;"</span>
  <span class="hljs-attr">media</span>=<span class="hljs-string">"print"</span>
  <span class="hljs-attr">onload</span>=<span class="hljs-string">"this.media='all'"</span>
/&gt;</span>

<span class="hljs-comment">&lt;!-- Inlined critical CSS --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="xml">
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span> <span class="hljs-attr">compilation.assets</span>[<span class="hljs-attr">htmlwebpackplugin.files.css.filter</span>(<span class="hljs-attr">e</span> =&gt;</span> /app/.test(e)) [0].substr(htmlWebpackPlugin.files.publicPath.length) ].source() %&gt;
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>Let's <strong>measure</strong> if, after all this, we have actually done anything good. Measuring the Sametable's <a target="_blank" href="https://web.sametable.app/signup">signup page</a>:</p>
<p><strong>BEFORE</strong>
<img src="https://i.imgur.com/rfy7og8.png" width="600" height="400" alt="rfy7og8" loading="lazy"></p>
<p><strong>AFTER</strong>
<img src="https://i.imgur.com/n5llJWx.png" width="600" height="400" alt="n5llJWx" loading="lazy"></p>
<p>Looks like we have improved almost all of the important user-centric metrics (not sure about the First Input Delay..)! ?</p>
<p>Here is a <a target="_blank" href="https://www.youtube.com/watch?v=j9LW94EN7n4">good video tutorial</a> about measuring web performance in the Chrome Dev tool.</p>
<h4 id="heading-code-splitting">Code splitting</h4>
<p>Rather than lump all your app's components, routes, and third-party libraries into a single <code>.js</code> file, you should split and load them on-demand based on a user's action at runtime. </p>
<p>This will <strong>dramatically</strong> reduce the bundle size of your SPA and reduces initial Javascript processing costs. This improves metrics like 'First interactive time' and 'First meaningful paint'.</p>
<p>Code splitting is done with the <a target="_blank" href="https://webpack.js.org/guides/code-splitting/#dynamic-imports">'dynamic imports'</a>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Editor.jsx</span>

<span class="hljs-comment">// LAZY-LOAD A GIGANTIC THIRD-PARTY LIBRARY</span>
componentDidMount() {
  <span class="hljs-keyword">const</span> { <span class="hljs-attr">default</span>: MarkdownIt } = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(
    <span class="hljs-comment">/* webpackChunkName: "markdown-it" */</span>
    <span class="hljs-string">"markdown-it"</span>
  );
  <span class="hljs-keyword">new</span> MarkdownIt({ <span class="hljs-attr">html</span>: <span class="hljs-literal">true</span> }).render(<span class="hljs-comment">/* stuff */</span>);
}

<span class="hljs-comment">// OR LAZY-LOAD A COMPONENT BASED ON USER ACTION</span>
checkout = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { <span class="hljs-attr">default</span>: CheckoutModal } = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(
    <span class="hljs-comment">/* webpackChunkName: "checkoutModal" */</span>
    <span class="hljs-string">"../routes/CheckoutModal"</span>
  );
}
</code></pre>
<p>Another use case for code splitting is to <strong>conditionally load polyfill</strong> for a Web API in a browser that doesn't support it. This spares others that do support it from paying the cost of the polyfill.</p>
<p>For example, if <code>IntersectionObserver</code> isn't supported, we will polyfill it with the <a target="_blank" href="https://www.npmjs.com/package/intersection-observer">'intersection-observer'</a> library:</p>
<pre><code class="lang-js"><span class="hljs-comment">// InfiniteScroll.jsx</span>

componentDidMount() {
  (<span class="hljs-built_in">window</span>.IntersectionObserver ? <span class="hljs-built_in">Promise</span>.resolve() : <span class="hljs-keyword">import</span>(<span class="hljs-string">"intersection-observer"</span>)).then(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">this</span>.io = <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.IntersectionObserver(<span class="hljs-function">(<span class="hljs-params">entries</span>) =&gt;</span> {
      entries.forEach(<span class="hljs-function">(<span class="hljs-params">entry</span>) =&gt;</span> {
        <span class="hljs-comment">// do stuff</span>
      });
    }, { <span class="hljs-attr">threshold</span>: <span class="hljs-number">0.5</span> });

    <span class="hljs-built_in">this</span>.io.observe(<span class="hljs-comment">/* DOM element */</span>);
  });
}
</code></pre>
<h5 id="heading-guide">Guide</h5>
<ul>
<li><a target="_blank" href="https://medium.com/@kilgarenone/pragmatic-code-splitting-with-preact-and-webpack-a3d3b19f86a3">https://medium.com/@kilgarenone/pragmatic-code-splitting-with-preact-and-webpack-a3d3b19f86a3</a></li>
</ul>
<h3 id="heading-differential-serving">Differential Serving</h3>
<p>You have probably configured your Webpack to build your app targeting both modern and legacy browsers like IE11, while serving every user with the same payload. This forces those users who are on modern browsers to pay the cost (parse/compile/execute) of unnecessary polyfills and extraneous transformed codes that are meant to support users on legacy browsers.</p>
<p>'Differential serving' will serve, on one hand, much leaner code to users on modern browsers. And on the other hand, it'll serve properly polyfilled and transformed code to support users on legacy browsers such as IE11.</p>
<p>Although this approach makes for an even more complex build setup and doesn't come without a <a target="_blank" href="https://philipwalton.com/articles/deploying-es2015-code-in-production-today/#double-download-issue">few caveats</a>, the benefits gained (you can find in the resources below) certainly <em>outweigh</em> the costs. That is unless the majority of your user base is on IE11. In that case, you can probably skip this. But even so, this approach is future-proof as legacy browsers are being phased out.</p>
<h4 id="heading-repo-1">Repo</h4>
<p><a target="_blank" href="https://github.com/kilgarenone/differential-serving">https://github.com/kilgarenone/differential-serving</a></p>
<h4 id="heading-resources">Resources</h4>
<ul>
<li><a target="_blank" href="https://jasonformat.com/modern-script-loading/#option1loaddynamically">https://jasonformat.com/modern-script-loading/#option1loaddynamically</a> — A very good overview of different approaches to differential serving. Sametable is on the 'Option-1'.</li>
<li><a target="_blank" href="https://github.com/firsttris/html-webpack-multi-build-plugin">https://github.com/firsttris/html-webpack-multi-build-plugin</a> — This Webpack plugin passes the manifest(i.e. assets' reference) of your modern &amp; legacy scripts to 'html-webpack-plugin' so you can access them in your 'index.html'.</li>
<li><a target="_blank" href="https://calendar.perfplanet.com/2018/doing-differential-serving-in-2019/">https://calendar.perfplanet.com/2018/doing-differential-serving-in-2019/</a> — I learned here about structuring my babel config with its 'babel.config.js' method.</li>
<li><a target="_blank" href="https://github.com/nystudio107/annotated-webpack-4-config">https://github.com/nystudio107/annotated-webpack-4-config</a> — I learned a lot here about structuring my Webpack configs.</li>
</ul>
<h3 id="heading-fonts">Fonts</h3>
<p>Font files can be costly. Take my favorite font <a target="_blank" href="https://rsms.me/inter/">Inter</a> for example: If I used 3 of its font styles, the total size could get up to 300KB, exacerbating the FOUT and FOIT situations, particularly in low-end devices.</p>
<p>To meet my font needs in my projects, I usually just go with the 'system fonts' that come with the machines:</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">font-family</span>: -apple-system, BlinkMacSystemFont, <span class="hljs-string">"Segoe UI"</span>, Roboto,
    Oxygen-Sans, Ubuntu, Cantarell, <span class="hljs-string">"Helvetica Neue"</span>, sans-serif;
}

<span class="hljs-selector-tag">code</span> {
  <span class="hljs-attribute">font-family</span>: SFMono-Regular, Menlo, Monaco, Consolas, <span class="hljs-string">"Liberation Mono"</span>,
    <span class="hljs-string">"Courier New"</span>;
}
</code></pre>
<p>But if you must use custom web fonts, consider doing it right:</p>
<ul>
<li>You should <a target="_blank" href="https://kevq.uk/how-to-self-host-your-web-fonts/">host</a> them <a target="_blank" href="https://www.tunetheweb.com/blog/should-you-self-host-google-fonts/">yourself</a>.</li>
<li><a target="_blank" href="https://medium.com/@kilgarenone/subsetting-your-fonts-in-windows-10-using-wsl-bae4fafa35fc">'Font-subsetting'</a> to dramatically reduce the size of the font file.</li>
<li>Go through this <a target="_blank" href="https://www.zachleat.com/web/font-checklist/">checklist</a>.</li>
</ul>
<h3 id="heading-icons">Icons</h3>
<p>Icons in Sametable are SVG. There are different ways that you can do it:</p>
<ul>
<li>Copy and paste the markup of an SVG icon wherever you need it. The downside is it will bloat the HTML and incur parsing costs particularly on mobile.</li>
<li>Request for your SVG icons over the network:<code>&lt;img src="./tick.svg" /&gt;</code>. Unless an SVG is huge (&gt; 5KB), making a request for every one of them seems a bit much.</li>
<li>Make an icon reusable in the form of a <a target="_blank" href="https://medium.com/@david.gilbertson/icons-as-react-components-de3e33cb8792">React component</a>. The downside is it unnecessarily introduces Javascript and its associated costs.</li>
</ul>
<p>Instead, the solution I opted for my icons was '<strong>SVG sprites</strong>' which is closer to the nature of SVG itself ( <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use"><code>&lt;use&gt;</code></a> and <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol"><code>&lt;symbol&gt;</code></a>).</p>
<p>Let's see how.</p>
<p>Say there are many places that will use two of our SVG icons. In your <code>index.html</code>:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"display: none;"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">symbol</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"pin-it"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 96 96"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Give it a title<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">desc</span>&gt;</span>Give it a description for accessibility<span class="hljs-tag">&lt;/<span class="hljs-name">desc</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M67.7 40.3c-.3 2.7-2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">symbol</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">symbol</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"unpin-it"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 96 96"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Un-pin this entity<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">desc</span>&gt;</span>Click to un-pin this entity<span class="hljs-tag">&lt;/<span class="hljs-name">desc</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M67.7 40.3c-.3 2.7-2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">symbol</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<ol>
<li>Hide the parent SVG element <code>style="display: none"</code>.</li>
<li>Give each SVG symbol an unique id <code>&lt;symbol id="unique-id"</code>.</li>
<li>Make sure to define the <code>viewBox</code>(usually already provided), but skip the <code>width</code> and <code>height</code>.</li>
<li>Give it <code>title</code> and <code>desc</code> for accessibility.</li>
<li>And of course, the <code>path</code> data of an icon.</li>
</ol>
<p>And finally, here is how you can use them in your components:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// example.jsx</span>

render() {
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">svg</span>
    <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>
    <span class="hljs-attr">xmlnsXlink</span>=<span class="hljs-string">"http://www.w3.org/1999/xlink"</span>
    <span class="hljs-attr">width</span>=<span class="hljs-string">"24"</span>
    <span class="hljs-attr">height</span>=<span class="hljs-string">"24"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">use</span> <span class="hljs-attr">xlinkHref</span>=<span class="hljs-string">"#pin-it"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span></span>

}
</code></pre>
<ol>
<li>Define the <code>width</code> and <code>height</code> as desired.</li>
<li>Specify the <code>id</code> of the <code>&lt;symbol&gt;</code>: <code>&lt;use xlinkHref="#pin-it" /&gt;</code>.</li>
</ol>
<h4 id="heading-lazy-load-svg-sprites">Lazy load SVG sprites</h4>
<p>Rather than having your SVG symbols in the <code>index.html</code>, you can put them in a <code>.svg</code> file which is loaded only when needed:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">symbol</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"header-1"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 26 24"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Header 1<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">desc</span>&gt;</span>Toggle a h1 header<span class="hljs-tag">&lt;/<span class="hljs-name">desc</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">text</span> <span class="hljs-attr">x</span>=<span class="hljs-string">"0"</span> <span class="hljs-attr">y</span>=<span class="hljs-string">"20"</span> <span class="hljs-attr">font-weight</span>=<span class="hljs-string">"600"</span>&gt;</span>H1<span class="hljs-tag">&lt;/<span class="hljs-name">text</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">symbol</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">symbol</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"header-2"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 26 24"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Header 2<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">desc</span>&gt;</span>Toggle a h2 header<span class="hljs-tag">&lt;/<span class="hljs-name">desc</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">text</span> <span class="hljs-attr">x</span>=<span class="hljs-string">"0"</span> <span class="hljs-attr">y</span>=<span class="hljs-string">"20"</span> <span class="hljs-attr">font-weight</span>=<span class="hljs-string">"600"</span>&gt;</span>H2<span class="hljs-tag">&lt;/<span class="hljs-name">text</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">symbol</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
</code></pre>
<p>Put that file in <code>client/src/assets</code>:</p>
<pre><code class="lang-xml">- client
  - src
    - assets
      - svg-sprites.svg
</code></pre>
<p>Finally, to use one of the symbols in the file:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Editor.js</span>

<span class="hljs-keyword">import</span> svgSprites <span class="hljs-keyword">from</span> <span class="hljs-string">"../../assets/svg-sprites.svg"</span>;

<span class="hljs-comment">/* component stuff */</span>

render() {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">svg</span>
        <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>
        <span class="hljs-attr">xmlnsXlink</span>=<span class="hljs-string">"http://www.w3.org/1999/xlink"</span>
        <span class="hljs-attr">width</span>=<span class="hljs-string">"24"</span>
        <span class="hljs-attr">height</span>=<span class="hljs-string">"24"</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">use</span> <span class="hljs-attr">xlinkHref</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">svgSprites</span>}#<span class="hljs-attr">header-1</span>`} /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  )
}
</code></pre>
<p>And a browser will, during runtime, fetch the <code>.svg</code> file if it hasn't already.</p>
<p>And there you have it! No more plastering those lengthy <code>path</code> data all over the place.</p>
<h4 id="heading-sources-of-icons">Sources of icons</h4>
<ul>
<li><a target="_blank" href="https://material.io/resources/icons/?style=baseline">https://material.io/resources/icons/?style=baseline</a></li>
<li><a target="_blank" href="https://logomakr.com/">https://logomakr.com/</a></li>
<li><a target="_blank" href="https://github.com/wmira/react-icons-kit#bundled-icon-sets">https://github.com/wmira/react-icons-kit#bundled-icon-sets</a> (has a nice list of sources)</li>
</ul>
<h4 id="heading-references">References</h4>
<ul>
<li><a target="_blank" href="https://css-tricks.com/mega-list-svg-information/#svg-icons">https://css-tricks.com/mega-list-svg-information/#svg-icons</a></li>
</ul>
<h3 id="heading-favicon">Favicon</h3>
<p>If I hadn't disabled the <code>inject</code> option of 'html-webpack-plugin', I would have used a plugin called <a target="_blank" href="https://github.com/jantimon/favicons-webpack-plugin">'favicons-webpack-plugin'</a> that automatically generates all types of favicons (beware - it's a lot!), and injects them in my <code>index.html</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// webpack.config.js</span>

<span class="hljs-attr">plugins</span>: [
  <span class="hljs-keyword">new</span> HtmlWebpackPlugin(), <span class="hljs-comment">// 'inject' is true by default</span>
  <span class="hljs-comment">// must come after html-webpack-plugin</span>
  <span class="hljs-keyword">new</span> FaviconsWebpackPlugin({
    <span class="hljs-attr">logo</span>: path.resolve(__dirname, <span class="hljs-string">"../src/assets/logo.svg"</span>),
    <span class="hljs-attr">prefix</span>: <span class="hljs-string">"icons-[hash]/"</span>,
    <span class="hljs-attr">persistentCache</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">inject</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">favicons</span>: {
      <span class="hljs-attr">appName</span>: <span class="hljs-string">"Sametable"</span>,
      <span class="hljs-attr">appDescription</span>: <span class="hljs-string">"Manage your tasks in spreadsheets"</span>,
      <span class="hljs-attr">developerName</span>: <span class="hljs-string">"Kheoh Yee Wei"</span>,
      <span class="hljs-attr">developerURL</span>: <span class="hljs-string">"https://kheohyeewei.com"</span>, <span class="hljs-comment">// prevent retrieving from the nearest package.json</span>
      <span class="hljs-attr">theme_color</span>: <span class="hljs-string">"#fcbdaa"</span>,
      <span class="hljs-comment">// specify the vendors that you want favicon for</span>
      <span class="hljs-attr">icons</span>: {
        <span class="hljs-attr">coast</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">yandex</span>: <span class="hljs-literal">false</span>,
      },
    },
  }),
];
</code></pre>
<p>But since I have disabled the auto-injection, here is how I handle my favicon:</p>
<ol>
<li><p>Go to <a target="_blank" href="https://realfavicongenerator.net/">https://realfavicongenerator.net/</a></p>
<ul>
<li>Provide your logo in SVG format.</li>
<li>Select the 'Version/Refresh' option to enable cache-busting your favicon asset in your users' browser.</li>
<li>Complete the instructions at the end. You can store your favicons in any folder in your project.</li>
</ul>
</li>
<li><p>Use <a target="_blank" href="https://webpack.js.org/plugins/copy-webpack-plugin/">'copy-webpack-plugin'</a> to copy all your favicon assets generated from Step-1, from the folder where you store them (in my case, <code>src/assets/favicon</code>) to Webpack's output's <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/client/config/webpack.production.js#L140">path</a> (<a target="_blank" href="https://github.com/webpack-contrib/copy-webpack-plugin#to">default behaviour</a>), so that they will be accessible from the root (i.e. https://example.com/favicon.ico).</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// webpack.config.js</span>
<span class="hljs-keyword">const</span> CopyWebpackPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">"copy-webpack-plugin"</span>);

plugins: [<span class="hljs-keyword">new</span> CopyWebpackPlugin([{ <span class="hljs-attr">from</span>: <span class="hljs-string">"src/assets/favicon"</span> }])];
</code></pre>
</li>
</ol>
<p>And that's it!</p>
<h3 id="heading-api-calls">API Calls</h3>
<p>A client needs to communicate with a server to perform 'CRUD' operations - Create, Read, Update, and Delete:</p>
<p><img src="https://i.imgur.com/VjAWItp.png" alt="client and server communication diagram" width="600" height="400" loading="lazy"></p>
<p>Here is my hopefully easy to understand <code>api.js</code>:</p>
<details>
  <summary>API WRAPPER</summary>

<code>javascript
import { route } from "preact-router";

function checkStatus(response) {
  const responseCode = response.status;

  if (responseCode &gt;= 200 &amp;&amp; responseCode &lt; 300) {
    return response;
  }

  // handle user not authorized scenario
  if (responseCode === 401) {
    response
      .json()
      .then((json) =&gt;
        route(`/signin${json.refererUri ? `?dest=${json.refererUri}` : ""}`)
      );
    return;
  }

  // pass along error response to the 'catch' block of your await/async try &amp; catch block
  return response.json().then((json) =&gt; {
    return Promise.reject({
      status: responseCode,
      ok: false,
      statusText: response.statusText,
      body: json,
    });
  });
}

function handleError(error) {
  error.response = {
    status: 0,
    statusText:
      "Cannot connect. Please make sure you are connected to internet.",
  };
  throw error;
}

function parseJSON(response) {
  if (response.status === 204 || response.status === 205) {
    return null;
  }
  return response.json();
}

function request(url, options) {
  return fetch(url, options)
    .catch(handleError) // handle network issues
    .then(checkStatus)
    .then(parseJSON)
    .catch((e) =&gt; {
      throw e;
    });
}

export function api(endPoint, userOptions = {}) {
  const url = process.env.API_BASE_URL + endPoint;

  // to pass along our auth cookie to server
  userOptions.credentials = "include";

  const defaultHeaders = {
    "Content-Type": "application/json",
    Accept: "application/json",
  };

  if (userOptions.body instanceof File) {
    const formData = new FormData();
    formData.append("file", userOptions.body);
    userOptions.body = formData;
    // let browser set content-type to multipart/etc.
    delete defaultHeaders["Content-Type"];
  }

  if (userOptions.body instanceof FormData) {
    // let browser set content-type to multipart
    delete defaultHeaders["Content-Type"];
  }

  const options = {
    ...userOptions,
    headers: {
      ...defaultHeaders,
      ...userOptions.headers,
    },
  };

  return request(url, options);
}</code>

</details>

<p>There is almost nothing new to learn to start using this API module if you have used the native <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch"><code>fetch</code></a> before.</p>
<h4 id="heading-usage">Usage</h4>
<pre><code class="lang-javascript"><span class="hljs-comment">// Home.jsx</span>
<span class="hljs-keyword">import</span> { api } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/api"</span>;

<span class="hljs-keyword">async</span> componentDidMount() {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// POST-ing data</span>
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> api(
      <span class="hljs-string">'/projects/save/121212121'</span>,
      {
        <span class="hljs-attr">method</span>: <span class="hljs-string">'PUT'</span>,
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(dataObject)
      }
    )

    <span class="hljs-comment">// or GET-ting data</span>
    <span class="hljs-keyword">const</span> { myWorkspaces } = <span class="hljs-keyword">await</span> api(<span class="hljs-string">'/users/home'</span>);

  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-comment">// handle Promise.reject passed from api.js</span>
  }
}
</code></pre>
<p>But if you prefer to use a library to handle your HTTP calls, I'd recommend <a target="_blank" href="https://github.com/developit/redaxios">'redaxios'</a>. It not only shares an API with the popular <a target="_blank" href="https://www.npmjs.com/package/axios">axios</a>, but it's much more lightweight.</p>
<h3 id="heading-test-production-build-locally">Test Production Build Locally</h3>
<p>I always build my client app locally to test and measure in my browser before I deploy to the cloud.</p>
<p>I have an npm script (<code>npm run test-build</code>) in the <code>package.json</code> of the 'client' folder that will build and serve on a local web server. This way I can play with it in my browser at http://localhost:5000:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"test-build"</span>: <span class="hljs-string">"cross-env NODE_ENV=production TEST_RUN=true node_modules/.bin/webpack &amp;&amp; npm run serve"</span>,
    <span class="hljs-attr">"serve"</span>: <span class="hljs-string">"ws --spa index.html --directory dist --port 5000 --hostname localhost"</span>
  }
</code></pre>
<p>The app is served using a tool called <a target="_blank" href="https://www.npmjs.com/package/local-web-server">'local-web-server'</a>. It's so far the only one I find works perfectly for a SPA.</p>
<h3 id="heading-security">Security</h3>
<p>Consider adding the <a target="_blank" href="https://developers.google.com/web/fundamentals/security/csp/">CSP</a> security headers.</p>
<p>To add headers in firebase: <a target="_blank" href="https://firebase.google.com/docs/hosting/full-config#headers">https://firebase.google.com/docs/hosting/full-config#headers</a></p>
<p>Sample of CSP headers in your <code>firebase.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"source"</span>: <span class="hljs-string">"**"</span>,
  <span class="hljs-attr">"headers"</span>: [
    {
      <span class="hljs-attr">"key"</span>: <span class="hljs-string">"Strict-Transport-Security"</span>,
      <span class="hljs-attr">"value"</span>: <span class="hljs-string">"max-age=63072000; includeSubdomains; preload"</span>
    },
    {
      <span class="hljs-attr">"key"</span>: <span class="hljs-string">"Content-Security-Policy"</span>,
      <span class="hljs-attr">"value"</span>: <span class="hljs-string">"default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"</span>
    },
    { <span class="hljs-attr">"key"</span>: <span class="hljs-string">"X-Content-Type-Options"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"nosniff"</span> },
    { <span class="hljs-attr">"key"</span>: <span class="hljs-string">"X-Frame-Options"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"DENY"</span> },
    { <span class="hljs-attr">"key"</span>: <span class="hljs-string">"X-XSS-Protection"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"1; mode=block"</span> },
    { <span class="hljs-attr">"key"</span>: <span class="hljs-string">"Referrer-Policy"</span>, <span class="hljs-attr">"value"</span>: <span class="hljs-string">"same-origin"</span> }
  ]
}
</code></pre>
<p>If you use Stripe, make sure you add their CSP directives too:
<a target="_blank" href="https://stripe.com/docs/security/guide#content-security-policy">https://stripe.com/docs/security/guide#content-security-policy</a></p>
<p>Finally, make sure you get an <strong>A</strong> <a target="_blank" href="https://observatory.mozilla.org/">here</a> and pat yourself on the back!</p>
<h2 id="heading-design">Design</h2>
<p>Before I start to code anything up, I wanted to have a mental reel of how I would want to <strong>on-board</strong> a new user to my app. Then I would sketch on a notebook of what it might look like doing it, and re-iterate the sketches while playing and rehashing the reel in my head. </p>
<p>For my very first 'sprint', I would primarily build a 'UI/UX framework' upon which I would add pieces over time. However, it's important to remember that every decision you make during this process should be one that's open-ended and easy to undo. </p>
<p>This way a 'small'— but careful—decision won't spell doom when you get carried away with any over-confident and romantic convictions.</p>
<p>Not sure if that made any sense, but let's explore a few concepts that helped structure my design to be coherent in practise.</p>
<h3 id="heading-modular-scale">Modular Scale</h3>
<p>Your design will make more sense to your users when it flows according to a 'modular scale'. That scale should specify a scale of spaces or sizes that each increment with a certain ratio.</p>
<figure>
<img src="https://i.imgur.com/WWl6KuB.png" alt="modular scale illustration" width="600" height="400" loading="lazy">
<figcaption><em>Figure: Modular scale</em></figcaption>
</figure>

<p>One way to create a scale is with CSS <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">'Custom Properties'</a>(credits to view-source <a target="_blank" href="https://every-layout.dev/">every-layout.dev</a>):</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">:root</span> {
  <span class="hljs-attribute">--ratio</span>: <span class="hljs-number">1.414</span>;
  <span class="hljs-attribute">--s-3</span>: <span class="hljs-built_in">calc</span>(var(--s0) / <span class="hljs-built_in">var</span>(--ratio) / <span class="hljs-built_in">var</span>(--ratio) / <span class="hljs-built_in">var</span>(--ratio));
  <span class="hljs-attribute">--s-2</span>: <span class="hljs-built_in">calc</span>(var(--s0) / <span class="hljs-built_in">var</span>(--ratio) / <span class="hljs-built_in">var</span>(--ratio));
  <span class="hljs-attribute">--s-1</span>: <span class="hljs-built_in">calc</span>(var(--s0) / <span class="hljs-built_in">var</span>(--ratio));
  <span class="hljs-attribute">--s0</span>: <span class="hljs-number">1rem</span>;
  <span class="hljs-attribute">--s1</span>: <span class="hljs-built_in">calc</span>(var(--s0) * <span class="hljs-built_in">var</span>(--ratio));
  <span class="hljs-attribute">--s2</span>: <span class="hljs-built_in">calc</span>(var(--s0) * <span class="hljs-built_in">var</span>(--ratio) * <span class="hljs-built_in">var</span>(--ratio));
  <span class="hljs-attribute">--s3</span>: <span class="hljs-built_in">calc</span>(var(--s0) * <span class="hljs-built_in">var</span>(--ratio) * <span class="hljs-built_in">var</span>(--ratio) * <span class="hljs-built_in">var</span>(--ratio));
}
</code></pre>
<p>If you don't know what scale to use, just <a target="_blank" href="https://www.modularscale.com/">pick a scale</a> that fits closest to your design and <strong>stick to it</strong>.</p>
<p>Then create a bunch of utility classes, each associated with a scale, in a file call <code>spacing.scss</code>. I will use them to space my UI elements across a project:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.mb-1</span> {
  <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-built_in">var</span>(--s1);
}
<span class="hljs-selector-class">.mb-2</span> {
  <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-built_in">var</span>(--s2);
}
<span class="hljs-selector-class">.mr-1</span> {
  <span class="hljs-attribute">margin-right</span>: <span class="hljs-built_in">var</span>(--s1);
}
<span class="hljs-selector-class">.mr--1</span> {
  <span class="hljs-attribute">margin-right</span>: <span class="hljs-built_in">var</span>(--s-<span class="hljs-number">1</span>);
}
</code></pre>
<p>Notice that I try to define the spacing only in the <code>right</code> and <code>bottom</code> direction as <a target="_blank" href="https://csswizardry.com/2012/06/single-direction-margin-declarations/">suggested here</a>.</p>
<p>In my experience, it's better to not bake in any spacing definitions in your UI components:</p>
<p><strong>DON'T</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Button.scss</span>
.btn {
  <span class="hljs-attr">margin</span>: <span class="hljs-number">10</span>px; <span class="hljs-comment">// a default spacing; annoying to have in most cases</span>
  font-style: normal;
  border: <span class="hljs-number">0</span>;
  background-color: transparent;
}

<span class="hljs-comment">// Button.jsx</span>
<span class="hljs-keyword">import</span> s <span class="hljs-keyword">from</span> <span class="hljs-string">'./Button.scss'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Button</span>(<span class="hljs-params">{children, ...props}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">{s.btn}</span> {<span class="hljs-attr">...props</span>}&gt;</span>{children}<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  )
}

<span class="hljs-comment">// Usage</span>
&lt;Button /&gt;
</code></pre>
<p><strong>DO</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Button.scss</span>
.btn {
  font-style: normal;
  border: <span class="hljs-number">0</span>;
  background-color: transparent;
}

<span class="hljs-comment">// Button.jsx</span>
<span class="hljs-keyword">import</span> s <span class="hljs-keyword">from</span> <span class="hljs-string">'./Button.scss'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Button</span>(<span class="hljs-params">{children, className, ...props}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">s.btn</span>} ${<span class="hljs-attr">className</span>}`} {<span class="hljs-attr">...props</span>}&gt;</span>{children}<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  )
}

<span class="hljs-comment">// Usage</span>
<span class="hljs-comment">// Pass your spacing utility classes when building your pages</span>
&lt;Button className=<span class="hljs-string">"mr-1 pb-1"</span>&gt;Sign Up&lt;/Button&gt;
</code></pre>
<h3 id="heading-colors">Colors</h3>
<p>There are many color palette tools out there. But the one from <a target="_blank" href="https://material.io/design/color/the-color-system.html#tools-for-picking-colors">Material</a> is the one I always go to for my colors simply because they are laid out in all their glory! ?</p>
<p>Then I will define them as CSS Custom Properties again:</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">:root</span> {
  <span class="hljs-attribute">--black-100</span>: <span class="hljs-number">#0b0c0c</span>;
  <span class="hljs-attribute">--black-80</span>: <span class="hljs-number">#424242</span>;
  <span class="hljs-attribute">--black-60</span>: <span class="hljs-number">#555759</span>;
  <span class="hljs-attribute">--black-50</span>: <span class="hljs-number">#626a6e</span>;

  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">105%</span>;
  <span class="hljs-attribute">color</span>: <span class="hljs-built_in">var</span>(--black-<span class="hljs-number">100</span>);
}
</code></pre>
<h3 id="heading-css-reset">CSS Reset</h3>
<p>The purpose of a 'CSS reset' is to remove the default styling of common browsers.</p>
<p>There are quite a few of those out there. Beware that some can get quite opinionated and potentially give you more headaches than they're worth. Here is a popular one: <a target="_blank" href="https://meyerweb.com/eric/tools/css/reset/reset.css">https://meyerweb.com/eric/tools/css/reset/reset.css</a></p>
<p>Here is mine:</p>
<pre><code class="lang-css">*,
*<span class="hljs-selector-pseudo">::before</span>,
*<span class="hljs-selector-pseudo">::after</span> {
  <span class="hljs-attribute">box-sizing</span>: border-box;
  <span class="hljs-attribute">overflow-wrap</span>: break-word;
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
  <span class="hljs-attribute">border</span>: <span class="hljs-number">0</span> solid;
  <span class="hljs-attribute">font-family</span>: inherit;
  <span class="hljs-attribute">color</span>: inherit;
}

<span class="hljs-comment">/* Set core body defaults */</span>
<span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">scroll-behavior</span>: smooth;
  <span class="hljs-attribute">text-rendering</span>: optimizeLegibility;
}

<span class="hljs-comment">/* Make images easier to work with */</span>
<span class="hljs-selector-tag">img</span> {
  <span class="hljs-attribute">max-width</span>: <span class="hljs-number">100%</span>;
}

<span class="hljs-comment">/* Inherit fonts for inputs and buttons */</span>
<span class="hljs-selector-tag">button</span>,
<span class="hljs-selector-tag">input</span>,
<span class="hljs-selector-tag">textarea</span>,
<span class="hljs-selector-tag">select</span> {
  <span class="hljs-attribute">color</span>: inherit;
  <span class="hljs-attribute">font</span>: inherit;
}
</code></pre>
<p>You could also consider using <a target="_blank" href="https://github.com/csstools/postcss-normalize">postcss-normalize</a> that generates one according to your targeted browsers.</p>
<h3 id="heading-a-styling-practice">A Styling Practice</h3>
<p>I always try to style at the <strong>tag</strong>-level first before bringing out the big gun if necessary, in my case, <a target="_blank" href="https://github.com/css-modules/css-modules">'CSS Modules'</a>, for encapsulating styles per component:</p>
<pre><code class="lang-xml">- src
  - routes
    - SignIn
      - SignIn.js
      - SignIn.scss
</code></pre>
<p>The <code>SignIn.scss</code> contains CSS that pertains only to the <code>&lt;SignIn /&gt;</code> component.</p>
<p>Furthermore, I don't use the CSS libraries popular in the React ecosystem such as 'styled-components' and 'emotion'. I try to <strong>use pure HTML and CSS whenever I can, and only let Preact handle the DOM and state updates</strong> for me.</p>
<p>For example, for the <code>&lt;input/&gt;</code> element:</p>
<pre><code class="lang-css">// <span class="hljs-selector-tag">index</span><span class="hljs-selector-class">.scss</span>

<span class="hljs-selector-tag">label</span> {
  <span class="hljs-attribute">display</span>: block;
  <span class="hljs-attribute">color</span>: <span class="hljs-built_in">var</span>(--black-<span class="hljs-number">100</span>);
  <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">600</span>;
}

<span class="hljs-selector-tag">input</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">font-weight</span>: <span class="hljs-number">400</span>;
  <span class="hljs-attribute">font-style</span>: normal;
  <span class="hljs-attribute">border</span>: <span class="hljs-number">2px</span> solid <span class="hljs-built_in">var</span>(--black-<span class="hljs-number">100</span>);
  <span class="hljs-attribute">box-shadow</span>: none;
  <span class="hljs-attribute">outline</span>: none;
  <span class="hljs-attribute">appearance</span>: none;
}

<span class="hljs-selector-tag">input</span><span class="hljs-selector-pseudo">:focus</span> {
  <span class="hljs-attribute">box-shadow</span>: inset <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">2px</span>;
  <span class="hljs-attribute">outline</span>: <span class="hljs-number">3px</span> solid <span class="hljs-number">#fd0</span>;
  <span class="hljs-attribute">outline-offset</span>: <span class="hljs-number">0</span>;
}
</code></pre>
<p>Then using it in a JSX file with its vanilla tag:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// SignIn.js</span>

render() {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">"email"</span>&gt;</span>
        Email<span class="hljs-symbol">&amp;nbsp;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">abbr</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"This field is required"</span>&gt;</span>*<span class="hljs-tag">&lt;/<span class="hljs-name">abbr</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
        <span class="hljs-attr">required</span>
        <span class="hljs-attr">value</span>=<span class="hljs-string">{this.email}</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span>
        <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"e.g. sara@widgetco.com"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  )
}
</code></pre>
<h3 id="heading-layout">Layout</h3>
<p>I use <strong>CSS Flexbox</strong> for layout works in Sametable. I didn't need any CSS frameworks. Learn CSS Flexbox from its first principles to do more with less code. Plus, in many cases, the result will already be responsive thanks to the layout algorithms, saving those <code>@media</code> queries.</p>
<p>Let's see how to build a common layout in Flexbox with a minimal amount of CSS:</p>
<p><img src="https://i.imgur.com/PTCrd0K.png" alt="sidebar and content layout" width="600" height="400" loading="lazy"></p>
<p>
  <span>See the Pen <a href="https://codepen.io/kilgarenone/pen/mdeLwvx">
  Sidebar/Content layout</a>
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

<h4 id="heading-resources-1">Resources</h4>
<ul>
<li><a target="_blank" href="https://flexboxfroggy.com/">Flexbox froggy</a></li>
<li><a target="_blank" href="https://www.freecodecamp.org/news/understanding-flexbox-everything-you-need-to-know-b4013d4dc9af/">Everything you need to know about flexbox</a></li>
</ul>
<h2 id="heading-server">Server</h2>
<pre><code class="lang-json">- server
  - server.js
  - package.json
  - .env
</code></pre>
<p>The <a target="_blank" href="https://github.com/kilgarenone/boileroom/tree/master/server">server</a> is run on NodeJS(ExpressJS framework) to serve all my <strong>API</strong> endpoints.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Example endpoint: https://example.com/api/tasks/save/12345</span>
router.put(<span class="hljs-string">"/save/:taskId"</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {});
</code></pre>
<p>The <a target="_blank" href="https://github.com/kilgarenone/boileroom/blob/master/server/server.js"><code>server.js</code></a> contains the <a target="_blank" href="https://expressjs.com/en/starter/hello-world.html">familiar</a> codes to start a Nodejs server.</p>
<h3 id="heading-file-structure">File Structure</h3>
<p>I'm grateful for this digestible <a target="_blank" href="https://node-postgres.com/guides/project-structure">guide</a> about project structure, which allowed me to hunker down and quickly build out my API.</p>
<h3 id="heading-npm-scriptserver">Npm Script(Server)</h3>
<p>In the <code>package.json</code> inside the 'server' folder, there is a npm script that will start your server for you:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
  <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"nodemon -r dotenv/config server.js"</span>,
  <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node server.js"</span>
}
</code></pre>
<ul>
<li><p>The <code>dev</code> script <a target="_blank" href="https://www.npmjs.com/package/dotenv#preload">'preload'</a> dotenv as suggested <a target="_blank" href="https://medium.com/the-node-js-collection/making-your-node-js-work-everywhere-with-environment-variables-2da8cdf6e786#b1af">here</a>. And that's it— You will have access to the env variables defined in the <code>.env</code> file from the <code>process.env</code> object.</p>
</li>
<li><p>The <code>start</code> script is used to start our Nodejs server in production. In my case, GCP will run this script to bootup my Nodejs.</p>
</li>
</ul>
<h3 id="heading-database">Database</h3>
<p>I use <strong>Postgresql</strong> as my database. Then I use the <a target="_blank" href="https://node-postgres.com/">'node-postgres'</a>(a.k.a <code>pg</code>) library to connect my Nodejs to the database. Once that's done, I can do CRUD operations between my API endpoints and the database.</p>
<h4 id="heading-setup">Setup</h4>
<p>For local development:</p>
<ol>
<li><p>Download <a target="_blank" href="https://www.enterprisedb.com/downloads/postgres-postgresql-downloads">Postgresql here</a>. Get the latest version. Leave everything as it is. Remember the password you set. Then,</p>
<ul>
<li>Open 'pgAdmin'. It's a browser application.</li>
<li>Create a database for you app:
<img src="https://i.imgur.com/trcAaSi.png" width="600" height="400" alt="trcAaSi" loading="lazy"></li>
</ul>
</li>
<li><p>Define a set of environment variables in the <code>.env</code> file:</p>
<pre><code class="lang-json">DB_HOST='localhost'
DB_USER=postgres
DB_NAME=&lt;YOUR_CUSTOM_DATABASE_NAME_HERE&gt;
DB_PASSWORD=&lt;YOUR_MASTER_PASSWORD&gt;
DB_PORT=<span class="hljs-number">5432</span>
</code></pre>
</li>
<li><p>Then we will <a target="_blank" href="https://node-postgres.com/features/connecting">connect</a> a new client through a <a target="_blank" href="https://node-postgres.com/features/pooling">connection pool</a> to our Postgresql database from our Nodejs. I do it in <code>server/db/index.js</code>:</p>
<p><a href="#db-wrapper" id="db-wrapper">#</a></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { Pool } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"pg"</span>);

<span class="hljs-keyword">const</span> pool = <span class="hljs-keyword">new</span> Pool({
  <span class="hljs-attr">user</span>: process.env.DB_USER,
  <span class="hljs-attr">host</span>: process.env.DB_HOST,
  <span class="hljs-attr">port</span>: process.env.DB_PORT,
  <span class="hljs-attr">database</span>: process.env.DB_NAME,
  <span class="hljs-attr">password</span>: process.env.DB_PASSWORD,
});

<span class="hljs-comment">// TRANSACTION</span>
<span class="hljs-comment">// https://github.com/brianc/node-postgres/issues/1252#issuecomment-293899088</span>
<span class="hljs-keyword">const</span> tx = <span class="hljs-keyword">async</span> (callback, errCallback) =&gt; {
  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">await</span> pool.connect();
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">"BEGIN"</span>);
    <span class="hljs-keyword">await</span> callback(client);
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">"COMMIT"</span>);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.log((<span class="hljs-string">"DB ERROR:"</span>, err));
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">"ROLLBACK"</span>);
    errCallback &amp;&amp; errCallback(err);
  } <span class="hljs-keyword">finally</span> {
    client.release();
  }
};
<span class="hljs-comment">// the pool will emit an error on behalf of any idle clients</span>
<span class="hljs-comment">// it contains if a backend error or network partition happens</span>
pool.on(<span class="hljs-string">"error"</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
  process.exit(<span class="hljs-number">-1</span>);
});

pool.on(<span class="hljs-string">"connect"</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"❤️ Connected to the Database ❤️"</span>);
});

<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">query</span>: <span class="hljs-function">(<span class="hljs-params">text, params, callback</span>) =&gt;</span> pool.query(text, params, callback),
  tx,
  pool,
};
</code></pre>
<ul>
<li>I will use the <code>tx</code> function in an API if I have to call <strong>many</strong> queries that depend on each other.</li>
<li>If I'm making a <strong>single</strong> query, I will use the <code>query</code> function.</li>
</ul>
</li>
</ol>
<p>And that's it! Now you have a database to work with for your local development ?</p>
<h4 id="heading-usage-1">Usage</h4>
<p>I will confess: I <strong>hand-crafted</strong> all the queries for Sametable.</p>
<p>In my opinion, SQL itself is already a declarative language that needs no further abstraction—it's easy to read, understand, and write. It can be maintainable if you separated well your API endpoints. </p>
<p>If you knew you were building a facebook-scale app, perhaps it would be wise to use an ORM. But I'm just a <a target="_blank" href="https://www.youtube.com/watch?v=5PsnxDQvQpw">everyday normal guy</a> building a very narrow-scoped SaaS all by myself. </p>
<p>So I needed to avoid overhead and complexity while considering factors such as ease of onboarding, performance, ease of reiteration, and the potential lifespan of the knowledge. </p>
<p>This reminds me of being urged to learn vanilla JavaScript before jumping on the bandwagon of a popular front-end framework. Because you just might realize: That's all you need for what you have set out to accomplish to reach your 1000th customer.</p>
<p>To be fair, though, when I decided to go down this path, I'd had modest experiences in writing MySQL. So if you know nothing about SQL and you are anxious to ship it, then you might want to consider a library like <a target="_blank" href="http://knexjs.org/">knex.js</a>.</p>
<h5 id="heading-example">Example</h5>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/routes/projects.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> asyncHandler = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express-async-handler"</span>);
<span class="hljs-keyword">const</span> db = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../db"</span>);

<span class="hljs-keyword">const</span> router = express.Router();

<span class="hljs-built_in">module</span>.exports = router;

<span class="hljs-comment">// [POST] api/projects/create</span>
router.post(
  <span class="hljs-string">"/create"</span>,
  express.json(),
  asyncHandler(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">const</span> { title, project_id } = req.body;

    db.tx(<span class="hljs-keyword">async</span> (client) =&gt; {
      <span class="hljs-keyword">const</span> {
        rows,
      } = <span class="hljs-keyword">await</span> client.query(
        <span class="hljs-string">`INSERT INTO tasks (title) VALUES ($1) RETURNING mask_id(task_id) as masked_task_id, task_id`</span>,
        [title]
      );

      res.json({ <span class="hljs-attr">id</span>: rows[<span class="hljs-number">0</span>].masked_task_id });
    }, next);
  })
);
</code></pre>
<ul>
<li><p>The <a target="_blank" href="https://github.com/Abazhenov/express-async-handler/blob/master/index.js"><code>express-async-handler</code></a> is mainly used to handle the async errors in my route handlers. It won't be needed anymore when Express 5 drops.</p>
</li>
<li><p>Import the <code>db</code> module to use the <code>tx</code> method. Pass your hand-crafted SQL queries and <a target="_blank" href="https://node-postgres.com/features/queries">parameters</a>.</p>
</li>
</ul>
<p>That's it!</p>
<h3 id="heading-creating-table-schemas">Creating table schemas</h3>
<p>Before you can start querying a database, you need to create tables. Each table contains information about an entity. </p>
<p>But we don't just lump all information about an entity in the same table. We need to organize the information in a way that promotes query performance and data maintainability. And what has helped me in that exercise is a concept called <a target="_blank" href="https://firebase.google.com/docs/database/web/structure-data"><strong>denormalization</strong></a>. </p>
<p>As mentioned, we don't want to store everything about an entity in the same table. For example, say, we have a <code>users</code> table storing <code>fullname</code>, <code>password</code> and <code>email</code>. That's fine so far. </p>
<p>But problem arises when we are also storing the ids of all the projects assigned to a particular user in a separate column in the same table. Instead, I will break them up into separate tables:</p>
<ol>
<li><p>Create the <code>users</code> table. Notice that it's not storing any data related to 'projects':</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span>(
  user_id BIGSERIAL PRIMARY <span class="hljs-keyword">KEY</span>,
  fullname <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  pwd <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  email <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
);
</code></pre>
</li>
<li><p>Create a <code>projects</code> table to store data solely about a project's details:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> projects(
  project_id BIGSERIAL PRIMARY <span class="hljs-keyword">KEY</span>,
  title <span class="hljs-built_in">TEXT</span>,
  <span class="hljs-keyword">content</span> <span class="hljs-built_in">TEXT</span>,
  due_date TIMESTAMPTZ,
  <span class="hljs-keyword">status</span> <span class="hljs-built_in">SMALLINT</span>,
  created_on TIMESTAMPTZ <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">now</span>()
);
</code></pre>
</li>
<li><p>Create a 'bridge' table about projects' ownerships by associating the ID of an user with the ID of a project that she owns:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> project_ownerships(
  project_id <span class="hljs-built_in">BIGINT</span> <span class="hljs-keyword">REFERENCES</span> projects <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">CASCADE</span>,
  user_id <span class="hljs-built_in">BIGINT</span> <span class="hljs-keyword">REFERENCES</span> <span class="hljs-keyword">users</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">CASCADE</span>,
  PRIMARY <span class="hljs-keyword">KEY</span> (project_id, user_id),
  <span class="hljs-keyword">CONSTRAINT</span> project_user_unique <span class="hljs-keyword">UNIQUE</span> (user_id, project_id)
);
</code></pre>
</li>
<li><p>Finally, to get all the projects that are assigned to a particular user, we will do what relational database do best: <a target="_blank" href="https://www.postgresqltutorial.com/postgresql-joins/"><code>join</code></a>.</p>
</li>
</ol>
<p>I will put all my schemas in a <code>.sql</code> file at my project's root <a href="#schemas-file" id="schemas-file">#</a>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> <span class="hljs-string">"uuid-ossp"</span>;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span>(
  user_id BIGSERIAL PRIMARY <span class="hljs-keyword">KEY</span>,
  fullname <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  pwd <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  email <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  created_on TIMESTAMPTZ <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">now</span>()
);
</code></pre>
<p>Then, I will copy, paste, and run them in pgAdmin:</p>
<p><img src="https://i.imgur.com/gm9YFZF.png" alt="create table schemas in pgadmin" width="600" height="400" loading="lazy"></p>
<p>No doubt there are more advanced ways of doing this, so it's up to you if you want to explore what you like.</p>
<h3 id="heading-dropping-a-database">Dropping a database</h3>
<p>Deleting an entire database to start with a new set of schemas was something I had to do very often at the beginning.</p>
<p>The trick is: Well, you copy, paste, and run the command below in the database's query editor in pgAdmin:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">DROP</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">CASCADE</span>;
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span>;
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">TO</span> postgres;
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">TO</span> <span class="hljs-keyword">public</span>;
<span class="hljs-keyword">COMMENT</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">IS</span> <span class="hljs-string">'standard public schema'</span>;
</code></pre>
<h3 id="heading-crafting-sql-queries">Crafting SQL queries</h3>
<p>I write my SQL queries in <strong>pgAdmin</strong> to get the data I want out of an API endpoint.</p>
<p>To give a sense of direction to doing that in pgAdmin:
<img src="https://i.imgur.com/54tIRzc.png" alt="writing sql queries in pgadmin editor" width="600" height="400" loading="lazy"></p>
<h4 id="heading-common-table-expressionsctes">Common Table Expressions(CTEs)</h4>
<p>I stumbled upon a pattern called <a target="_blank" href="https://www.postgresql.org/docs/9.1/queries-with.html"><strong>CTEs</strong></a> when I was exploring how I was going to get the data I wanted from disparate tables and structure them as I wished, without doing lots of separate database queries and for-loops.</p>
<p>The way CTE works is simple enough, even though it looks daunting: You write your queries. Each query is given an alias name (<code>q</code>, <code>q1</code>, <code>q3</code>). And a next query can access any previous query's results by their alias name (<code>q1.workspace_id</code>):</p>
<pre><code class="lang-sql"><span class="hljs-keyword">WITH</span> q <span class="hljs-keyword">AS</span> (<span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> projects_tasks <span class="hljs-keyword">WHERE</span> task_id=$<span class="hljs-number">1</span>)
, q1 <span class="hljs-keyword">AS</span> (<span class="hljs-keyword">SELECT</span> wp.workspace_id, wp.project_id, q.task_id <span class="hljs-keyword">FROM</span> workspaces_projects wp, q <span class="hljs-keyword">WHERE</span> wp.project_id = q.project_id)
, q3 <span class="hljs-keyword">AS</span> (<span class="hljs-keyword">SELECT</span> q1.workspace_id <span class="hljs-keyword">AS</span> workspace_id, wp.name <span class="hljs-keyword">AS</span> workspace_title, mask_id(q1.project_id) <span class="hljs-keyword">AS</span> project_id, p.title <span class="hljs-keyword">AS</span> project_title, mask_id(t.task_id) <span class="hljs-keyword">AS</span> task_id, t.title, t.content, t.due_date, t.priority, t.status)

<span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> q3;
</code></pre>
<p>Almost all the queries in Sametable are written this way.</p>
<h3 id="heading-redis">Redis</h3>
<p>Redis is a NoSQL database that stores data in memory. In Sametable, I used Redis for two purposes:</p>
<ol>
<li>Store a user's session data and basic info from the <code>users</code> table—name, email, and a flag that indicates the user is a subscriber or not—once they have logged in.</li>
<li>Cache the results of some of my Postgresql's queries to avoid having to query the database if the cache is still fresh.</li>
</ol>
<h4 id="heading-installation">Installation</h4>
<p>I'm on a Windows 10 machine with Windows Subsystem Linux (WSL) installed. This was the only guide I followed to install Redis on my machine:</p>
<p><a target="_blank" href="https://redislabs.com/blog/redis-on-windows-10/">https://redislabs.com/blog/redis-on-windows-10/</a></p>
<p>Follow the guide to install WSL if you don't have it already.</p>
<p>Then I will start my local Redis server in WSL bash:</p>
<ol>
<li>Press <kbd>Win</kbd> + <kbd>R</kbd>.</li>
<li>Type <code>bash</code> and enter.</li>
<li>In the terminal, run <code>sudo service redis-server start</code></li>
</ol>
<p>Now install the <a target="_blank" href="https://www.npmjs.com/package/redis"><code>redis</code></a> npm package:</p>
<pre><code class="lang-json">cd server

npm i redis
</code></pre>
<p>Make sure to install it in the <code>server</code>'s <code>package.json</code>, hence the <code>cd server</code>.</p>
<p>Then I create a file named <code>redis.js</code> under <code>server/db</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/db/redis.js</span>

<span class="hljs-keyword">const</span> redis = <span class="hljs-built_in">require</span>(<span class="hljs-string">"redis"</span>);
<span class="hljs-keyword">const</span> { promisify } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"util"</span>);

<span class="hljs-keyword">const</span> redisClient = redis.createClient(
  NODE_ENV === <span class="hljs-string">"production"</span>
    ? {
        <span class="hljs-attr">host</span>: process.env.REDISHOST,
        <span class="hljs-attr">no_ready_check</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">auth_pass</span>: process.env.REDIS_PASSWORD,
      }
    : {}
);

redisClient.on(<span class="hljs-string">"error"</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"ERR:REDIS:"</span>, err));

<span class="hljs-keyword">const</span> redisGetAsync = promisify(redisClient.get).bind(redisClient);
<span class="hljs-keyword">const</span> redisSetExAsync = promisify(redisClient.setex).bind(redisClient);
<span class="hljs-keyword">const</span> redisDelAsync = promisify(redisClient.del).bind(redisClient);

<span class="hljs-comment">// 1 day expiry</span>
<span class="hljs-keyword">const</span> REDIS_EXPIRATION = <span class="hljs-number">7</span> * <span class="hljs-number">86400</span>; <span class="hljs-comment">// seconds</span>

<span class="hljs-built_in">module</span>.exports = {
  redisGetAsync,
  redisSetExAsync,
  redisDelAsync,
  REDIS_EXPIRATION,
  redisClient,
};
</code></pre>
<ul>
<li><p>By <a target="_blank" href="https://www.npmjs.com/package/redis#options-object-properties">default</a>, <code>node-redis</code> will connect to <code>localhost</code> at port <code>6379</code>. But that might not be the case in production if you host your Redis in a VM. So I provide this object if it's in production mode:</p>
<pre><code class="lang-js">{
   <span class="hljs-attr">host</span>: process.env.REDISHOST,
   <span class="hljs-attr">no_ready_check</span>: <span class="hljs-literal">true</span>,
   <span class="hljs-attr">auth_pass</span>: process.env.REDIS_PASSWORD,
 }
</code></pre>
<ul>
<li>TBH, I'm not entirely sure about the <code>no_ready_check</code>. I got it from this official <a target="_blank" href="https://docs.redislabs.com/latest/rs/references/client_references/client_nodejs/">tutorial</a>.</li>
<li>The <code>auth_pass</code> and <code>host</code> are provided as custom since I host my Redis in a GCE VM where I have set a password on my Redis.</li>
</ul>
</li>
<li><p>I <a target="_blank" href="https://www.npmjs.com/package/redis#promises">promisfy</a> the Redis methods that I will use to make them async to avoid blocking NodeJS's single-thread.</p>
</li>
</ul>
<p>And now you have the Redis for your local development!</p>
<h3 id="heading-error-handling-amp-logging">Error handling &amp; Logging</h3>
<h4 id="heading-error-handling">Error handling</h4>
<p>Error handling in Nodejs has a paradigm which we will explore in 3 different contexts.</p>
<p>To set the stage, we need two things in place first:</p>
<ol>
<li><p>An npm package called <a target="_blank" href="https://www.npmjs.com/package/http-errors">http-errors</a> that will give us a standard error data structure to work with especially in client-side.</p>
<pre><code class="lang-json">npm install http-errors
</code></pre>
</li>
<li><p>We create a custom error handler at the global level to capture <strong>all</strong> propagated errors from the routes or the <code>catch</code> blocks via <code>next(err)</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app.js</span>
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> createError = <span class="hljs-built_in">require</span>(<span class="hljs-string">"http-errors"</span>);

<span class="hljs-comment">// our central custom error handler</span>
<span class="hljs-comment">// <span class="hljs-doctag">NOTE:</span> DON"T REMOVE THE 'next' even though eslint complains it's not being used!!!</span>
app.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">err, req, res, next</span>) </span>{
  <span class="hljs-comment">// errors wrapped by http-errors will have 'status' property defined. Otherwise, it's a generic unexpected error</span>
  <span class="hljs-keyword">const</span> error = err.status
    ? err
    : createError(<span class="hljs-number">500</span>, <span class="hljs-string">"Something went wrong. Notified dev."</span>);

  res.status(error.status).json(error);
});
</code></pre>
<p>As you will see, the general pattern of error handling in Nodejs revolves around the 'middleware' chain and the <code>next</code> parameter:</p>
<blockquote>
<p>Calls to next() and next(err) indicate that the current handler is complete and in what state. next(err) will skip all remaining handlers in the chain except for those that are set up to handle errors . . . <a target="_blank" href="https://expressjs.com/en/guide/error-handling.html">source</a></p>
</blockquote>
<p>Note that although this is a common pattern of handling error in Express, you might want to consider an <a target="_blank" href="https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/errorhandling/centralizedhandling.md">alternative way</a> that's, however, more complicated.</p>
</li>
</ol>
<h5 id="heading-handle-input-validation-errors">Handle input validation errors</h5>
<p>It's a <a target="_blank" href="https://github.com/goldbergyoni/nodebestpractices#-610-validate-incoming-json-schemas">good practise</a> to validate a user's inputs both in the client and server-side. </p>
<p>At the server-side, I use a library called <a target="_blank" href="https://express-validator.github.io/docs/">'express-validator'</a> to do the job. If any input is invalid, I will handle it by responding with an HTTP code and an error message to inform the user about it.</p>
<p>For example, when an email provided by a user is invalid, we will exit early by creating an error object with the 'http-errors' library, and then pass it to the <code>next</code> function:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { body, validationResult } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express-validator"</span>);

router.post(
  <span class="hljs-string">"/login"</span>,
  upload.none(),
  [body(<span class="hljs-string">"email"</span>, <span class="hljs-string">"Invalid email format"</span>).isEmail()],
  asyncHandler(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">const</span> errors = validationResult(req);
    <span class="hljs-keyword">if</span> (!errors.isEmpty()) {
      <span class="hljs-keyword">return</span> next(createError(<span class="hljs-number">422</span>, errors.mapped()));
    }

    res.json({});
  })
);
</code></pre>
<p>The following response will be sent to the client:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Unprocessable Entity"</span>,
  <span class="hljs-attr">"email"</span>: {
    <span class="hljs-attr">"value"</span>: <span class="hljs-string">"hello@mail.com232"</span>,
    <span class="hljs-attr">"msg"</span>: <span class="hljs-string">"Invalid email format"</span>,
    <span class="hljs-attr">"param"</span>: <span class="hljs-string">"email"</span>,
    <span class="hljs-attr">"location"</span>: <span class="hljs-string">"body"</span>
  }
}
</code></pre>
<p>Then it's up to you what you want to do with it. For example, you can access the <code>email.msg</code> property to display the error message below the email input field.</p>
<h5 id="heading-handle-errors-from-business-logic">Handle errors from business logic</h5>
<p>Let's say we have a situation where a user entered an email that didn't exist in the database. In that case, we need to tell the user to try again:</p>
<pre><code class="lang-javascript">router.post(
  <span class="hljs-string">"/login"</span>,
  upload.none(),
  asyncHandler(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">const</span> { email, password } = req.body;

    <span class="hljs-keyword">const</span> { rowCount } = <span class="hljs-keyword">await</span> db.query(
      <span class="hljs-string">`SELECT * FROM users WHERE email=($1)`</span>,
      [email]
    );

    <span class="hljs-keyword">if</span> (rowCount === <span class="hljs-number">0</span>) {
      <span class="hljs-comment">// issue an error with generic message</span>
      <span class="hljs-keyword">return</span> next(
        createError(<span class="hljs-number">422</span>, <span class="hljs-string">"Please enter a correct email and password"</span>)
      );
    }

    res.json({});
  })
);
</code></pre>
<p>Remember, any error object passed to 'next'(<code>next(err)</code>) will be captured by the custom error handler that we have set above.</p>
<h5 id="heading-handle-unexpected-errors-from-database">Handle unexpected errors from database</h5>
<p>I pass the route handler's <code>next</code> to my db's <a href="#db-wrapper">transaction</a> wrapper function to handle any unexpected erorrs.</p>
<pre><code class="lang-javascript">router.post(
  <span class="hljs-string">"/invite"</span>,
  <span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    db.tx(<span class="hljs-keyword">async</span> (client) =&gt; {
          <span class="hljs-keyword">const</span> {
            rows,
            rowCount,
          } = <span class="hljs-keyword">await</span> client.query(
            <span class="hljs-string">`SELECT mask_id(user_id) AS user_id, status FROM users WHERE users.email=$1`</span>,
            [email]
          );
    }, next)
)
</code></pre>
<h4 id="heading-logging">Logging</h4>
<p>When an error occurs, it's a common practice to 1) Log it to a system for records, and 2) Automatically notify you about it.</p>
<p>There are many tools out there in this area. But I ended up with two of them:</p>
<ul>
<li><a target="_blank" href="https://sentry.io/welcome/"><strong>Sentry</strong></a> for storing details (e.g. stack traces) of my errors, and displaying them on their web-based dashboard.</li>
<li><a target="_blank" href="https://github.com/pinojs/pino"><strong>pino</strong></a> to enable logging in my Nodejs.</li>
</ul>
<p><strong>Why Sentry</strong>? Well, it was recommended by lots of devs and small startups. It offers 5000 errors you can send per month for free. For perspective, if you are operating a small side project and careful about it, I would say that'd last you until you can afford a more luxurious vendor or plan. </p>
<p>Another option worth exploring is <a target="_blank" href="https://www.honeybadger.io/">honeybadger.io</a> with more generous free-tier but without a <a target="_blank" href="https://getpino.io/#/docs/transports">pino transport</a>.</p>
<p><strong>Why Pino</strong>- Why not the official SDK provided by Sentry? Because Pino has <a target="_blank" href="https://github.com/pinojs/pino#low-overhead">'low overhead'</a>, whereas, Sentry SDK, although it gives you a more complete picture of an error, seemed to have a complex <a target="_blank" href="https://github.com/getsentry/sentry-javascript/issues/1762">memory issue</a> that I couldn't see myself being able to circumvent.</p>
<p>With that, here is how the logging system is hooked up in Sametable:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/lib/logger.js</span>

<span class="hljs-comment">// install missing packages</span>
<span class="hljs-keyword">const</span> pino = <span class="hljs-built_in">require</span>(<span class="hljs-string">"pino"</span>);
<span class="hljs-keyword">const</span> { createWriteStream } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"pino-sentry"</span>);
<span class="hljs-keyword">const</span> expressPino = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express-pino-logger"</span>);

<span class="hljs-keyword">const</span> options = { <span class="hljs-attr">name</span>: <span class="hljs-string">"sametable"</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">"error"</span> };

<span class="hljs-comment">// SENTRY_DSN is provided by Sentry. Store it as env var in the .env file.</span>
<span class="hljs-keyword">const</span> stream = createWriteStream({ <span class="hljs-attr">dsn</span>: process.env.SENTRY_DSN });

<span class="hljs-keyword">const</span> logger = pino(options, stream);
<span class="hljs-keyword">const</span> expressLogger = expressPino({ logger });

<span class="hljs-built_in">module</span>.exports = {
  expressLogger, <span class="hljs-comment">// use it like app.use(expressLogger) -&gt; req.log.info('haha)</span>
  logger,
};
</code></pre>
<p>Rather than attaching the logger(<code>expressLogger</code>) as a middleware at the top of the chain(<code>app.use(expressLogger)</code>), I use the <code>logger</code> object only where I want to log an error.</p>
<p>For example, the custom global error handler uses the <code>logger</code> object:</p>
<pre><code class="lang-javascript">app.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">err, req, res, next</span>) </span>{
  <span class="hljs-keyword">const</span> error = err.status
    ? err
    : createError(<span class="hljs-number">500</span>, <span class="hljs-string">"Something went wrong. Notified dev."</span>);

  <span class="hljs-keyword">if</span> (isProduction) {
    <span class="hljs-comment">// LOG THIS ERROR IN MY SENTRY DASHBOARD</span>
    logger.error(error);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Custom error handler:"</span>, error);
  }

  res.status(error.status).json(error);
});
</code></pre>
<p>That's it! And don't forget to enable email <strong>notification</strong> in your Sentry dashboard to get an alert when your Sentry receives an error! ❤️</p>
<h3 id="heading-permalink-for-url-sharing">Permalink for URL Sharing</h3>
<p>We have seen URLs consist of cryptic alphanumeric string such as those on Youtube: <code>https://youtube.com/watch?v=upyjlOLBv5o</code>. This URL points to a specific video, which can be shared with someone by sharing the URL. The key component in the URL representing the video is the unique ID at the end: <code>upyjlOLBv5o</code>. </p>
<p>We see this kind of ID in other sites too: <code>vimeo.com/259411563</code> and subscription's ID in Stripe <code>sub_aH2s332nm04</code>.</p>
<p>As far as I know, there are three ways to achieve this outcome:</p>
<ol>
<li><p><a target="_blank" href="https://stackoverflow.com/a/41988979/73323">Generate the ID when inserting data in your database</a>. The generated ID will be the ID in your <code>id</code> column rather than the auto-increment ones:</p>
<p>| id         | title        |
| ---------- | ------------ |
| owmCAx552Q | How to cry   |
| ZIofD6l3X9 | How to smile |</p>
</li>
</ol>
<p>Then you will expose these IDs in public-facing URLs: <code>https://example.com/task/owmCAx552Q</code>. Given this URL to your backend, you can retrieve the respective resource from the database:</p>
<pre><code class="lang-javascript">   router.get(<span class="hljs-string">"/task/:taskId"</span>, <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
     <span class="hljs-keyword">const</span> { taskId } = req.params;
     <span class="hljs-comment">// SELECT * FROM tasks WHERE id=&lt;taskId&gt;</span>
   });
</code></pre>
<p>The downsides to this method that I know of:</p>
<ul>
<li>The IDs might be sensitive information to be exposed publicly like that.</li>
<li>These IDs are detrimental to the performance of indexing and 'joining' on your tables.</li>
</ul>
<ol start="2">
<li><p>You keep auto-incrementing your IDs in your tables, but you will represent them by <a target="_blank" href="https://hashids.org/postgresql/">generating their alphanumeric counterpart during database operations</a>:</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">SELECT</span> hash_encode(<span class="hljs-number">123</span>, <span class="hljs-string">'this is my salt'</span>, <span class="hljs-number">10</span>); <span class="hljs-comment">-- Result: 4xpAYDx0mQ</span>
  <span class="hljs-keyword">SELECT</span> hash_decode(<span class="hljs-string">'4xpAYDx0mQ'</span>, <span class="hljs-string">'this is my salt'</span>, <span class="hljs-number">10</span>); <span class="hljs-comment">-- Result: 123</span>
</code></pre>
<p>I had trouble integrating this library on my Windows machine. So I went with the next option.</p>
</li>
<li><p><a target="_blank" href="https://old.reddit.com/r/PostgreSQL/comments/6gw866/best_practice_for_id_system_that_is_obscure_for/diu8cr1/">Similar to the second option above but different approach</a>. This will generate numeric ID: <code>https://example.com/task/2013732563294762</code></p>
</li>
</ol>
<h2 id="heading-user-authentication-system">User Authentication System</h2>
<p>A user authentication system can get very complicated if you need to support things like SSO and third-party OAuth providers. That's why we have third-party tools such as Auth0, Okta, and PassportJS to abstract that out for us. But those tools cost: vendor lock-in, more Javascript payload, and cognitive overhead.</p>
<p>I would argue that if you are starting out and just need <em>some</em> <em>kind of</em> authentication system so you can move on to other parts of your app, and at the same time, overwhelmed by all the dated tutorials that deal with stuff you don't use, well, chances are all you need is the good old way of doing authentication: <strong>Session cookie</strong> with <strong>email</strong> and <strong>password</strong>! And we are not talking about 'JWT' either! None of that.</p>
<h3 id="heading-guide-1">Guide</h3>
<p><a target="_blank" href="https://medium.com/@kilgarenone/easily-implements-user-authentication-in-nodejs-b22bdb6f15bc">Here is a guide</a> I ended up writing. Follow it and you got yourself a user authentication system!</p>
<h2 id="heading-email">Email</h2>
<p>Currently, in Sametable, the only emails it sends are of 'transactional' type like sending a reset password email when users reset their password.</p>
<p>There are two ways to send emails in Nodejs:</p>
<ol>
<li><p><strong>Roll your own</strong> with <a target="_blank" href="https://nodemailer.com/about/">Nodemailer</a>.</p>
<p>I wouldn't go down this path because although sending one email might seem a trivial task, doing it 'at scale' is hard; every email must be sent successfully; and they must not end up in a user's spam folder; and other things I'm not aware of.</p>
</li>
<li><p>Choose one of the <strong>email service providers</strong>.</p>
</li>
</ol>
<p>Many email services offer a free-tier plan offering a limited number of emails you can send per month/day for free. When I started exploring this space for Sametable in October 2019, Mailgun stood out to be a no-brainer—It offers 10,000 emails for free per month! </p>
<p>But, sadly, as I was researching for this section write-up, I learned that it no longer offers that. Despite that, though, I would still stick to Mailgun, on their <a target="_blank" href="https://www.mailgun.com/pricing">pay-as-you-go</a> plan: 1000 emails sent will cost you 80 cents.</p>
<p>If you would rather not pay a cent for whatever reason, here are two options for you that I could find:</p>
<ul>
<li><a target="_blank" href="https://www.mailjet.com/pricing/">https://www.mailjet.com/pricing/</a></li>
<li><a target="_blank" href="https://www.sendinblue.com/pricing/">https://www.sendinblue.com/pricing/</a></li>
</ul>
<p>But do go down this path while being aware there's no guarantee that these free-tier plans will stay that way forever as was the case with Mailgun.</p>
<h3 id="heading-implementation">Implementation</h3>
<h4 id="heading-wrapper-file">Wrapper file</h4>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/lib/email.js</span>

<span class="hljs-comment">// Run 'npm install mailgun-js' in your 'server' folder</span>
<span class="hljs-keyword">const</span> mailgun = <span class="hljs-built_in">require</span>(<span class="hljs-string">"mailgun-js"</span>);

<span class="hljs-keyword">const</span> DOMAIN = <span class="hljs-string">"mail.sametable.app"</span>;

<span class="hljs-keyword">const</span> mg = mailgun({
  <span class="hljs-attr">apiKey</span>: process.env.MAILGUN_API_KEY,
  <span class="hljs-attr">domain</span>: DOMAIN,
});

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">send</span>(<span class="hljs-params">data</span>) </span>{
  mg.messages().send(data, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">error</span>) </span>{
    <span class="hljs-keyword">if</span> (!error) <span class="hljs-keyword">return</span>;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Email send error:"</span>, error);
  });
}

<span class="hljs-built_in">module</span>.exports = {
  send,
};
</code></pre>
<h4 id="heading-usage-2">Usage</h4>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> mailer = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../lib/email"</span>);

<span class="hljs-comment">// Simplified for only email-related stuff</span>
router.post(
  <span class="hljs-string">"/resetPassword"</span>,
  upload.none(),
  <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { email } = req.body;
    <span class="hljs-keyword">const</span> data = {
      <span class="hljs-attr">from</span>: <span class="hljs-string">"Sametable &lt;feedback@sametable.app&gt;"</span>,
      <span class="hljs-attr">to</span>: email,
      <span class="hljs-attr">subject</span>: <span class="hljs-string">"Reset your password"</span>,
      <span class="hljs-attr">text</span>: <span class="hljs-string">`Click this link to reset your password: https://example.com?token=1234`</span>,
    };
    mailer.send(data);
    res.json({});
  })
);
</code></pre>
<h3 id="heading-email-templates">Email templates</h3>
<p>Each type of email you send could have its own email template whose content can be varied with dynamic values you can provide.</p>
<h4 id="heading-tool">Tool</h4>
<p><a target="_blank" href="https://mjml.io/"><strong>mjml</strong></a> is the tool I use to build my email templates. Sure, there are many drag-and-drop email builders out there that don't intimidate with the sight of 'codes'. But if you know just basic React/HTML/CSS, mjml would give you great usability and maximum flexibility.</p>
<p>It's easy to <a target="_blank" href="https://mjml.io/getting-started/1">get started</a>. Like the email builders, you compose a template with a bunch of reusable components, and you customize them by providing values to their props.</p>
<p>Here are the places where I would write my templates:</p>
<ul>
<li>This <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml">VSCode extension</a></li>
<li><a target="_blank" href="https://mjml.io/try-it-live">Live code editor</a></li>
</ul>
<h4 id="heading-example-template">Example template</h4>
<details>
  <summary>Email Template</summary>

<code>html
&lt;mjml&gt;
  &lt;mj-head&gt;
    &lt;mj-attributes&gt;
      &lt;mj-class
        name="font-family"
        font-family="-apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',sans-serif"
      /&gt;
      &lt;mj-class name="fw-600" font-weight="600" /&gt;
    &lt;/mj-attributes&gt;
  &lt;/mj-head&gt;
  &lt;mj-body&gt;
    &lt;mj-section&gt;
      &lt;mj-column&gt;
        &lt;mj-image
          width="150px"
          src="https://www.dl.dropboxusercontent.com/s/pgtwrnfa3lqkf5r/sametable_logo_with_text.png"
        /&gt;
      &lt;/mj-column&gt;
    &lt;/mj-section&gt;
    &lt;mj-section&gt;
      &lt;mj-column&gt;
        &lt;mj-text align="center" font-size="20px" mj-class="font-family"
          &gt;{{assigner_name}} assigned a project to you&lt;/mj-text
        &gt;
        &lt;mj-spacer height="10px" /&gt;
        &lt;mj-text align="center" font-size="25px" mj-class="font-family fw-600"
          &gt;{{project_title}}&lt;/mj-text
        &gt;
        &lt;mj-spacer height="25px" /&gt;
        &lt;mj-button
          font-size="16px"
          mj-class="font-family fw-600"
          background-color="#000"
          color="white"
          href="{{invite_link}}"
          &gt;View the project&lt;/mj-button
        &gt;
      &lt;/mj-column&gt;
    &lt;/mj-section&gt;
    &lt;mj-spacer height="55px" /&gt;
    &lt;mj-section background-color="#EEEBE7" padding="25px 40px"&gt;
      &lt;mj-column&gt;
        &lt;mj-text
          align="center"
          color="#45495d"
          font-size="15px"
          line-height="14px"
        &gt;
          Problems or questions? Feel free to reply to this email.
        &lt;/mj-text&gt;
        &lt;mj-text padding="30px 0 0 0" align="center" font-size="16px"&gt;
          Made with ❤️ by
          &lt;a href="https://twitter.com/kheohyeewei"&gt;@kheohyeewei&lt;/a&gt;
        &lt;/mj-text&gt;
      &lt;/mj-column&gt;
    &lt;/mj-section&gt;
  &lt;/mj-body&gt;
&lt;/mjml&gt;</code>

</details>

<h5 id="heading-result">Result</h5>
<p><img src="https://i.imgur.com/JnaSDYJ.png" width="600" height="400" alt="JnaSDYJ" loading="lazy"></p>
<p>Notice the placeholder names that are wrapped in double curly brackets such as <code>{{project_title}}</code>. They will be replaced with their corresponding value by, in my case, Mailgun, before being sent out.</p>
<h4 id="heading-integration-with-mailgun">Integration with Mailgun</h4>
<p>First, generate HTML from your mjml templates. You are able to do that with the VSCode extension or the web-based editor:</p>
<p><img src="https://i.imgur.com/O9mnBvN.png" alt="generate html in mjml online code editor" width="600" height="400" loading="lazy"></p>
<p>Then create a new template on your Mailgun dashboard:</p>
<p><img src="https://i.imgur.com/OkvLxiT.png" alt="create message template on mailgun dashboard" width="600" height="400" loading="lazy"></p>
<h4 id="heading-send-an-email-with-mailgun-in-nodejs">Send an email with Mailgun in Nodejs</h4>
<p>Inside a route:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> data = {
  <span class="hljs-attr">from</span>: <span class="hljs-string">"Sametable &lt;feedback@sametable.app&gt;"</span>,
  <span class="hljs-attr">to</span>: email,
  <span class="hljs-attr">subject</span>: <span class="hljs-string">`Hello`</span>,
  <span class="hljs-attr">template</span>: <span class="hljs-string">"invite_project"</span>, <span class="hljs-comment">// the template's name you gave when you created it in mailgun</span>
  <span class="hljs-string">"v:invite_link"</span>: inviteLink,
  <span class="hljs-string">"v:assigner_name"</span>: fullname,
  <span class="hljs-string">"v:project_title"</span>: title,
};

mailer.send(data);
</code></pre>
<p>Notice that, to associate a value with a placeholder name in a template: <code>"v:project_title":'Project Mario'</code>.</p>
<h3 id="heading-how-to-get-one-of-those-hiexamplecom">How to get one of those <code>hi@example.com</code></h3>
<p>It's an email address people use to contact you about your SaaS, rather than with a <code>lola887@hotmail.com</code>.</p>
<p>There are three options on my radar:</p>
<ol>
<li>If you are on Mailgun, follow <a target="_blank" href="https://renzo.lucioni.xyz/mail-forwarding-with-mailgun/">this guide</a>. However, the new pay-as-you-go tier has excluded the feature(<code>Inbound Email Routing</code>) that makes this possible. So perhaps the next option;</li>
<li>If I ever get kicked out of my '10,000' free-tier in Mailgun, I would give this a shot https://forwardemail.net/en</li>
<li>If all else failed, pay for <a target="_blank" href="https://gsuite.google.com.my/intl/en_my/products/gmail/">'Gmail on G Suite'</a>.</li>
</ol>
<h2 id="heading-tenancy">Tenancy</h2>
<p>When an organization, say, Acme Inc., signs up on your SaaS, it's considered a 'tenant' — They 'occupy' a spot on your service.</p>
<p>While I'd heard of the 'multi-tenancy' term being associated with a SaaS before, I never had the slightest idea about implementing it. I always thought that it'd involve some cryptic computer-sciency maneuvering that I couldn't possibly have figured it all out by myself.</p>
<p>Fortunately, there is an easy way to do 'multi-tenancy':</p>
<blockquote>
<p>Single database; all clients share the same tables; each client has a <code>tenant_id</code>; queries the database as per an API request by <code>WHERE tenant_id = $ID</code>.</p>
</blockquote>
<p>So don't worry—If you know basic SQL (again indicating the importance of mastering the basics in anything you do!), you should have a clear picture on the steps required to implement this.</p>
<p>Here are three instrumental resources about 'multi-tenancy' I bookmarked before:</p>
<ul>
<li><a target="_blank" href="https://stackoverflow.com/a/47783180/73323">https://stackoverflow.com/a/47783180/73323</a></li>
<li><a target="_blank" href="https://stackoverflow.com/a/44530588/73323">https://stackoverflow.com/a/44530588/73323</a></li>
<li><a target="_blank" href="https://blog.checklyhq.com/building-a-multi-tenant-saas-data-model/">https://blog.checklyhq.com/building-a-multi-tenant-saas-data-model/</a></li>
</ul>
<h2 id="heading-domain-name">Domain name</h2>
<p>Sametable.app domain and all its DNS records are hosted in <a target="_blank" href="https://www.namecheap.com/"><strong>NameCheap</strong></a>. I was on <a target="_blank" href="https://www.hover.com/">hover</a> before(it still hosts my personal website's domain). But I hit a limitation there when I tried to enter my Mailgun's DKIM value. Namecheap also has more competitive prices in my experience.</p>
<p>At which stage in your SaaS development should you get a domain name? Well, I would say not until when the lack of a DNS registrar is blocking your development. In my case, I deferred it until I had to integrate Mailgun which requires creating a bunch of DNS records in a domain.</p>
<h3 id="heading-how-to-get-one-of-those-appexamplecom">How to get one of those <code>app.example.com</code></h3>
<p>You know those URLs that has a <code>app</code> in front of it like <code>app.example.io</code>? Yea, that's a 'custom domain' with the 'app' as its 'subdomain'. And it all started with having a domain name. </p>
<p>So go ahead and get one in Namecheap or whatever. Then, in my case with Firebase, just <a target="_blank" href="https://firebase.google.com/docs/hosting/custom-domain">follow this tutorial</a> and you will be fine.</p>
<h2 id="heading-deployment">Deployment</h2>
<p>Ugh. This was a stage where I struggled for the longest time ?. It was one hell of a journey where I found myself doubling down on a cloud platform but end up bailing out as I found out their downsides to optimize for developer experience, costs, quota, and performance(latency).</p>
<p>The journey started with me jumping head-first(bad idea) into Digital Ocean since I saw it recommended a lot in the IndieHackers forum. And sure enough, I managed to get my Nodejs up and running in a VM by following <a target="_blank" href="https://coderrocketfuel.com/article/create-and-deploy-an-express-rest-api-to-a-digitalocean-server#configure-and-deploy-your-node-js-app">closely</a> the <a target="_blank" href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-18-04">tutorials</a>. </p>
<p>Then I found out that the <a target="_blank" href="https://www.digitalocean.com/products/spaces/">DO Space</a> wasn't exactly AWS S3—It <a target="_blank" href="https://ideas.digitalocean.com/ideas/DO-I-318">can't</a> host my SPA. </p>
<p>Although I <a target="_blank" href="https://coderrocketfuel.com/article/deploy-a-create-react-app-website-to-digitalocean">could have</a> hosted it in my droplet and <a target="_blank" href="https://www.youtube.com/watch?v=2X_Tp_G7aTs">hook up</a> a third-party CDN like CloudFlare to the droplet, it seemed to me unnecessarily convoluted compared to the S3+Cloudfront setup. I was also using a DO's Managed Database(Postgresql) because I didn't want to manage my DB and tweak in the <code>*.config</code> files myself. That costs a fixed \$15/month.</p>
<p>Then I learned about <a target="_blank" href="https://aws.amazon.com/lightsail/">AWS Lightsail</a> which is a mirror image of DO, but to my surprise, with more competitive <a target="_blank" href="https://aws.amazon.com/lightsail/pricing/">quota</a> at a given price point:</p>
<p><strong>VM at \$5/month</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>AWS Lightsail</td><td>Digital Ocean</td></tr>
</thead>
<tbody>
<tr>
<td>1 GB Memory</td><td>1 GB Memory</td></tr>
<tr>
<td>1 Core Processor</td><td>1 Core Processor</td></tr>
<tr>
<td><strong>40 GB</strong> SSD Disk</td><td>25 GB SSD Disk</td></tr>
<tr>
<td><strong>2 TB</strong> transfer</td><td>1 TB transfer</td></tr>
</tbody>
</table>
</div><p><strong>Managed database at \$15/month</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>AWS Lightsail</td><td>Digital Ocean</td></tr>
</thead>
<tbody>
<tr>
<td>1 GB Memory</td><td>1 GB Memory</td></tr>
<tr>
<td>1 Core Processor</td><td>1 Core Processor</td></tr>
<tr>
<td><strong>40 GB</strong> SSD Disk</td><td>10 GB SSD Disk</td></tr>
</tbody>
</table>
</div><p>So I started betting on Lightsail instead. But, the \$15/month for a managed database in Lightsail got to me at one point. I didn't want to have to pay that money when I wasn't even sure that I would ever have any paying customers.</p>
<p>At this point, I supposed that I had to get my hands dirty to optimize for the cost factor. So I started looking into wiring AWS EC2, RDS, etc. But there were just too many of AWS-specific things I had to pick up, and the AWS doc wasn't exactly helping either—It's one rabbit hole after another just to do one thing because I just needed something to host my SPA and Nodejs for goodness sake!</p>
<p>Then I checked back in IndieHacker for a sanity check, and came across <a target="_blank" href="https://render.com/">render.com</a>. It seemed perfect! It's one of those tools that are on a mission '<em>so you can focus on building your app</em>'. The tutorials were short and got you up and running in no time. And here is the '<em>but</em>'—It was <a target="_blank" href="https://render.com/pricing">expensive</a>:</p>
<p><strong>Comparison of Lightsail and Render at their lowest price point</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>AWS Lightsail(\$3.50/mo)</td><td>Render(\$7/mo)</td></tr>
</thead>
<tbody>
<tr>
<td>512 GB Memory</td><td>512 MB Memory</td></tr>
<tr>
<td>1 Core Processor</td><td>Shared Processor</td></tr>
<tr>
<td>20 GB SSD Disk</td><td>$0.25/GB/mo SSD Disk(20GB = $5/mo)</td></tr>
<tr>
<td>1 TB transfer</td><td>100 GB/mo. $0.10/GB above that(1TB = $90/mo)</td></tr>
</tbody>
</table>
</div><p>And that's just for hosting my Nodejs!</p>
<p>So what now?! Do I just say <em>f*** it</em> and do whatever it takes to 'ship it'?</p>
<p>But I held my ground. I revisited AWS again. I still believed AWS was the answer because everyone else is singing its song. I must be missing something! </p>
<p>This time I considered their higher-level tools like AWS AppSync and Amplify. But I couldn't overlook the fact that both of them force me to completely work by their standards and library. So at this point, I'd had it with AWS, and turned to another...platform: <strong>Google Cloud Platform(GCP)</strong>.</p>
<p>Sametable's Nodejs, Redis, and Postgresql are hosted on <strong>GCP</strong>.</p>
<p>The thing that drew me to GCP was its documentation—It's much more linear; code snippets everywhere for your specific language; step-by-step guides about the common things you would do for a web app. Plus, it's serverless! Which means your cost is proportional to your usage.</p>
<h3 id="heading-deploy-nodejs">Deploy Nodejs</h3>
<p>The GAE <a target="_blank" href="https://cloud.google.com/appengine/docs/the-appengine-environments">'standard environment'</a> hosts my Nodejs.</p>
<h4 id="heading-cost">Cost</h4>
<p>GAE's standard environment has <a target="_blank" href="https://cloud.google.com/free/docs/gcp-free-tier#always-free-usage-limits">free quota</a> unlike the 'flexible environment'. Beyond that, you will pay only if somebody is using your SaaS ?.</p>
<h4 id="heading-guide-2">Guide</h4>
<p>This was the <em>only</em> guide I relied on. It was my north star. It covers Nodejs, Postgresql, Redis, file storage, and more:</p>
<blockquote>
<p><a target="_blank" href="https://cloud.google.com/appengine/docs/standard/nodejs">https://cloud.google.com/appengine/docs/standard/nodejs</a></p>
</blockquote>
<p>Start with the <a target="_blank" href="https://cloud.google.com/appengine/docs/standard/nodejs/quickstart">'Quick Start'</a> tutorial because it will set you up with the <code>gcloud cli</code> which you are going to need when following the rest of the guides, where you will find commands you can run to follow along. </p>
<p>If you aren't comfortable with the CLI environment, the guides will provide alternative steps to achieve the same thing on the GCP dashboard. I love it.</p>
<p>I noticed that while going through the GCP doc, I never had to open more than 4 tabs in my browser. It was the complete opposite with AWS doc—My browser would be <em>packed</em> with it.</p>
<h3 id="heading-deploy-postgresql">Deploy Postgresql</h3>
<h4 id="heading-guide-3">Guide</h4>
<p><a target="_blank" href="https://cloud.google.com/sql/docs/postgres/connect-app-engine-standard">https://cloud.google.com/sql/docs/postgres/connect-app-engine-standard</a></p>
<p>Just follow it and you will be fine.</p>
<h4 id="heading-cost-1">Cost</h4>
<p>An instance of Cloud SQL runs a full virtual machine. And once a VM has been provisioned, it won't automatically turn itself off when, for example, it has not seen any usage for 15 minutes. So you will be billed for every hour an instance is running for an entire month unless it'd been manually stopped.</p>
<p>The primary factor that will affect your cost here, particularly in the early days, is the grade of the <a target="_blank" href="https://cloud.google.com/sql/pricing#2nd-gen-instance-pricing">machine type</a>. The default machine type for a Cloud SQL is a <code>db-n1-standard-1</code>, and the 'cheapest' one you can get is a <code>db-f1-micro</code>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>db-n1-standard-1</td><td>db-f1-micro</td><td>Digital Ocean Managed DB</td></tr>
</thead>
<tbody>
<tr>
<td>1 vCPU</td><td>1 shared vCPU</td><td>1 vCPU</td></tr>
<tr>
<td>3.75GB Memory</td><td>0.6GB Memory</td><td>1GB Memory</td></tr>
<tr>
<td>10GB SSD Storage</td><td>10GB SSD Storage</td><td>10GB SSD Storage</td></tr>
<tr>
<td><a target="_blank" href="https://cloud.google.com/products/calculator/#id=c0040a15-933d-4dc3-8022-1428fc210050">~USD 51.01</a></td><td><a target="_blank" href="https://cloud.google.com/sql/pricing#2nd-gen-instance-pricing">~USD 9.37</a></td><td><a target="_blank" href="https://www.digitalocean.com/pricing/#managed-databases">USD 15.00</a></td></tr>
</tbody>
</table>
</div><p>The other two cost factors are <a target="_blank" href="https://cloud.google.com/sql/pricing#pg-storage-networking-prices">storage and network egress</a>. But they are charged monthly, so they probably won't have as big of an impact on the bill of your nascent SaaS.</p>
<p>If you find the price tags to be too hefty to your liking, keep in mind that they are a <em>managed</em> database. You are paying for all the times and anxiety saved from doing devops on your database. For me, it's worth it.</p>
<h3 id="heading-setup-schemas-in-production-database">Setup schemas in production database</h3>
<p>Now that I have got a database deployed for production, it's time to dress it up with all my schemas from the <code>.sql</code> <a href="#schemas-file">file</a>. To do that, I need to connect to the database from pgAdmin:</p>
<ol>
<li><a target="_blank" href="https://cloud.google.com/sql/docs/postgres/external-connection-methods">https://cloud.google.com/sql/docs/postgres/external-connection-methods</a></li>
<li>There you will find a table with a list of options for connecting from an external application. I went with the first one: <strong>Public IP address with SSL</strong>. Follow all the guides in the 'More information' column and you will have all the information needed to create a server in your pgAdmin. You will be fine. If not, <a href="mailto:oldjoy@protonmail.com">email me</a> and I will provide assistance.</li>
</ol>
<h3 id="heading-deploy-redis">Deploy Redis</h3>
<p>If you were following the main guide about Nodejs, you can't miss <a target="_blank" href="https://cloud.google.com/appengine/docs/standard/nodejs/using-memorystore">this guide</a> about setting up your Redis in MemoryStore. But I figured it would be more cost-effective to <strong>host my Redis in a Google Compute Engine</strong>(GCE) which has, unlike MemoryStore, free quota in certain aspects. (<a target="_blank" href="https://github.com/ripienaar/free-for-dev#major-cloud-providers">See this</a> for comparison of free quota across different cloud platforms)</p>
<h4 id="heading-guide-4">Guide</h4>
<ol>
<li><a target="_blank" href="https://cloud.google.com/community/tutorials/setting-up-redis">Setup</a> Redis in a VM.</li>
</ol>
<p>2) <a target="_blank" href="https://cloud.google.com/appengine/docs/standard/python/connecting-vpc">Setup</a> VPC:</p>
<blockquote>
<p>Serverless VPC Access enables you to connect from your App Engine app directly to your VPC network, <strong>allowing access to Compute Engine VM instances</strong>, Memorystore instances, and any other resources with an internal IP address.</p>
</blockquote>
<ol start="3">
<li><p>In your <code>app.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">vpc_access_connector:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">"&lt;YOURS_HERE&gt;"</span>

<span class="hljs-attr">env_variables:</span>
  <span class="hljs-attr">REDIS_PASSWORD:</span> <span class="hljs-string">"&lt;PASSWORD_YOU_SET_IN_A_GUIDE_ABOVE&gt;"</span>
  <span class="hljs-attr">REDISHOST:</span> <span class="hljs-string">"&lt;INTERNAL_IP_OF_YOUR_VM&gt;"</span>
  <span class="hljs-attr">REDISPORT:</span> <span class="hljs-string">"6379"</span> <span class="hljs-comment"># default port when install redis</span>
</code></pre>
<figure>
 <img src="https://i.imgur.com/N4bqhcT.png" alt="internal ip of a GCE vm" width="600" height="400" loading="lazy">
 <figcaption>
     <small>Internal IP of a GCE</small>
 </figcaption>
</figure>
</li>
<li><p>Finally, in <code>lib/redis.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> redis = <span class="hljs-built_in">require</span>(<span class="hljs-string">"redis"</span>);

<span class="hljs-keyword">const</span> redisClient = redis.createClient(
  NODE_ENV === <span class="hljs-string">"production"</span>
    ? {
        <span class="hljs-attr">host</span>: process.env.REDISHOST,
        <span class="hljs-attr">port</span>: process.env.REDISPORT, <span class="hljs-comment">// default to 6379 if wasn't set</span>
        <span class="hljs-attr">no_ready_check</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">auth_pass</span>: process.env.REDIS_PASSWORD,
      }
    : {} <span class="hljs-comment">// just use the default: localhost and ports</span>
);

<span class="hljs-built_in">module</span>.exports = {
  redisClient,
};
</code></pre>
</li>
</ol>
<h3 id="heading-file-storage">File Storage</h3>
<p><a target="_blank" href="https://cloud.google.com/storage/docs">Cloud Storage</a> is what you need for your users to upload their files such as images which they will need to retrieve and possibly display later.</p>
<h4 id="heading-cost-2">Cost</h4>
<p>There is a <a target="_blank" href="https://cloud.google.com/free/docs/gcp-free-tier#always-free-usage-limits">free tier</a> for Cloud Storage too.</p>
<h4 id="heading-guide-5">Guide</h4>
<ul>
<li>You will be fine:
<a target="_blank" href="https://cloud.google.com/appengine/docs/standard/nodejs/using-cloud-storage">https://cloud.google.com/appengine/docs/standard/nodejs/using-cloud-storage</a></li>
</ul>
<h3 id="heading-deploy-new-changes-in-back-end">Deploy New Changes in Back-end</h3>
<p>I have a npm script in the root's <code>package.json</code> to publish new changes in my back-end to GCP:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"deploy-server"</span>: <span class="hljs-string">"gcloud app deploy ./server/app.yaml"</span>
}
</code></pre>
<p>Then run it in a terminal at your project's root:</p>
<pre><code class="lang-json">npm run deploy-server
</code></pre>
<h2 id="heading-hosting-your-spa">Hosting Your SPA</h2>
<p>When I was still on Lightsail, my SPA was <a target="_blank" href="https://medium.com/@kilgarenone/deploy-spa-to-aws-9302796acd88">hosted</a> on S3+Cloudfront because I assumed it's better to keep them under the same platform for better latency. Then I found GCP. </p>
<p>As a beat refugee from AWS landing in GCP, I first explored the 'Cloud Storage' to host my SPA, and turns out it wasn't ideal for SPA. It's rather convoluted. So you can skip that.</p>
<p>Then I tried hosting my SPA in <a target="_blank" href="https://firebase.google.com/docs/hosting/quickstart"><strong>Firebase</strong></a>. Easily done in minutes even when it was my first time there. I love it.</p>
<p>Another option you can consider is <a target="_blank" href="https://netlify.com">Netlify</a> which is super easy to get started too.</p>
<h3 id="heading-deploy-new-changes-in-front-end">Deploy New Changes in Front-end</h3>
<p>Similarly to deploying back-end changes, I have another npm script in the root's <code>package.json</code> to publish new changes in my front-end to Firebase:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"deploy-client"</span>: <span class="hljs-string">"npm run build-client &amp;&amp; firebase deploy"</span>,
    <span class="hljs-attr">"build-client"</span>: <span class="hljs-string">"npm run test &amp;&amp; cd client &amp;&amp; npm i &amp;&amp; npm run build"</span>,
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"npm run lint"</span>,
    <span class="hljs-attr">"lint"</span>: <span class="hljs-string">"npm run lint:js &amp;&amp; npm run lint:css"</span>,
    <span class="hljs-attr">"lint:js"</span>: <span class="hljs-string">"eslint 'client/src/**/*.js' --fix"</span>,
    <span class="hljs-attr">"lint:css"</span>: <span class="hljs-string">"stylelint '**/*.{scss,css}' '!client/dist/**/*'"</span>
}
</code></pre>
<p><em>"Whoa hold on, where all that stuff come from??"</em></p>
<p>They are a chain of scripts that each runs sequentially upon triggered by the <code>deploy-client</code> script. The <code>&amp;&amp;</code> character is what glues them together.</p>
<p>Let's hold each other's hands and walk through it from start to finish:</p>
<ol>
<li>First, we do <code>npm run deploy-client</code>,</li>
<li>which runs <code>build-client</code> first,</li>
<li>which runs <code>test</code> first, (see, we are just following where a script and its <code>&amp;&amp;</code> lead us, which is why <code>firebase deploy</code> won't run just yet)</li>
<li>which runs <code>lint</code>,</li>
<li>which brings us to <code>lint:js</code> first, and next, <code>lint:css</code>,</li>
<li>then back to <code>cd client</code>, followed by <code>npm i</code> and <code>npm run build</code>,</li>
<li>and finally, it's <code>firebase deploy</code>'s turn to run.</li>
</ol>
<p><strong>Tip</strong>: If the changes you made are full-stack, you could have a script that deploys 'client' and 'server' together:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"deploy-all"</span>: <span class="hljs-string">"npm run deploy-server &amp;&amp; npm run deploy-client"</span>,
}
</code></pre>
<h2 id="heading-rich-text-editor">Rich-text Editor</h2>
<p>Building the rich-text editor in Sametable was the second most challenging thing for me. I realized that I could have had it easy with those drop-in editors such as CKEditor and TinyMCE, but I wanted to be able to craft the writing experience in the editor, and nothing can do that better than <a target="_blank" href="https://prosemirror.net/"><strong>ProseMirror</strong></a>. Sure, I had other options too, which I decided against for several reasons:</p>
<ol>
<li><a target="_blank" href="https://quilljs.com/">Quilljs</a><ul>
<li>Seemed many unaddressed <a target="_blank" href="https://github.com/quilljs/quill/issues">issues</a>.</li>
<li>Shaped specifically by a special-interest group.</li>
<li>Involves hacky workaround once you ventured out from the set of standard use cases.</li>
</ul>
</li>
<li><a target="_blank" href="https://draftjs.org/">Draftjs</a><ul>
<li>They are tightly coupled with React.</li>
<li>With the overhead of virtual DOM, they won't perform as well as Prosemirror.</li>
</ul>
</li>
<li><a target="_blank" href="https://trix-editor.org/">trix</a><ul>
<li>Based on Web Component. I had issues integrating it in Preact.</li>
<li>It wasn't flexible to build a customized editing experience.</li>
</ul>
</li>
</ol>
<p><strong>Prosemirror</strong> is undoubtedly an <em>impeccable</em> library. But learning it was not for the faint of heart as far as I'm concerned. </p>
<p>I failed to build any encompassing mental models of it even after I'd read the <a target="_blank" href="https://prosemirror.net/docs/guide/">guide</a> several times. The only way I could make progress from there was by cross-referencing existing code examples and the <a target="_blank" href="https://prosemirror.net/docs/ref/">manual</a>, and trial-and-error from there. And if I exhausted that too, then I would ask in the forum and it's always answered. I wouldn't bother with StackOverflow unless maybe for the popular Quilljs.</p>
<p>These were the places I went scrounging for code samples:</p>
<ul>
<li>The <a target="_blank" href="https://prosemirror.net/examples/">official examples</a></li>
<li>Search the <a target="_blank" href="https://discuss.prosemirror.net/">forum</a></li>
<li>'Fork' <a target="_blank" href="https://github.com/prosemirror/prosemirror-example-setup">prosemirror-example-setup</a></li>
<li>An editor called <a target="_blank" href="https://tiptap.scrumpy.io/">tiptap</a> that's based on Prosemirror but built for Vuejs. The codebase actually has very few bits of Vuejs. So you can find lots of helpful Prosemirror-specific snippets there(thanks guys!).</li>
</ul>
<p>In keeping with the spirit of this learning journey, I have extracted the rich-text editor of Sametable in a CodeSandBox:</p>
<p><a target="_blank" href="https://codesandbox.io/s/compassionate-montalcini-gcgwc">https://codesandbox.io/s/compassionate-montalcini-gcgwc</a></p>
<p>?</p>
<p>(<strong>Note</strong>: Prosemirror is framework-agnostic; the CodeSandBox demo only uses 'create-react-app' for bundling ES6 modules.)</p>
<h2 id="heading-cors">CORS</h2>
<p>To stop your browser from complaining about CORS issues, it's <a target="_blank" href="https://medium.com/@kilgarenone/deploy-your-nodejs-app-to-digital-ocean-1de40797666f#4aa4">all about</a> getting your backend to send those <code>Access-Control-Allow-*</code> headers back per request. (Apologies for oversimplification is in order)</p>
<p>But, correct me if I'm wrong, there's <a target="_blank" href="https://stackoverflow.com/a/60502433/73323">no way</a> to configure CORS in GAE itself. So I had to do it with the <a target="_blank" href="https://www.npmjs.com/package/cors">cors</a> npm package:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">"cors"</span>);

<span class="hljs-keyword">const</span> ALLOWED_ORIGINS = [
  <span class="hljs-string">"http://localhost:8008"</span>,
  <span class="hljs-string">"https://web.sametable.app"</span>, <span class="hljs-comment">// your SPA's domain</span>
];

app.use(
  cors({
    <span class="hljs-attr">credentials</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// include Access-Control-Allow-Credentials: true. remember set xhr.withCredentials = true;</span>
    origin(origin, callback) {
      <span class="hljs-comment">// allow requests with no origin</span>
      <span class="hljs-comment">// (like mobile apps or curl requests)</span>
      <span class="hljs-keyword">if</span> (!origin) <span class="hljs-keyword">return</span> callback(<span class="hljs-literal">null</span>, <span class="hljs-literal">true</span>);
      <span class="hljs-keyword">if</span> (ALLOWED_ORIGINS.indexOf(origin) === <span class="hljs-number">-1</span>) {
        <span class="hljs-keyword">const</span> msg =
          <span class="hljs-string">"The CORS policy for this site does not "</span> +
          <span class="hljs-string">"allow access from the specified Origin."</span>;
        <span class="hljs-keyword">return</span> callback(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(msg), <span class="hljs-literal">false</span>);
      }
      <span class="hljs-keyword">return</span> callback(<span class="hljs-literal">null</span>, <span class="hljs-literal">true</span>);
    },
  })
);
</code></pre>
<h2 id="heading-payment-amp-subscription">Payment &amp; Subscription</h2>
<p>A SaaS usually allows users to pay and subscribe to access the paid features you have designated.</p>
<p>To enable that possibility in Sametable, I use <strong>Stripe</strong> to handle both the payment and subscription flows.</p>
<h3 id="heading-guide-6">Guide</h3>
<p>There are two ways to implement them:</p>
<ol>
<li><a target="_blank" href="https://stripe.com/docs/billing/subscriptions/fixed-price">Very hands-on</a> that's great for customizing your UI.</li>
<li><a target="_blank" href="https://stripe.com/docs/payments/checkout/set-up-a-subscription"><strong>Checkout</strong></a>. Fastest to implement. This was what I did.</li>
</ol>
<h3 id="heading-webhook">Webhook</h3>
<p>The last key component I needed for this piece was a 'webhook' which is basically just a typical endpoint in your Nodejs that can be called by a third-party such as Stripe.</p>
<p>I created a webhook that will be called when a payment has been charged successfully to signify in the user record that corresponds to the payee as a PRO user in Sametable from there onwards:</p>
<pre><code class="lang-javascript">router.post(
  <span class="hljs-string">"/webhook/payment_success"</span>,
  bodyParser.raw({ <span class="hljs-attr">type</span>: <span class="hljs-string">"application/json"</span> }),
  asyncHandler(<span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">const</span> sig = req.headers[<span class="hljs-string">"stripe-signature"</span>];

    <span class="hljs-keyword">let</span> event;

    <span class="hljs-keyword">try</span> {
      event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).send(<span class="hljs-string">`Webhook Error: <span class="hljs-subst">${err.message}</span>`</span>);
    }

    <span class="hljs-comment">// Handle the checkout.session.completed event</span>
    <span class="hljs-keyword">if</span> (event.type === <span class="hljs-string">"checkout.session.completed"</span>) {
      <span class="hljs-comment">// 'session' doc: https://stripe.com/docs/api/checkout/sessions/object</span>
      <span class="hljs-keyword">const</span> session = event.data.object;

      <span class="hljs-comment">// here you can query your database to, for example,</span>
      <span class="hljs-comment">// update a particular user/tenant's record</span>

      <span class="hljs-comment">// Return a res to acknowledge receipt of the event</span>
      res.json({ <span class="hljs-attr">received</span>: <span class="hljs-literal">true</span> });
    } <span class="hljs-keyword">else</span> {
      res.status(<span class="hljs-number">400</span>);
    }
  })
);
</code></pre>
<h4 id="heading-reference">Reference</h4>
<p>Here is a code snippet of a webhook:
<a target="_blank" href="https://stripe.com/docs/webhooks/signatures#verify-official-libraries">https://stripe.com/docs/webhooks/signatures#verify-official-libraries</a></p>
<h4 id="heading-guide-7">Guide</h4>
<ul>
<li><a target="_blank" href="https://stripe.com/docs/webhooks">https://stripe.com/docs/webhooks</a></li>
</ul>
<h2 id="heading-landing-page">Landing page</h2>
<h3 id="heading-building">Building</h3>
<p>I use <a target="_blank" href="https://www.11ty.dev/"><strong>Eleventy</strong></a> to build the landing page of Sametable. I <a target="_blank" href="https://twitter.com/devongovett/status/1222953655722110981">wouldn't</a> recommend Gatsby or Nextjs. They are overkill for this job.</p>
<p>I started with one of the <a target="_blank" href="https://www.11ty.dev/docs/starter/">starter projects</a> as I was impatient to get my page off the ground. But I struggled working in them. </p>
<p>Although Eleventy claims to be a simple SSG, there are actually quite a few concepts to grasp if you are new to <a target="_blank" href="https://www.staticgen.com/">static site generators</a>(SSG). Coupled with the tools introduced by the starter kits, things can get complex. So I decided to start from zero and take my time reading the doc from start to finish, slowly building up. Quiet and easy.</p>
<h4 id="heading-guides">Guides</h4>
<ul>
<li><strong>Long version</strong><ul>
<li><a target="_blank" href="https://tatianamac.com/posts/beginner-eleventy-tutorial-parti/">https://tatianamac.com/posts/beginner-eleventy-tutorial-parti/</a></li>
<li><a target="_blank" href="https://www.11ty.dev/docs/">https://www.11ty.dev/docs/</a></li>
</ul>
</li>
<li><strong>Short version</strong>: <a target="_blank" href="https://github.com/kilgarenone/personal-website">https://github.com/kilgarenone/personal-website</a> (the first website I built as my personal site while learning 11ty. It has a homepage and blog posts. Very few concepts introduced here. You could start with this 'starter project')</li>
</ul>
<h3 id="heading-hosting-1">Hosting</h3>
<p>I use <a target="_blank" href="https://www.netlify.com/"><strong>Netlify</strong></a> to host the landing page. There are also <a target="_blank" href="https://surge.sh/">surge.sh</a> and <a target="_blank" href="https://vercel.com">Vercel</a>. You will be fine here.</p>
<h2 id="heading-terms-and-conditions">Terms and Conditions</h2>
<p>T&amp;C makes your SaaS legit. As far as I know, here are your options to come up with them:</p>
<ol>
<li>Write your own <a target="_blank" href="https://pinboard.in/tos/">https://pinboard.in/tos/</a>.</li>
<li>Copy and paste others'. Change accordingly. Never easy in my experience.</li>
<li>Lawyer up.</li>
<li>Generate them in <a target="_blank" href="https://getterms.io/"><strong>getterms.io</strong></a>.</li>
</ol>
<h2 id="heading-marketing">Marketing</h2>
<p>There is no shortage of marketing posts saying it was a bad idea to "<em>Let the product speaks for itself</em>". Well, not unless you were trying to 'hack growth' to win the game.</p>
<p>The following is the trajectory of existence I have in mind for Sametable:</p>
<ol>
<li>Build something that purportedly solves a problem.</li>
<li>Do your SEO. Write the blog posts. Anyone who is affected by the problem that you have solved for will search for it, or know about it by word of mouth.</li>
<li>If it still didn't take off, well, chances are you weren't solving a huge real problem, or enough people have already solved it. In that case, just be grateful for whatever success that comes your way over the long haul.</li>
</ol>
<h3 id="heading-resources-2">Resources</h3>
<ul>
<li><a target="_blank" href="https://stripe.com/en-my/atlas/guides/starting-sales">https://stripe.com/en-my/atlas/guides/starting-sales</a></li>
<li><a target="_blank" href="https://github.com/LisaDziuba/Marketing-for-Engineers">https://www.coryzue.com/writing/seo-for-developers/</a></li>
</ul>
<h2 id="heading-well-being">Well-being</h2>
<p>It's easy to sit and get lost in our contemporary work. And we do that by accumulating debts from the future. One of the debts is our personal <strong>health</strong>.</p>
<p>Here is how I try to stay on top of my health debt:</p>
<ul>
<li><strong>Install</strong> <a target="_blank" href="https://workrave.org/"><strong>Workrave</strong></a>. You can set it to lock your screen after an interval has passed. Most importantly, it can show some exercises that you can perform behind your computer!</li>
<li>Get an adjustable <strong>standing desk</strong> if you can afford it. I got mine from IKEA.</li>
<li>Do <a target="_blank" href="https://www.youtube.com/watch?v=Kjhl-8yU6hI"><strong>burpees</strong></a>. Stretch those joints. Maintain good posture. <a target="_blank" href="https://www.youtube.com/watch?v=59MaNHq8UDo">Planking</a> helps.</li>
<li><strong>Meditate</strong> to stay sane. I'm using <a target="_blank" href="https://meditofoundation.org/">Medito</a>.</li>
</ul>
<p>??</p>
<p>Thanks for reading. Be sure to check out my own SaaS tool <strong>Sametable</strong> to <a target="_blank" href="https://www.sametable.app">manage your work in spreadsheets</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How I got my first 10 customers for my side-project and what I’ve learned from them ]]>
                </title>
                <description>
                    <![CDATA[ By Tigran Hakobyan My name is Tigran, I’m 29, and I’m the creator of Cronhub. It’s a side-project, because I’m also a full-time remote engineer at Buffer. For the past 5 months, I’ve been building Cronhub into a profitable side-business. Cronhub is a... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-i-acquired-my-first-10-customers-for-my-side-project-c4ee892a70a2/</link>
                <guid isPermaLink="false">66c34d3293db2451bd441483</guid>
                
                    <category>
                        <![CDATA[ Entrepreneurship ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 27 Aug 2018 15:24:54 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*UFLMGUj8KK_Dfx93vSE_JA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Tigran Hakobyan</p>
<p>My name is <a target="_blank" href="http://www.tigranhakobyan.com/">Tigran</a>, I’m 29, and I’m the creator of <a target="_blank" href="http://www.cronhub.io/">Cronhub</a>. It’s a side-project, because I’m also a full-time remote engineer at <a target="_blank" href="http://www.buffer.com/">Buffer</a>.</p>
<p>For the past 5 months, I’ve been building Cronhub into a profitable side-business. Cronhub is a cron monitoring tool for developers. It monitors and alerts you if any of your scheduled jobs fail or run longer than expected.</p>
<p>In the past, I’ve shared an <a target="_blank" href="https://www.indiehackers.com/@tigran/how-i-shipped-my-first-saas-side-project-while-working-full-time-42862e847b">article</a> on how I launched Cronhub while working full-time. Today I’m writing about a topic that I had been wondering about for a long time after the launch. Finding the first customers is probably the hardest challenge for every product maker in early stages. This explains why so many people are curious how others managed to find their first customers and read their stories.</p>
<p>In this article, I want to share my story and experience of acquiring my first paid customers after launching my side-project. It’s been a fun and interesting journey full of joy and challenges. I hope I can shed some light on this process and inspire other developers to find their own path by reading my story.</p>
<h3 id="heading-working-on-cronhub">Working on Cronhub</h3>
<p>Having a full-time job and trying to build an online business on the side is challenging. The biggest challenge is being very organized with your time and not burning yourself out. Apart from my full-time job at Buffer I usually work on Cronhub 1–2 hours on weekdays and maybe 5–6 hours on the weekends.</p>
<p>I try to get 8-hours sleep each day otherwise I feel tired the next day which really hurts my productivity. Of course, there are some days when I feel down but I know it’s just temporary and I have to wait for the fog to lift. Even though Cronhub is a side-business I pay a lot of attention to prioritize the tasks. I group all my tasks into two high-level buckets.</p>
<ol>
<li>Tasks that will improve the product and make customers happy (product work)</li>
<li>Tasks that will bring more visitors by increasing the awareness of the site (marketing work)</li>
</ol>
<p>There is also the other aspect that improves the activation of the product like creating and activating a Cronhub monitor but I keep it in the product bucket just for the sake of simplicity. The challenge here is finding the sweet spot where you balance the two buckets. Being an engineer I’m naturally more inclined to prioritize more product work but I’m deliberately practicing to overcome this bias.</p>
<h3 id="heading-current-numbers">Current numbers</h3>
<p>Currently, I have 7–10 sign-ups daily. Most of the visitors come from my past blog post articles that are not necessarily my target audience. However, content marketing has been the only marketing channel that I’ve used with Cronhub. No cold emails or ads. Here are some metrics that I thought could be interesting to share.</p>
<h4 id="heading-product-metrics">Product Metrics</h4>
<ul>
<li>Cronhub has around ~1100 signed up users</li>
<li>450 active monitors are making around 130,000 pings daily. A monitor is active if it has received at least one ping from an external job or script (e.g. your daily database backup job, weekly digest email job)</li>
<li>Only in the last 3 weeks, Cronhub reported around 3000 cron job failures to our users. The failure can be either that job failed to run on schedule or ran longer than expected.</li>
</ul>
<h4 id="heading-financial-metrics">Financial Metrics</h4>
<ul>
<li>Net Revenue since <a target="_blank" href="https://www.producthunt.com/posts/cronhub">the launch day</a> is $710 (actual total gross revenue minus any fees, refunds, disputes)</li>
<li>Trial conversion rate 83% (I expect this number will go down eventually)</li>
<li>Monthly expenses, $57</li>
</ul>
<p>My expenses haven’t changed since the launch day and I plan to keep it as low as possible. Something I didn’t expect at all is seeing almost half of my customers choosing the yearly billing plan over monthly. I don’t know if I should make some conclusions here. I may if I see this pattern repeats when I gain more customers and data points.</p>
<p>One thing I’m very proud of but still plan to improve is Cronhub’s SEO score. It’s really cool to see a big chunk of my visitors coming from organic search especially for an early product like Cronhub. I think it’s a great validation for me to continue focusing on SEO and improving the organic growth.</p>
<p>Apart from the basic SEO techniques (like keywords, fast page load, etc) and writing content on <a target="_blank" href="https://blog.cronhub.io/">Cronhub’s blog</a> I haven’t done anything else. I’ll talk more about this a bit later.</p>
<h3 id="heading-acquiring-my-first-customers">Acquiring my first customers</h3>
<p>When I launched Cronhub, I could only dream about having 10 customers. Now when I’m here it seems like I’ve so much yet to cover. Getting my first paid customer was crucial for the product. I spent a couple of months building the MVP (minimal viable product) and I wanted to see the returns of my hard work. Also, I think having a product that people pay for is a great way to validate your idea and know that you’re onto something.</p>
<p>I was very lucky to get my first ever customer signing up for the “<a target="_blank" href="https://cronhub.io/pricing">Developer ($7)</a>” plan the next day after the launch. At that time I only had one paid plan on Cronhub. Getting the Stripe notification of having a new customer was something special. It immediately boosted my confidence and pushed my motivation to the roof.</p>
<p>Later on, this customer switched to the business plan because he and his team needed more monitors. However, when he emailed me the “Business” plan was still in the “Coming soon” phase. I told him that I’d upgrade him within the next couple of days and I did. I made it my top priority. I had him on the business plan the next week.</p>
<p>For the business plan, I had to build team member support from scratch. It was worth it, because I knew I had to support teams on Cronhub anyway. Cronhub is primarily built for developer teams, so team invitation and management support was a no-brainer. It was just a matter of time. I manually upgraded the customer and offered a lifetime discount on the business plan.</p>
<p>I’m always in touch with my first customer. He has been very helpful with providing valuable feedback. My first customer came from the Product Hunt launch. It took me 2 weeks to get two more customers, and both came within the same week.</p>
<p>After the launch, the number of visitors went down, so I had to think about ways to promote Cronhub. I really didn’t think for too long here and decided to do what I enjoy the most: write.</p>
<p>I really love writing. I think of writing as a way of meditating. It helps me to stay focused on one thing which is not always possible with my monkey brain. I knew I could write about my experience of building Cronhub as an online business which I’ve been doing ever since.</p>
<p>Writing helped me to grow my audience as well as market Cronhub. After starting to write I started to grow my Twitter following as well. Below is the graph that illustrates the evolution of my Twitter followers after I started blogging regularly. I think you can see the breaking point!</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*oLU77vfrTou_4VHQ.png" alt="Image" width="800" height="628" loading="lazy">
<em>I started writing regularly in March and you can see how my audience grew ever since</em></p>
<p>When I decided to make content my primary marketing channel, I started a blog with a new subdomain <a target="_blank" href="https://blog.cronhub.io/">blog.cronhub.io</a>. With the new blog, my intention was to squeeze out all the SEO benefits. Since I wanted to improve the SEO score to get more organic visitors, I dedicated a week or so to making Cronhub more SEO-optimized.</p>
<p>There are many resources covering the basics which you can find on the web. One example of how my site optimization and content writing helped on the SEO side is this screenshot from Google Analytics. It shows that almost 40% of my traffic on August 23–24 came from organic search. This percentage fluctuates between 20% — 40% every day. I think it’s great, and I have to keep the spirit high. It would be great to know what the industry average is for SaaS products.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*1qNC8wEimz_drcna.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Organic Search was the top acquisition channel for these days</em></p>
<p>I publish all my articles on Cronhub’s blog first and then I republish them on Medium and <a target="_blank" href="https://www.indiehackers.com/">Indie Hackers</a>. It’s great that Indie Hackers allows setting a canonical URL for your articles which helps with SEO. One thing I do with my Medium articles is trying to get them published on popular publications such as <a target="_blank" href="https://medium.freecodecamp.org/">freeCodeCamp</a>. It’s a great exposure and opportunity to get your voice heard by a large audience. I highly recommend that you give it a try. Here is a quick gif showing my post stats.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*PUT3oDou0CJQ7wUn.gif" alt="Image" width="600" height="400" loading="lazy">
<em>My medium states started to grow including the followers count after getting published on FreeCodeCamp</em></p>
<p>One downside of content marketing is that its very time consuming but it’s a long-term game. It’s like an investment you make now for a better future.</p>
<p>My hunch is that most of my current articles are read by many developers. The question is whether those developers are likely to show interest in cron jobs or not.</p>
<p>As an example, If I had to write an article about <a target="_blank" href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/">Kubernetes Cron Jobs</a> and how they work, I’d expect that most of my readers know at least what cron jobs are. I want to narrow down the scope of my articles and target to more relevant developers. What I care about most is not the number of visitors but the type of visitors. I have a couple of ideas and am excited to see how they perform in the future.</p>
<p>After having three paying customers within the first month, I was very pumped when I got my first yearly “Developer” plan customer in early May. It was around the time when I started to read and learn more about product pricing. I knew nothing about pricing but I was keen to learn.</p>
<p>I talked to product managers and people who understood SaaS pricing model better than I did. (btw I wrote <a target="_blank" href="https://blog.cronhub.io/how-i-priced-my-side-project/">an entire article</a> where I share all my learnings on pricing). Then, I changed my pricing table and added a new intermediate plan called “Startup”. It was in-between “Developer ($7)” and “Business ($49)” plans because I thought the pricing gap between these two plans was too high.</p>
<p>I changed the pricing, and to my big surprise my next customer signed up for the yearly “Startup” plan. It felt really good. I felt like that soccer manager who brings a substitute player to the field and that player scores a goal. Then, I started to slowly acquire new customers until I got to the first sweet spot, 10 paying customers.</p>
<p>However, looking back it did take me almost 5 months until I got my first 10 customers. This is me being a solo founder working around 10–15 hours per week.</p>
<p>It’s been a slow journey and, of course, there are times when you don’t have a single new customer in the span of multiple weeks and you feel terrible with lack of motivation. But I know these things will pass and I’m excited more than ever to get to 100 customers.</p>
<p>If I reflect on my past journey I can certainly say that there is no universal formula that one can use to succeed in this process. However, I believe that patience is the biggest player. You have to believe in yourself and be patient.</p>
<p><strong>If I could make a list of my main learnings, it would look like this;</strong></p>
<ul>
<li>Focus on the core product and user experience in the beginning.</li>
<li>Do not spend too much time thinking about your pricing in your early days. Think of pricing as a feature that you can always iterate on. Start from the MVP.</li>
<li>Customer support is very important. Be a human, not a company. Be in touch with your customers and ask for feedback.</li>
<li>Decide what your primary marketing channel is and focus on it in the early days.</li>
<li>Share all the cool things you’re working on with your audience. People are genuinely interested in them.</li>
<li>Ask for help and advice. People are generally nice and want you to succeed.</li>
</ul>
<h3 id="heading-whats-next">What’s next</h3>
<p>My next goal is to go from 10 to 100 customers. Today I asked for advice on Twitter and Joel Gascoigne replied with a great one.</p>
<p>I want to talk more to my customers and adjust my marketing strategy around the value that Cronhub provides to them. Apart from content marketing, I want to create more acquisition channels that have a higher activation rate. Finding a product market fit is my eventual goal. I hope I will get there soon.</p>
<p>As a side note, I’m very excited to share that Cronhub got accepted into YC’s <a target="_blank" href="https://www.startupschool.org/">Startup School</a> Advisor track. It’s a 10-week long online course where you get exposed to a mentor and a great community. I’m excited to apply all my learnings on Cronhub and share my experience with you! Stay tuned for a new blog article.</p>
<p>Thanks for reading and let me know in the comments if you have questions for me. I’m happy to share more.</p>
<p>If you’re a developer who is using cron jobs or any scheduled tasks and need monitoring, then I’d love if you could try <a target="_blank" href="https://cronhub.io/">Cronhub</a>. It will mean a lot to me to know what you think. Thanks.</p>
<p>_I also want to thank my wife <a target="_blank" href="https://www.instagram.com/_nyut/">Ani</a> for helping me to edit this article. ❤️_</p>
<p><em>Originally published at <a target="_blank" href="https://blog.cronhub.io/how-i-acquired-my-first-10-customers-for-cronhub/">blog.cronhub.io</a> on August 27, 2018.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to price your side-project ]]>
                </title>
                <description>
                    <![CDATA[ By Tigran Hakobyan Introduction If you had asked me a couple months ago if I could ever write an article about pricing, it probably would have made me laugh. Until a few months agao, I didn’t know anything about product pricing even though I’d launch... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-price-your-side-project-f4e0f86dbfde/</link>
                <guid isPermaLink="false">66c353f4b3da455a9c10dc21</guid>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ side project ]]>
                    </category>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Tue, 29 May 2018 21:44:22 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*GvFlbBgvsCc4ofc3Wg2uPQ.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Tigran Hakobyan</p>
<h4 id="heading-introduction">Introduction</h4>
<p>If you had asked me a couple months ago if I could ever write an article about pricing, it probably would have made me laugh.</p>
<p>Until a few months agao, I didn’t know anything about product pricing even though <a target="_blank" href="https://blog.cronhub.io/my-first-saas-project/">I’d launched my first SaaS project</a>. The way I priced my product was by solely relying on my own intuition, as well as looking at how other similar SaaS products priced their services. I think this is what most beginners do.</p>
<p>Then why on earth write an article if I’m not an expert on pricing? Because I’ve come to realize that it’s totally okay to write about topics you’re not expert on.</p>
<p>Chris Coyier’s <a target="_blank" href="https://twitter.com/chriscoyier/status/925081793576837120?lang=en">tweet</a> inspired me to start writing about things I wish I could find on Google.</p>
<p>I started to use blogging as an excuse to learn a specific topic. This article is the fruit of that inspiration, and I’m happy to share it with you all.</p>
<p>There aren’t many articles on the pricing of side-projects, even though the number of revenue-generating side-projects is growing. If you go to <a target="_blank" href="http://www.indiehackers.com/">Indie Hackers</a> you can see many of them there, and many makers in the community wonder and ask questions about pricing as well.</p>
<p>In this article, I’ve summarized everything I’ve learned about side-project pricing. For the last two months or so, I’ve been reading about pricing and talking to people who are knowledgeable about pricing. It really helped me to understand the basics and create a more successful pricing model for an SaaS project.</p>
<p>I’ve used <a target="_blank" href="http://www.cronhub.io/">Cronhub</a> (my side-project) as an example to better explain my view and thinking process, because I believe it’s easier to digest new information using an example.</p>
<p>All my learnings here apply to SaaS products, but I think other types of businesses can benefit from them, too.</p>
<h3 id="heading-why-monetizing">Why monetizing?</h3>
<p>Monetizing your project from day one is very important. You can think of the “monetization” as a feature that you want your product to have. This feature significantly increases your product’s chances of success. Usually, you build features for your customers but this one is for you, to keep you more motivated to work on your project.</p>
<p>Your motivation and persistence are the key drivers to move your project forward. If you value something, it keeps your motivation high. What’s the easiest way to know whether your project is valuable? Asking people to pay for it. In my opinion, if you can find 10 people willing to pay for your project, you know you’ve got something that people are ready to pay for. After that, you can find the next 100, 1000, and even more. Finding the first customers is the hardest part.</p>
<p>Because finding the first customers is hard, it’s important to set the right pricing model in the beginning. Pricing is a living feature, and as any other feature, it requires multiple iterations until you get it right. It’s totally okay to make mistakes in the beginning. In fact, for Cronhub, I’m still not sure whether I’ve got it right or not. ?</p>
<p>However, I’m open to changing it if that’s what’s best, and that’s what matters.</p>
<p>If your intention is to build a business from your side-project, then you should charge for your project. It’s hard and more often demotivating to work for free. You value your time and others should, too.</p>
<h3 id="heading-pricing-strategies">Pricing strategies</h3>
<p>Since I’m not an expert in this field, I can’t give you very technical answers to pricing strategies. However, one thing I can do is to explain these strategies in a more human way with simple words.</p>
<p>Maybe that’s even better for you? ? Good. Let’s go for it.</p>
<p>The primary goal of a pricing strategy is to maximize your product’s revenue. According to a book that I’ve read, “<a target="_blank" href="https://www.priceintelligently.com/developing-your-saas-pricing-strategy">The anatomy of SaaS pricing strategy</a>” (which a great book btw), there are three pricing strategies for SaaS products.</p>
<h4 id="heading-cost-plus-pricing"><strong>Cost-Plus Pricing</strong></h4>
<p>This is the most basic pricing model you can think of, because it really makes sense. First, you calculate everything that costs money for your project and then add a healthy margin on top of it. The margin is your profit, and it represents the value that you give to your customers.</p>
<p>Let’s take Cronhub as an example. Cronhub is a cron job (or any scheduled task) monitoring tool. Monitors cost me money, because they send emails and SMSs to my users. Sending emails and SMSs is costly for my business. Having more monitors will increase my cost, and that’s why I priced Cronhub by the number of monitors. If you want more monitors, you should pay more.</p>
<p>I think most first-time side-project builders take this approach, because it’s simple and it covers the costs. The challenge with this pricing strategy is the unpredictability of the future. If your costs unexpectedly go up in the future your profit margin will drop and you will have to increase your prices.</p>
<h4 id="heading-competitor-based-pricing"><strong>Competitor-Based Pricing</strong></h4>
<p>As the name implies, this pricing strategy is influenced by your competitors. When you don’t know the initial value of your product, you usually turn to your competitors. You check their pricing tables so you can come up with yours. Well, I’ve done it for Cronhub, too.</p>
<p>If you’re launching a product in a new industry, it makes sense to check competitor pricing because you don’t want to go too high or too low. We’re always afraid of losing customers because of the big price difference from our competitors.</p>
<p>This pricing strategy is simple and less prone to be wrong. You can probably spend an hour or so researching competitors and come up with a pricing table that is similar to your competitors. This way your future customers won’t think that your product is too expensive or too cheap.</p>
<p>The one downside I see for this model is that you don’t want to be guided by your competitors from day one. You want your product to have its own personality, and it should be reflected in the pricing as well. Use competitor pricing as an inspiration and benchmark, but not your guiding strategy. For me, I see this strategy mostly used in combination with other pricing models.</p>
<h4 id="heading-value-based-pricing"><strong>Value-based pricing</strong></h4>
<p>This strategy is also called customer-based strategy and is solely based on your customer surveys and research. You want to know how much your customers are willing to pay for your product, and the only way to do so is to go and talk to them. Most companies use in-product surveys to collect this data.</p>
<p>There are many other benefits with this strategy, too. You get to know your customers and their needs, which helps you to build the best product. I think that by following this strategy, you’re most likely to come up with a better pricing table.</p>
<p>However, this model can only be applied after some initial iterations when you have a decent customer base. That’s why I believe that this model should be used as part of the last pricing iterations on your pricing table. It doesn’t mean that you should not talk to your customers from early days. Just set a goal for yourself that you eventually want to use this strategy to decide your pricing table.</p>
<p>You may want to re-apply this strategy often, because your customer base and the market needs keep changing.</p>
<h3 id="heading-break-down-your-customers-into-groups">Break down your customers into groups</h3>
<p>I strongly believe that breaking down your user base into groups is not only important for building a better product, but also for modeling your pricing. When you start thinking about pricing, you naturally measure it with only one dimension using just a single variable (like I used only the monitor count as a pricing measure for Cronhub).</p>
<p>However, you want to add another dimension based on your customer groups. There is a term called “Pricing Axes” which I first heard from <a target="_blank" href="https://twitter.com/joelgascoigne">Joel Gascoigne</a> in <a target="_blank" href="http://www.buffer.com/">our company</a> retreat this year in Singapore. These axes represent variables that you can use to better model your pricing. For instance, Cronhub has two pricing axes now after launching the team plan: the number of monitors and the team member count.</p>
<p>Breaking down my customers into groups helped me come up with the second axis for Cronhub. I have divided my potential customers into two groups, solo developers, and developer teams. It naturally makes sense that there should be different pricing for each of these groups, because their needs are different. Following this strategy, I’ve created two pricing plans for the team depending on the number of team members they need.</p>
<p>Of course, you can add more axes along the way, and it depends on your product. I think keeping your pricing axes around 2 - 3 makes sense at least in the beginning. Adding more variables may confuse your users, and you don’t want that.</p>
<h3 id="heading-applying-the-strategies-to-cronhub">Applying the strategies to Cronhub</h3>
<p>I want to also talk about how I’ve applied the above-mentioned pricing strategies for Cronhub and the pricing challenges I’ve faced when starting Cronhub.</p>
<p>Cronhub is a product for developers. I knew that the developer market is not an easy one to be in, but it didn’t stop me starting something I’m very passionate about. I really enjoy hanging out with developers and getting to know them better. For me, what mattered most was to launch a product that would serve this market.</p>
<p>Being a developer, I know how much developers love to use free tools. However, they’re also willing to pay for a product that provides significant value to their flow and productivity. I want Cronhub to be one of these tools. I launched Cronhub with only two plans, “Free” and “Developer ($7/month)”. I wanted to see whether this was something other developers would pay for or not.</p>
<p>My very first pricing model was mostly a symbolic strategy to validate my idea. I didn’t spend much time thinking about the pricing plan and followed Cost-Plus pricing strategy to use the monitor count as a divider between the free and the paid plan. In the first month, I got a couple of paid customers which made me revisit my pricing. That’s when I’ decided to invest a little bit of time to learn about this topic.</p>
<p>After two months of reading and seeking advice from product people, I’ve come up with these key learnings that I’ve applied to Cronhub.</p>
<h4 id="heading-dont-give-up-too-many-things-in-the-free-plan"><strong>Don’t give up too many things in the free plan</strong></h4>
<p>We tend to give up too much in the free plan, especially in the beginning. It’s quite possible that your product doesn’t offer any free plan. But for Cronhub, I wanted to use this channel as a way to acquire customers. Limit the free plan to the minimum.</p>
<h4 id="heading-know-your-customer-groups"><strong>Know your customer groups</strong></h4>
<p>Knowing your customers is important, because it helps you understand how much they can afford to pay for your product. Cronhub’s free and developer plans are intended only for solo developers who have projects on the side.</p>
<p>I now also have two team plans: “Startup $19/month” and “Business $49/month,” and they are intended for engineering teams. Engineering teams should collaborate together, so the team support is necessary. Team sizes are different and that’s why I’ve priced these plans by the number of team members.</p>
<h4 id="heading-think-about-your-conversion-flow"><strong>Think about your conversion flow</strong></h4>
<p>Picturing the customer conversion flow in your head is really helpful in understanding how pricing may work, and that’s why I’ve decided to offer a free plan.</p>
<p>I imagine the flow like this: a developer signs up for the free plan to use it for their personal projects. If they like my product very much, the chances are high that they will also promote my product within their company. It’s the word-of-mouth effect. Now someone from their company signs up to try the team plan, and there you have your potential lead who is very likely to become a customer.</p>
<h4 id="heading-your-pricing-is-probably-too-cheap"><strong>Your pricing is probably too cheap</strong></h4>
<p>Don’t be afraid to raise your prices. We tend to undervalue our product, because we are afraid that no one will pay if it’s expensive. This is true especially for people who don’t have experience with pricing.</p>
<p>Unfortunately, pricing your project very cheap may discredit the value your product provides. It can make people question the quality of your product. That’s why you want to find the <strong>sweet spot</strong> where it’s not cheap and also not expensive.</p>
<h4 id="heading-pricing-will-affect-your-long-term-productivity"><strong>Pricing will affect your long-term productivity</strong></h4>
<p>If you’re a single person team, then you probably do the product support as well. Higher paying customers tend to create fewer support requests compared to the customers who have signed up for the lowest pricing plan.</p>
<p>Think about your time and how you want to spend it. If you’re planning to stay a one-person team, then this will make a huge difference.</p>
<p>For Cronhub, eventually, I’m thinking I’ll only have two plans: a free plan and a business plan. The free plan will be for developers and the business plan for small and medium-sized engineering teams. Instead of having many low paying customers (and spending many hours answering support questions), I would prefer having a small number of high paying customers (and enjoy building a product).</p>
<p>Thank you very much for your time. I hope you’ve enjoyed reading this article and learned a little bit about side-project pricing. Writing is what I enjoy doing most after programming, and I hope to write more articles in the future. If you don’t want to miss any of my writings, <a target="_blank" href="http://www.twitter.com/@tiggreen">follow me on Twitter.</a> Feel free to send me a message or ask questions below!</p>
<p>Finally, If you’re a developer or part of a developer team that uses cron jobs, you can try <a target="_blank" href="http://www.cronhub.io/">Cronhub</a> for free to monitor all your cron jobs in a beautiful dashboard.</p>
<p><em>Originally published at <a target="_blank" href="https://blog.cronhub.io/how-i-priced-my-side-project/">blog.cronhub.io</a> on May 27, 2018.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to create a WordPress plugin for your web app ]]>
                </title>
                <description>
                    <![CDATA[ By Feedier by Alkalab Today, we are going to see how to create a very simple WordPress plugin for any web app that needs to insert a piece of code to your site. _Credits: [https://unsplash.com/photos/I8OhOu-wLO4](https://unsplash.com/photos/I8OhOu-w... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-create-a-wordpress-plugin-for-your-web-app-5c31733f3a9d/</link>
                <guid isPermaLink="false">66c3511d9972b7c5c7624eda</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ WordPress ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 28 May 2018 21:58:04 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*wXmt_4PB07yn2zG5zgNOSg.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Feedier by Alkalab</p>
<p>Today, we are going to see how to create a very simple WordPress plugin for any web app that needs to insert a piece of code to your site.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*wXmt_4PB07yn2zG5zgNOSg.jpeg" alt="Image" width="600" height="400" loading="lazy">
_Credits: [https://unsplash.com/photos/I8OhOu-wLO4](https://unsplash.com/photos/I8OhOu-wLO4" rel="noopener" target="<em>blank" title=")</em></p>
<p>To follow this tutorial, you need some knowledge of these basics:</p>
<ul>
<li><strong>PHP</strong> and OOP</li>
<li><strong>JavaScript</strong> (we’ll use jQuery and Ajax)</li>
<li><strong>WordPress development</strong> (as most functions are from the WordPress core).</li>
</ul>
<p>You can find a working result of this tutorial on <a target="_blank" href="http://pxlme.me/611bFPFB">this Github repository</a>.</p>
<p>These web apps could be anything, like <a target="_blank" href="http://crazyegg.com/">CrazyEgg</a>, <a target="_blank" href="https://freshdesk.com/">Freshbook</a>, <a target="_blank" href="https://analytics.google.com/analytics/web/">Google Analytics</a>, <a target="_blank" href="https://www.facebook.com/business/a/facebook-pixel">Facebook Pixel</a>, or <a target="_blank" href="https://feedier.com/">Feedier</a>. Why? They all need to inject some HTML / JavaScript code to your site for various purposes.</p>
<p>This “code” is always parametrized with variables, and is usually a pain for the site owner. This is because you need to edit the theme’s templates. So, how about we create a plugin to do that for us? Okay, let’s do it!</p>
<h3 id="heading-step-1-find-your-web-app">Step 1: Find your web app</h3>
<p>The goal of this tutorial is to create a WordPress plugin that adds a WordPress admin page. Plus, we’ll also add some settings to configure the app’s in-site widget and inject the HTML / JS code in our web page automatically. Nothing fancy, just something that works fine.</p>
<p><strong>Please note: we do need a web application for this tutorial.</strong> We will use <a target="_blank" href="https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app">Feedier</a> for this example. However, if you have another web application that you’d like to use in this tutorial, please do. Just rename anything named “feedier” with your app’s name and adapt the settings to what that app needs. Most of them will give you a snippet to add to your site in order to make it work.</p>
<p>Here’s a quick briefing of <a target="_blank" href="https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app">Feedier</a> if you’ve never heard of it:</p>
<ul>
<li>It’s a feedback collector tool, using surveys to understand your users</li>
<li>It’s very flexible</li>
<li><em>It’s free!</em></li>
<li><strong>Has a good API</strong> (very important here)</li>
<li><strong>Has an in-site widget</strong> (very important here)</li>
<li>Lets you reward your customers</li>
<li>Lets you create conditional questions</li>
<li>Has a complete analytic report dashboard</li>
<li>Lets you manage feedback individually</li>
</ul>
<p>Here is the widget we want to add automatically:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*c2GG9QpqM6aMth9s.jpg" alt="Image" width="800" height="446" loading="lazy">
<em>Preview of the widget on woffice.io</em></p>
<p>If you signed up for Feedier, then you can simply find the code in the Share tab of your survey:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*CIKS52RyV3b0DYy9.jpg" alt="Image" width="800" height="446" loading="lazy">
_Grab the snippet from [feedier.com](https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app" rel="noopener" target="<em>blank" title=")</em></p>
<h3 id="heading-step-2-setup-our-plugin-and-its-architecture">Step 2: Setup our plugin and its architecture</h3>
<p>WordPress plugin are by design very simple. Our plugin will only need two files.</p>
<ul>
<li><strong>feedier.php</strong>: main plugin’s PHP file.</li>
<li><strong>assets/js/admin.js</strong>: JavaScript script to save the options using Ajax.</li>
</ul>
<p>You can create a new “feedier” directory (or name of your web app) in your <strong>wp-content/plugins/</strong> folder.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*_n_Hxi7MHCqjzoO9.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The most important file will be the plugin’s <strong>feedier.php</strong> class. Here is its structure:</p>
<p>We are doing a few things here:</p>
<ul>
<li>Declaring our plugin using the header comments</li>
<li>Defining a few handy constants to be able to find the plugin’s URL and PATH easily</li>
<li>Declaring our plugin class that will contain everything we need in this plugin. We just need a constructor method for now.</li>
</ul>
<p>You should already see the plugin in your Plugins page, even though it’s not doing anything yet:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*9zBfprPe_aLld2TY.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>Activate the plugin from WordPress admin panel</em></p>
<h3 id="heading-step-3-create-our-admin-page">Step 3: Create our admin page</h3>
<p>For this part, we will add a new Feedier admin page to our WordPress site and dynamically fetch our surveys from Feedier’s API.</p>
<p>In our class’ constructor, let’s register three new actions which are required to add an admin page on WordPress:</p>
<ul>
<li><strong>addAdminMenu</strong> will add a new page in the WordPress left menu. There will be also a callback to another method containing the page’s content.</li>
<li><strong>storeAdminData</strong> will be called whenever the user clicks the “Save settings” button.</li>
<li><strong>addAdminScripts</strong> will register a new JavaScript file to our WordPress admin in order to save the form’s data. But it also exchanges some variables between the PHP side and JavaScript side.</li>
</ul>
<p>The first step is very easy. We just register the page, like this:</p>
<p>As you can see, we use <a target="_blank" href="https://codex.wordpress.org/I18n_for_WordPress_Developers">WordPress localization functions</a> for <em>all</em> strings. Note that the</p>
<pre><code>array($this, ‘adminLayout’)
</code></pre><p>is where we call another method containing the page’s content. The form needs to be adapted to your web app.</p>
<p>Here, we first need to get the public and private Feedier API keys. Once saved, we are going to use the private key to dynamically retrieve our surveys. Whenever we get the surveys and not an API error, we display some new options to configure the widget.</p>
<p>At the beginning of this method, you can see that we are first getting the saved data with:</p>
<pre><code>$data = $this-&gt;getData();
</code></pre><p>And getting the surveys from the Feedier API:</p>
<pre><code>$surveys = $this-&gt;getSurveys($data[‘private_key’]);
</code></pre><p>So let’s declare the first one:</p>
<p>This function just reads our plugin’s option and gives us an array back so we can save multiple values in the same option.</p>
<p>To get the second method working, we need the Feedier private key. This depends on the first one to access this key saved in the option:</p>
<p>The Feedier API is documented <a target="_blank" href="https://feedier.docs.apiary.io/#reference/0/carrier-collection/get-a-list-of-carriers">here</a>, so you can see what you will get in the response.</p>
<p>At this moment, we have a complete new Admin page. But nothing happens when we click on the save button, because there is no saving mechanism — <em>yet</em>.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*B9v_zAYSu7-4pa-u.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Good enough, let’s save our data!</p>
<p>As mentioned before, we will save our data using AJAX. Therefore, we need to register a new JavaScript file and exchange data using the <a target="_blank" href="https://codex.wordpress.org/Function_Reference/wp_localize_script">wp_localize_script()</a> function:</p>
<p>We also need to add a new file <strong>/assets/js/admin.js</strong>. That will simply make an Ajax call, and WordPress will automatically route the request correctly to the right method (already done in the constructor). You can read more about how WordPress smartly handles AJAX requests <a target="_blank" href="https://codex.wordpress.org/AJAX_in_Plugins">here</a>.</p>
<p>At this very moment, we can click the save button and the above script will make an HTTP POST request to WordPress. We also append an action parameter containing: <strong>store_admin_data</strong> (which we declared at the beginning at this part in the constructor):</p>
<pre><code>add_action( ‘wp_ajax_store_admin_data’, array( $this, ‘storeAdminData’ ) );
</code></pre><p>The method <strong>storeAdminData</strong> will receive the POST request and save the values we need in our WordPress option.</p>
<p>A few notes on the above method:</p>
<ul>
<li>We use a “WordPress nonce” to handle the security and make sure this is coming from the website and not a hacker faking the request.</li>
<li>We identify the fields we need to save using a “feedier_” prefix. Once received, we loop through all the $_POST data and only save those fields. We also remove the prefix before saving every field.</li>
</ul>
<p>That’s it for the saving process. When we click save, we can see a POST request and our data being saved on the database within the <strong>wp_options</strong> table.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/0*uTVEcizHs2jERzSM.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Perfect, we are done with the admin page.</p>
<h3 id="heading-step-4-insert-the-dynamic-code-automatically-into-our-pages">Step 4: Insert the dynamic code automatically into our pages</h3>
<p>Now that we have our options saved, we can create a dynamic widget that will depend on the options set by the user though our admin page. We already know what the web app expects from us.</p>
<p>Something like:</p>
<pre><code>&lt;div <span class="hljs-class"><span class="hljs-keyword">class</span></span>=”feedier-widget” data-type=”engager” data-position=”right” data-carrier-id=”x” data-key=”xxxxxxxxxxxxxxxxx”&gt;&lt;/div&gt;
</code></pre><pre><code>&lt;! — Include <span class="hljs-built_in">this</span> line only one time, also <span class="hljs-keyword">if</span> you have multiple widgets on the current page →
</code></pre><pre><code>&lt;script src=”https:<span class="hljs-comment">//feedier.com/js/widgets/widgets.min.js" type=”text/javascript” async&gt;&lt;/script&gt;</span>
</code></pre><p>Thus, the first thing we want to do is to create a new method to our plugin that will print this code depending on the variables set by the user. So, using the architecture we already set up in the last part:</p>
<p>Now, we just need to call this function on every page load to add it at the bottom of the page. To do this, we’ll hook our method to the <strong>wp_footer</strong> action. By registering a new action into our class’ constructor:</p>
<p>That’s it!</p>
<p>Any questions, feedback, or ideas? Let me know in the comments!</p>
<p>You can find a working version of this tutorial on <a target="_blank" href="http://pxlme.me/611bFPFB">this Github repository</a>.</p>
<p><a target="_blank" href="http://pxlme.me/611bFPFB"><strong>2Fwebd/feedier-wordpress-plugin</strong></a><br><a target="_blank" href="http://pxlme.me/611bFPFB">_Contribute to feedier-wordpress-plugin development by creating an account on GitHub._pxlme.me</a></p>
<p>Note that this is first version of the plugin, and many things can be improved. I’m open to suggestions and improvements. ?</p>
<p>We are building <a target="_blank" href="https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app">Feedier</a>. It becomes a no-brainer to collect feedback and build relationships with your customers!</p>
<p><a target="_blank" href="https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app"><strong>Feedier - Next generation feedback</strong></a><br><a target="_blank" href="https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app">_Meet Feedier, the next generation customer feedback software that lets you collect valuable feedback. Reward, engage…_feedier.com</a></p>
<p>Convinced? Sign up for <strong>free</strong> at <a target="_blank" href="https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app">feedier.com</a> ?</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*euK96ycNbjw9yVMXgflmLg.gif" alt="Image" width="600" height="400" loading="lazy">
_[Feedier.com](https://feedier.com/?utm_medium=article&amp;utm_source=medium&amp;utm_campaign=medium-wordpress-awareness-2018-05-21&amp;utm_content=how-to-create-a-wordpress-plugin-for-your-web-app" rel="noopener" target="<em>blank" title="), the next generation <strong>feedback</strong> application. <strong>Start for free now!</strong></em></p>
<p>Don’t forget to clap our article and <a target="_blank" href="https://alka-web.us16.list-manage.com/subscribe?u=cd5291c429df8270607277d16&amp;id=42520def8c">subscribe</a> to get more amazing articles if you liked it?. You can also find us on T<a target="_blank" href="http://pxlme.me/_dw36YLw">witter.</a></p>
<p><em>This article was initially published on our <a target="_blank" href="https://alkalab.com/blog/tutorial-wordpress-plugin-web-app/">blog here.</a></em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The 5 Stages of a SaaS Subscription ]]>
                </title>
                <description>
                    <![CDATA[ By Ben Sears This article will go into detail on what you need to automate in order for your SaaS company to have a functional subscription billing solution. One of the problems SaaS companies face when selling subscriptions is connecting their appli... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-5-stages-of-a-saas-subscription-5169307fd0c8/</link>
                <guid isPermaLink="false">66d45de47df3a1f32ee7f7ed</guid>
                
                    <category>
                        <![CDATA[ business ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Entrepreneurship ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ startup ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 09 Apr 2018 04:34:53 +0000</pubDate>
                <media:content url="https://cdn-media-2.freecodecamp.org/w1280/5f9caeab740569d1a4caa809.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Ben Sears</p>
<p>This article will go into detail on what you need to automate in order for your SaaS company to have a functional subscription billing solution.</p>
<p>One of the problems SaaS companies face when selling subscriptions is connecting their application to a billing process.</p>
<p>Some of the things that are need to be considered are:</p>
<ul>
<li>How will cancellations be handled</li>
<li>Free trials</li>
<li>Granting access to new customers</li>
</ul>
<p>The challenge in managing these billing processes is handling these events, such as restricting access to an application when a trial expires or if there no longer has a valid funding source attached to an account.</p>
<h3 id="heading-the-saas-subscription-lifecycle">The SaaS Subscription Lifecycle</h3>
<p><img src="https://cdn-media-1.freecodecamp.org/images/cF991ZGku1qgcPHY5FM8NHUwPlKdt82s6gHW" alt="Image" width="800" height="779" loading="lazy"></p>
<p>The process a customer goes through when doing business with a SaaS company can be broken down into the five events above. Managing these events is the key to integrating a billing system with a SaaS.</p>
<h4 id="heading-subscribe">Subscribe ?</h4>
<p>This is the first stage in the journey a user takes with a subscription. In this step, the customer has just signed up for a subscription which needs to trigger an automated process.</p>
<p>The process generally looks something like this:</p>
<ol>
<li>A customer orders a subscription for your application.</li>
<li>The customer is granted access to your application.</li>
<li>After the trial period is over (if there is a free trial), the customer is charged on a recurring basis.</li>
</ol>
<p>From a DevOps perspective, these are considered “<strong>Day 1</strong>” operations. These are the steps that a service goes through after being requested in order to be considered “provisioned,” such as installation and configurations of software.</p>
<h4 id="heading-trialing">Trialing ⏳</h4>
<p>In the trialing stage, a customer has subscribed to a service, but is not paying until their trial expires.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/F66cqbVeMQcPsYUx-joezF35PYJQhBlejYVC" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Approximately <a target="_blank" href="https://www.chargify.com/blog/increase-free-trial-conversions/">75% of SaaS companies</a> offer free trials. Although free trials are almost guaranteed to bring you more paying customers, one of the trickier things about offering them is deciding what happens when a trial expires without a customer adding a funding source.</p>
<p>At this stage of the service lifecycle, a company will need to build logic around trials which will, upon expiration, restrict access to an application and alert the customer that they need to pay.</p>
<h4 id="heading-upgrade">Upgrade ?</h4>
<p><img src="https://cdn-media-1.freecodecamp.org/images/FG7TZ3qnJI9DdlGresF5gspwvM4L8D-V3eNH" alt="Image" width="600" height="400" loading="lazy">
<em>Netflix offers different tiers</em></p>
<p>Many SaaS businesses support multiple tiers of service. If a customer pays a premium, they have access to additional features. This is considered a “<strong>Day 2</strong>” operation, actions which can be taken after a service has been provisioned which affect the end user.</p>
<p>Generally, it follows the pattern below:</p>
<ol>
<li>A customer submits a request to upgrade their subscription.</li>
<li>The customer’s subscription rate will be increased.</li>
<li>The customer will be granted access to new features within the application.</li>
</ol>
<p>While this usually takes the form of strict pricing tiers, sometimes customers pay “per user per month” or have “thresholds” which if passed will trigger higher rates.</p>
<h4 id="heading-cancellation">Cancellation ❌</h4>
<p>Inevitably, there will be cancellations of subscriptions, also called <a target="_blank" href="http://chaotic-flow.com/saas-metrics-faqs-what-is-churn/">churning</a>. The steps which occur in order to fulfill a cancellation go as follows:</p>
<ol>
<li>A customer requests a cancellation of a SaaS subscription.</li>
<li>They will no longer be charged on a recurring basis.</li>
<li>Access to the application will terminate at the end of the current billing cycle.</li>
</ol>
<p>Reaching out to your former customers after they have cancelled will also require some sort of process. It’s recommended that cancellation triggers a process which sends an automated email to the former customer, perhaps with an attempt to recover the customer or a feedback survey to see what reasons they may have had for canceling.</p>
<h4 id="heading-resubscribe">Resubscribe ↪️</h4>
<p>When a former customer decides to return after canceling, a company can’t just go through the original process to subscribe them as a new customer — they need to reactivate the previously terminated access so that they retain all their former data. This process can be described in three steps:</p>
<ol>
<li>A customer resubscribes by adding a valid funding source to their account.</li>
<li>Access to the customer account and data is reactivated.</li>
<li>The customer will be charged on a recurring basis once more.</li>
</ol>
<p>Some complex scenarios might include limited time discount codes for resubscribers, a free trial, or part of another service as part of a combo deal.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>The key to selling software-as-a-service is connecting software to a billing system that can support the lifecycle I just described. Being able to automate this process is a boon to businesses, since manual processes are one of the biggest barriers to scaling.</p>
<p>Trying to manage the challenges of SaaS billing? <a target="_blank" href="https://servicebot.io/contact">Let’s Talk</a>.</p>
<p>We solve challenges SaaS companies face when billing customers by providing easy to integrate hooks which can trigger automated processes.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to know if Kubernetes is right for your SaaS ]]>
                </title>
                <description>
                    <![CDATA[ By Ben Sears Kubernetes is an awesome technology, and I personally have seen great gains in my ability to scale, deploy, and manage my own SaaS because of it. But, not everyone would immediately benefit from adopting it for a number of reasons: Lack... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-know-if-kubernetes-is-right-for-your-saas-315dfffe0a25/</link>
                <guid isPermaLink="false">66d45ddc51f567b42d9f8443</guid>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SaaS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Sat, 03 Mar 2018 11:29:49 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*tJXXWOYkuMF3RkqZxMmvFw.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Ben Sears</p>
<p>Kubernetes is an awesome technology, and I personally have seen great gains in my ability to scale, deploy, and manage my own SaaS because of it. But, not everyone would immediately benefit from adopting it for a number of reasons:</p>
<ul>
<li>Lack of familiarity with container technology</li>
<li>Application architecture not being conducive to utilizing the benefits of Kubernetes</li>
<li>Increased amount of effort versus time spent</li>
</ul>
<p>If you are interested in Kubernetes but aren’t sure about investing the time/resources needed, this article is for you.</p>
<h3 id="heading-what-is-your-experience-with-containers">What is your experience with containers? ?</h3>
<p>In order to understand what Kubernetes can do for you, you first need to know what benefits containers provide. Before spending time on Kubernetes you should first:</p>
<h4 id="heading-containerize-your-application">Containerize your application</h4>
<p><img src="https://cdn-media-1.freecodecamp.org/images/AZhfT1f8jS0KnbLrYz7QXj1LYdXd9pADqp2r" alt="Image" width="600" height="400" loading="lazy"></p>
<p>First and foremost, your application must be containerized. This means defining the steps needed to take a base OS image and install your application on it in a file (usually a Dockerfile).</p>
<p>Going through this process as well as defining environment variables needed to configure your application (such as the URL, username, and password of the database your app uses) will be critical to making your container image usable by Kubernetes.</p>
<p>Also make note of any dependencies your application needs to function and learn how to use the containerized versions of those.</p>
<h4 id="heading-understand-how-storage-workshttpsdocsdockercomengineadminvolumes"><a target="_blank" href="https://docs.docker.com/engine/admin/volumes/">Understand how storage works</a></h4>
<p>Containers are designed to hold only the code needed to run an application. Any persistent data needs to be stored elsewhere, as the process of tearing down and spinning up containers (very common when dealing with containers) also destroys any data stored within the file system of that container.</p>
<p>Knowing how container storage is supposed to work and how to handle things like backing up data, moving that data between containers, and accessing the data from outside the container is very valuable when considering Kubernetes.</p>
<p>Kubernetes makes storage management easier with features such as auto-provisioning. This has the ability to have your storage provider (such as AWS EBS) create new volumes on the fly as new containers are created, automatically mounting them.</p>
<h4 id="heading-understand-how-networking-workshttpsdocsdockercomengineuserguidenetworking"><a target="_blank" href="https://docs.docker.com/engine/userguide/networking/">Understand how networking works</a></h4>
<p>How you implement networking can play a large role in how you use Kubernetes. Knowing how to open specific systems to the public internet and hiding others, such as databases, while maintaining communication between services is important to understand for starters. Some more complicated operations which I’ve needed to learn were how to integrate load balancing as well as giving each customer’s instance a custom hostname (things which Kubernetes makes a lot easier).</p>
<h3 id="heading-does-kubernetes-solve-problems-you-are-currently-facing">Does Kubernetes solve problems you are currently facing? ?</h3>
<p><img src="https://cdn-media-1.freecodecamp.org/images/zqFzeEAsKrXpWWtkZBdh-ju7DgxgV4-RvEcz" alt="Image" width="600" height="400" loading="lazy"></p>
<p>If you are not using containers to deploy your application, you probably shouldn’t be using Kubernetes yet. The problems Kubernetes aims to solve are problems that arise when you try and scale a container-based infrastructure.</p>
<p>Here are a few of the problems I think Kubernetes is great at solving when trying to deal with containers at scale.</p>
<h4 id="heading-scaling-up-resources">Scaling up resources</h4>
<p>Kubernetes is basically a cluster of nodes which provide compute resources that can be consumed by container workloads. This clustered architecture allows for a very easy scale-up or scale-down of resources. You just add or a remove nodes from the cluster, and Kubernetes will automatically utilize those resources or reassign workloads on your existing resources.</p>
<p>This solved a major problem I faced, because I went from having a single server I had to keep scaling up (an annoying manual process) to having the ability to scale up or down my infrastructure with a single command using the CLI.</p>
<h4 id="heading-performing-mass-updates">Performing mass updates</h4>
<p>Another problem that Kubernetes solves is the ability to update all your containers. Before, I was writing shell scripts which would pick each relevant container and recreate it using a new image tag. The process would take over an hour, and I had no way of validating that the update was successful. With Kubernetes I was able to perform an update with a single command as in the example below:</p>
<pre><code class="lang-bash">// Update all the pods of frontend to a new image tag
$ kubectl rolling-update frontend --image=image:v2
</code></pre>
<p>Kubernetes also allows you to update any part of Kubernetes (networks, storage, etc.) with commands based on any criteria. This is a huge step up from writing your own scripts to enact changes to your infrastructure.</p>
<h4 id="heading-self-healing">Self-healing</h4>
<p>The last and one of the most important pieces of Kubernetes I’d like to talk about is the ability to self-heal. If Kubernetes detects something wrong with part of its infrastructure, such as a node not responding or a container not passing its health check, it will perform steps to recreate those parts of itself until things start working again.</p>
<p>This is extremely useful because if a piece of the cluster goes down for any reason, the workload will be reassigned and you can even have Kubernetes recreate entire servers to fix the problem.</p>
<h3 id="heading-will-your-application-architecture-need-to-change">Will your application architecture need to change??</h3>
<p><img src="https://cdn-media-1.freecodecamp.org/images/I0-fuTV0uXQR1xoGvWjcyCI-eDJLgJlkrxHZ" alt="Image" width="600" height="400" loading="lazy">
<em>Sometimes adapting your app to Kubernetes is like fitting a square peg in a round hole</em></p>
<p>When I migrated to Kubernetes, there weren’t that many changes I had to make because it was originally architected to be a multi-instance platform deployed via containers.</p>
<p>Here are some of the things I’ve learned while moving my own workload to Kubernetes.</p>
<h4 id="heading-startup-time-of-your-app-is-important">Startup time of your app is important</h4>
<p>When you create a new deployment, you have to wait for your app to start before it becomes available to the end-user. This becomes a problem if your deployment process involves creating new instances when an end-user presses a button or if you are performing updates on all your customers instances, as that requires a rebuild of the pods.</p>
<p>When moving to Kubernetes you may need to make some changes to your codebase to make the startup process more efficient so the end-user doesn’t have a degraded experience using your product.</p>
<h4 id="heading-adapting-multi-tenant-architectures-is-difficult">Adapting multi-tenant architectures is difficult</h4>
<p>A multi-tenant architecture means you have a single instance of your application which manages all your end-users in partitioned tenants, usually with a single database being shared between everyone.</p>
<p>If your application is not built to utilize clustering (multiple servers acting as a single instance) you should not be using Kubernetes yet.</p>
<p>Generally I see two types of architectures when working with Kubernetes:</p>
<ul>
<li>Multi-instance with one instance of the app for each customer</li>
<li>Multi-tenant architecture with clustering capabilities as they can utilize scaling up and down resources</li>
</ul>
<p>I personally prefer multi-instance because they are much easier to implement compared to a clustered multi-tenant architecture. Also, the work involved in moving from multi-tenant to multi-instance isn’t too bad compared to adding cluster capabilities to a multi-instance architecture.</p>
<h4 id="heading-moving-to-a-stateless-application-is-a-large-amount-of-effort">Moving to a stateless application is a large amount of effort</h4>
<p>One of the great features of Kubernetes is the ability to scale up or down the number of pods in a deployment. But, if your application is not clustered or not stateless, this functionality is wasted since extra pods in a deployment wont be configured properly and can’t be utilized.</p>
<p>The process of utilizing statelessness in Kubernetes is often more trouble than it’s worth, since most times you will need to completely rework the way you handle configurations within your application.</p>
<p>Don’t be discouraged if you don’t want to spend the time to make your application stateless or clustered as there are many ways of adapting stateful deployments to use Kubernetes. But those have their own problems which I will not get into in this article.</p>
<h3 id="heading-should-you-adopt-kubernetes">Should you adopt Kubernetes? ?</h3>
<p>After asking yourself these questions, you should have a pretty good idea if Kubernetes will be a good fit for you at this time. Most early stage startups are probably not going to need it, and more mature ones may have a lot of investment in other technologies so it wouldn’t be feasible to switch.</p>
<p>I think the best case for someone moving to Kubernetes is a startup looking to move from having a Minimum Viable cloud infrastructure that is using containers to power production workloads to something more stable. That was my case, and I can say I went from having periodic downtimes due to resource mismanagement and overworked servers to not having to worry at all about my infrastructure thanks to the power of Kubernetes.</p>
<p>Looking to connect Kubernetes to your SaaS? Lets talk - <a target="_blank" href="mailto:ben@servicebot.io">ben@servicebot.io</a></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Doj-pHbdszxakMEVRnRFpJmZdW8nkJmsKoXo" alt="Image" width="600" height="400" loading="lazy"></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
