open-saas Documentation

A 100% free modern JS SaaS boilerplate (React, NodeJS, Prisma). Full-featured: Auth (email, google, github, slack, MS), Email sending, Background jobs,...

RAW Doc

Technical Documentation: wasp-lang/open-saas

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)

Welcome to your new SaaS App! 🎉

https://github.com/user-attachments/assets/3856276b-23e9-455e-a564-b5f26f4f0e98

You've decided to build a SaaS app with the Open SaaS template. Great choice!

This template is:

  1. fully open-source
  2. completely free to use and distribute
  3. comes with a ton of features out of the box!
  4. ready to work with your favorite AI coding tool or agent (Claude Code, Cursor, Codex, OpenCode, etc.)

🧑‍💻 Check it out in action here: OpenSaaS.sh
📚 Check out the Docs here: Open SaaS Docs

What's inside?

The template itself is built on top of some very powerful tools and frameworks, including:

  • 🐝 Wasp - a full-stack React, NodeJS, Prisma framework with superpowers
  • 🚀 Astro - Astro's lightweight "Starlight" template for documentation and blog
  • 💸 Stripe, Polar.sh, or Lemon Squeezy - for products and payments
  • 💅 ShadCN UI - for components & styling (plus admin dashboard!)
  • 🤖 AI-Ready - Custom Plugins, Skills, & Rules for AI-assisted coding with Claude Code, Cursor, or your favorite AI-assisted coding tool
  • 📈 Plausible or Google Analytics
  • 🤖 OpenAI - OpenAI API w/ function calling example
  • 📦 AWS S3 - for file uploads
  • 📧 SendGrid, MailGun, or SMTP - for email sending
  • 🧪 Playwright - end-to-end tests with Playwright

Because we're using Wasp as the full-stack framework, we can leverage a lot of its features to build our SaaS in record time, including:

  • 🔐 Full-stack Authentication - Email verified + social Auth in a few lines of code.
  • ⛑ End-to-end Type Safety - Type your backend functions and get inferred types on the front-end automatically, without the need to install or configure any third-party libraries. Oh, and type-safe Links, too!
  • 🤖 Jobs - Run cron jobs in the background or set up queues simply by defining a function in the config file.
  • 🚀 One-command Deploy - Easily deploy your DB, Server, & Client with one commaned to Railway or Fly.io via the CLI. Or deploy manually to any other hosting serivce of your choice.

You also get access to Wasp's diverse, helpful community if you get stuck or need help.

Getting Started

Simple Instructions

First, to install the latest version of Wasp on macOS, Linux, or Windows with WSL, run the following command:

bash
npm i @wasp.sh/wasp-cli

Then, create a new SaaS app with the following command:

bash
wasp new -t saas

This will create a clean copy of the Open SaaS template into a new directory, and you can start building your SaaS app right away!

Detailed Instructions

For everything you need to know about getting started and using this template, check out the Open SaaS Docs.

We've documented everything in great detail, including installation instructions, pulling updates to the template, guides for integrating services, SEO, deployment, and more. 🚀

Getting Help & Providing Feedback

There are two ways to get help or provide feedback (and we try to always respond quickly!):

  1. Open an issue
  2. Wasp Discord -- please direct questions to the #🙋questions forum channel

Development Tools

Code Quality Tools

This repository includes comprehensive code quality tooling to help maintain code standards:

Prettier (Code Formatting)

Prettier is configured for automatic code formatting across all JavaScript, TypeScript, and other supported files.

bash
# Check if files are formatted correctly
npm run prettier:check

# Automatically format all files
npm run prettier:format

ESLint (Code Linting)

ESLint is configured with TypeScript and React support to catch potential bugs and enforce code quality standards.

bash
# Run ESLint to check for issues
npm run lint

# Automatically fix fixable issues
npm run lint:fix

The ESLint configuration includes:

  • TypeScript support with @typescript-eslint
  • React and React Hooks linting
  • Sensible defaults tuned for a SaaS application
  • Automatic support for CommonJS (.cjs), ES Modules (.mjs), and TypeScript files

Both Prettier and ESLint checks are automatically run in CI/CD pipelines to ensure code quality.

For information about other development tools used to maintain derived projects (like opensaas.sh and template-test), see tools/README.md.

Contributing

Note that we've tried to get as many of the core features of a SaaS app into this template as possible, but there still might be some missing features or functionality.

We could always use some help tying up loose ends: contributions are welcome! Check out CONTRIBUTING.md for more details.


Opensaas Sh Blog Src Content Docs Index (`opensaas-sh/blog/src/content/docs/index.mdx`)


title: Introduction
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import HiddenLLMHelper from '../../components/HiddenLLMHelper.astro';

Welcome to your new SaaS App!

{/* TODO: add a screenshot of the app */}

You've decided to build a SaaS app with this template. Great choice! 🎉

This template is:

  1. fully open-source
  2. completely free to use and distribute
  3. comes with a ton of features out of the box
  4. community-driven and constantly improving!

Check it out in action here: OpenSaaS.sh
Check out the Code: Open SaaS GitHub Repo

:::tip[FREE & OPEN-SOURCE!? 🌟]
That's right. Use this template however you like. No strings attached.

If you find this template useful, consider giving us a star on GitHub. It helps us to keep bringing you open-source software just like this!
:::

What's inside?

The template itself is built on top of some very powerful tools and frameworks, including:

Because we're using Wasp as the full-stack framework, we can leverage a lot of its features to build our SaaS in record time, including:

  • 🔐 Full-stack Authentication - Email verified + social Auth in a few lines of code.
  • ⛑ End-to-end Type Safety - Type your backend functions and get inferred types on the front-end automatically, without the need to install or configure any third-party libraries. Oh, and type-safe Links for navigation, too!
  • 🤖 Jobs - Run cron jobs in the background or set up queues simply by defining a function in the config file.
  • 🚀 One-command Deploy - Easily deploy via the CLI to Fly.io or Railway, with guides for other providers like Netlify, Cloudflare, Heroku, etc.

You also get access to Wasp's diverse, helpful community if you get stuck or need help.

:::note
We've included many core SaaS features in this template, but not all features may be fully implemented or complete. This is an evolving project, and we welcome your contributions to help improve it!

Consider contributing to help us add new features and polish existing ones.
:::

In the next sections, we'll get our SaaS app started and tour its features. Let's get started!


Opensaas Sh Blog Src Content Docs 0.23 Index (`opensaas-sh/blog/src/content/docs/0.23/index.mdx`)


title: Introduction
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: "0.23"

import HiddenLLMHelper from '../../../components/HiddenLLMHelper.astro';

Welcome to your new SaaS App!

{/* TODO: add a screenshot of the app */}

You've decided to build a SaaS app with this template. Great choice! 🎉

This template is:

  1. fully open-source
  2. completely free to use and distribute
  3. comes with a ton of features out of the box
  4. community-driven and constantly improving!

Check it out in action here: OpenSaaS.sh
Check out the Code: Open SaaS GitHub Repo

:::tip[FREE & OPEN-SOURCE!? 🌟]
That's right. Use this template however you like. No strings attached.

If you find this template useful, consider giving us a star on GitHub. It helps us to keep bringing you open-source software just like this!
:::

What's inside?

The template itself is built on top of some very powerful tools and frameworks, including:

Because we're using Wasp as the full-stack framework, we can leverage a lot of its features to build our SaaS in record time, including:

  • 🔐 Full-stack Authentication - Email verified + social Auth in a few lines of code.
  • ⛑ End-to-end Type Safety - Type your backend functions and get inferred types on the front-end automatically, without the need to install or configure any third-party libraries. Oh, and type-safe Links for navigation, too!
  • 🤖 Jobs - Run cron jobs in the background or set up queues simply by defining a function in the config file.
  • 🚀 One-command Deploy - Easily deploy via the CLI to Fly.io or Railway, with guides for other providers like Netlify, Cloudflare, Heroku, etc.

You also get access to Wasp's diverse, helpful community if you get stuck or need help.

:::note
We've included many core SaaS features in this template, but not all features may be fully implemented or complete. This is an evolving project, and we welcome your contributions to help improve it!

Consider contributing to help us add new features and polish existing ones.
:::

In the next sections, we'll get our SaaS app started and tour its features. Let's get started!


Opensaas Sh Blog Src Content Docs General User Overview (`opensaas-sh/blog/src/content/docs/general/user-overview.md`)


title: User Overview
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


This reference will help you understand how the User entity works in this template.
This includes the user roles, subscription plans and statuses, and how to authorize access to certain pages and components.

User Entity

The User entity within your app is defined in the schema.prisma file:

prisma title="schema.prisma" ins="user: {}"
model User {
  id                        String          @id @default(uuid())
  createdAt                 DateTime        @default(now())

  email                     String?         @unique
  username                  String?         @unique
  isAdmin                   Boolean         @default(false)

  paymentProcessorUserId    String?         @unique
  lemonSqueezyCustomerPortalUrl String?     // You can delete this if you're not using Lemon Squeezy as your payments processor.
  subscriptionStatus        String?         // 'active', 'cancel_at_period_end', 'past_due', 'deleted'.
  subscriptionPlan          String?         // 'hobby', 'pro'.
  datePaid                  DateTime?
  credits                   Int             @default(3)

  gptResponses              GptResponse[]
  contactFormMessages       ContactFormMessage[]
  tasks                     Task[]
  files                     File[]
}

We store all user relevant information on the User entity, including identification, subscription, and payment processor information. Meanwhile, Wasp abstracts away all the Auth related entities dealing with passwords, sessions, and socialLogins, so you don't have to worry about these at all in your Prisma schema (if you want to learn more about this process, check out the Wasp Auth Docs).

Payment Processors and Subscriptions

We use payment processor to handle all of our subscription payments. The User entity has a number of fields that are related to payment processors and their ability to access features behind the paywall:

prisma title="schema.prisma" {3-7}
model User {
  // ...
  paymentProcessorUserId    String?         @unique
  lemonSqueezyCustomerPortalUrl String?     // You can delete this if you're not using Lemon Squeezy as your payments processor.
  subscriptionStatus        String?         // 'active', 'cancel_at_period_end', 'past_due', 'deleted'.
  subscriptionPlan          String?         // 'hobby', 'pro'.
  datePaid                  DateTime?
  credits                   Int             @default(3)
  // ...
}
  • paymentProcessorUserId: The payment processor's customer ID. This is created on checkout and used to identify the customer.
  • lemonSqueezyCustomerPortalUrl: Only relevant for Lemon Squeezy. A pernament URL to a user's custoemr portal.
  • subscriptionStatus: The subscription status of a user. This is set by the payment processor and is used to determine whether the user has access to the app or not. By default, we have four statuses: active, past_due, cancel_at_period_end, and deleted.
  • subscriptionPlan: The subscription plan a user is on. This is set by the app and is used to determine what features the user has access to. By default, we have three payment plans: hobby and pro subscription plans, as well as a credits10 one-time purchase plan.
  • credits (optional): By default, a user is given 3 credits to trial your product before they have to pay. You can create a one-time purchase product in Stripe to allow users to purchase more credits if they run out, e.g. the credits10 plan in the template.

Subscription Statuses

In general, we determine if a user has paid for an initial subscription by checking if the subscriptionStatus field is set. This field is set by payment processor within your webhook handler and is used to signify more detailed information on the user's current status. By default, the template handles four statuses: active, past_due, cancel_at_period_end, and deleted.

  • When active the user has paid for a subscription and has full access to the app.
  • When cancel_at_period_end, the user has canceled their subscription and has access to the app until the end of their billing period.
  • When deleted, the user has reached the end of their subscription period after canceling and no longer has access to the app.
  • When past_due, the user's automatic subscription renewal payment was declined (e.g. their credit card expired). You can choose how to handle this status within your app. For example, you can send the user an email to update their payment information:
    tsx title="src/payment/stripe/webhook.ts"
    import { emailSender } from "wasp/server/email";
    //...
    
    if (subscription.status === 'past_due') {
      const updatedCustomer = await context.entities.User.update({
        where: {
          id: customer.id,
        },
        data: {
          subscriptionStatus: 'past_due',
        },
      });
    
      if (updatedCustomer.email) {
        await emailSender.send({
          to: updatedCustomer.email,
          subject: 'Your Payment is Past Due',
          text: 'Please update your payment information to continue using our service.',
          html: '...',
        });
      }
    }

See the client-side authorization section below for more info on how to handle these statuses within your app.

Subscription Plans

The subscriptionPlan field is used to determine what features the user has access to.

By default, we have three plans: hobby and pro subscription plans, as well as a credits10 one-time purchase plan.

You can add more plans by adding more prodcuts to your payment processor and updating environment variables in your .env.server file as well as the relevant code in your app.

See the Payment Integrations Guide of a specific payment processor for more info on how to do this.

User Roles

At the moment, we have two user roles: admin and user. This is defined within the isAdmin field in the User entity:

prisma title="schema.prisma" {5}
model User {
  // ...
  email                     String?         @unique
  username                  String?         @unique
  isAdmin                   Boolean         @default(false)
  //...
}

As an admin, a user has access to the Admin dashboard, along with the user table where they can view and search for users, and edit and update information manually if necessary.

:::tip[Admin Privileges]
If you'd like to give yourself and/or certain users admin privileges, follow the instructions in the Admin Dashboard section.
:::

As a user, a user has access to the user-facing app that sits behind the login, but not the Admin dashboard. You can further restrict access to certain features within the app by following the authorization guide.


Opensaas Sh Blog Src Content Docs Start Getting Started (`opensaas-sh/blog/src/content/docs/start/getting-started.mdx`)


title: Getting Started
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import VideoPlayer from '../../../components/VideoPlayer.astro';

This guide will help you get your new SaaS app up and running.

If you prefer video tutorials, you can watch this walkthrough below which will guide you through most of the setup (installation, authentication, payments, etc.). If you get stuck at any point, you can refer back to these docs for more information.

:::tip[Prefer to learn while you build?]
Open Vibe is our free, open-source curriculum that turns Claude Code (or your agent of choice) into a SaaS-building tutor. Phase 2 walks you through building a real SaaS on top of this Open SaaS template — a great companion to this guide if you want to understand the systems as you go.
:::

Install Wasp

Pre-requisites

You must have Node.js (and NPM) installed on your machine and available in PATH to use Wasp.
Your version of Node.js must be >= 24.14.1 (you can check requirements here).

To switch easily between Node.js versions, we recommend using nvm.

:::note[Installing and using nvm]

text
Need help with nvm?


Install nvm via your OS package manager (`apt`, `pacman`, `homebrew`, ...) or via the [nvm](https://github.com/nvm-sh/nvm#install--update-script) install script.

Then, install a version of Node.js that you need:

```shell
nvm install 24
```

Finally, whenever you need to ensure a specific version of Node.js is used, run:

```shell
nvm use 24
```

to set the Node.js version for the current shell session.

You can run

```shell
node -v
```

to check the version of Node.js currently being used in this shell session.

Check NVM repo for more details: [https://github.com/nvm-sh/nvm](https://github.com/nvm-sh/nvm).

:::

Linux and macOS

Open your terminal and run:

shell
npm i @wasp.sh/wasp-cli

Windows

In order to use Wasp on Windows, you need to install WSL2 (Windows Subsystem for Linux) and a Linux distribution of your choice. We recommend using Ubuntu.

You can refer to this article for a step by step guide to using Wasp in the WSL environment. If you need further help, reach out to us on Discord.

:::caution[WSL2 Docker post installation steps]

text
Complete those steps to ensure that PostgreSQL and Docker work correctly with Wasp in WSL2.

It is recommended to complete those post-install steps in WSL, based on the official Docker guide. These work if you are experiencing an error similar to this one.

First, run

bash
sudo groupadd docker

command to create the docker group in case it doesn't exist. If it exists, don't worry, just continue with next steps. After that, add your current user to docker group by running

bash
sudo usermod -aG docker $USER

where $USER is your username. After that, log out and log back in to apply the changes. Finally, run

bash
su -s $USER

:::

Once in WSL2, run the following command in your WSL2 environment:

sh
npm i @wasp.sh/wasp-cli

:::caution[WSL2 and file system issues]

text
Are you getting file system issues using WSL2?

If you are using WSL2, make sure that your Wasp project is not on the Windows file system, but instead on the Linux file system. Otherwise, Wasp won't be able to detect file changes, due to this issue in WSL2.

:::

Finalize Installation

Run the following command to verify that Wasp was installed correctly:

shell
wasp version

Also be sure to install the Wasp VSCode extension to get the best DX, e.g. syntax highlighting, code scaffolding, autocomplete, etc.

:::tip[Installing the Wasp VSCode Extension]
You can install the Wasp VSCode extension by searching for "Wasp" in the Extensions tab in VSCode, or by visiting the Wasp VSCode Extension homepage
:::

Setting up your SaaS app

Cloning the Open SaaS template

From the directory where you'd like to create your new project run:

sh
wasp new

Then select option saas from the list of templates after entering the name of your project.
This will clone a clean copy of the Open SaaS template into a new directory! 🎉

Start your database

Before you start your app, you need to have a Postgres Database connected and running. With Wasp, that's super easy!

First, make sure you have Docker installed and running. If not, download and install it here

With Docker running, open a new terminal window/tab and position yourself in the app directory:

sh
cd app

Then run:

sh
wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just make sure to leave this terminal window open in the background while developing. Once you terminate the process, your DB will no longer be available to your app.

Now let's create our very first database migration, to ensure the database has a correct schema. Open a new terminal tab/window and run the following command:

sh
wasp db migrate-dev

This might take a bit since this is the first time you are running it and it needs to install all the
dependencies for your Wasp project.

In the future, you will also want to run wasp db migrate-dev whenever you make changes to your Prisma schema (Entities),
to apply those schema changes to the database.

Additionally, if you want to see or manage your DB via Prisma's DB Studio GUI, run:

sh
wasp db studio

Start your app

At this point, you should be positioned in the app/ directory and have the database running in another terminal session.

Next, copy the .env.server.example file to .env.server.

sh
cp .env.server.example .env.server

.env.server is where API keys for services like payments, email sender, and similar go, and this is where you will want to put them in later.
For now, you can leave it as it is (dummy API keys), this will be enough to run the app.

Then run:

sh
wasp start

This will install all the dependencies (if necessary) and start the app (client and server) for you :)!

If the app doesn't open automatically in your browser, you can open it manually by visiting the client URL that wasp start prints (http://localhost:3000 by default).

At this point, you should have:

  • Your database running in one terminal session, likely on port 5432.
  • Your app running in another terminal session, the client likely on port 3000, and the server likely on port 3001.

Run Blog and Docs

This SaaS app comes with a docs and blog section built with the Starlight theme on top of the Astro framework. You can use this as a starting point for your own blog and documentation, if necessary.

If you do not need this, you can simply delete the blog folder from the root of the project.

If you want to run the Starlight docs and blog, first navigate to the blog folder:

sh
cd ../blog

Then run:

sh
npm install

Then start the development server:

sh
npm run dev

Check the instructions in the terminal for the link to open the blog, it will typically be https://localhost:4321/.

What's next?

Awesome! We have our new app ready and we know how to run both it and the blog/docs! Now, in the next section, we'll give you a quick "guided tour" of the different parts of the app we created and understand how it works.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::


Opensaas Sh Blog Src Content Docs 0.23 General User Overview (`opensaas-sh/blog/src/content/docs/0.23/general/user-overview.md`)


title: User Overview
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/general/user-overview

This reference will help you understand how the User entity works in this template.
This includes the user roles, subscription plans and statuses, and how to authorize access to certain pages and components.

User Entity

The User entity within your app is defined in the schema.prisma file:

prisma title="schema.prisma" ins="user: {}"
model User {
  id                        String          @id @default(uuid())
  createdAt                 DateTime        @default(now())

  email                     String?         @unique
  username                  String?         @unique
  isAdmin                   Boolean         @default(false)

  paymentProcessorUserId    String?         @unique
  lemonSqueezyCustomerPortalUrl String?     // You can delete this if you're not using Lemon Squeezy as your payments processor.
  subscriptionStatus        String?         // 'active', 'cancel_at_period_end', 'past_due', 'deleted'.
  subscriptionPlan          String?         // 'hobby', 'pro'.
  datePaid                  DateTime?
  credits                   Int             @default(3)

  gptResponses              GptResponse[]
  contactFormMessages       ContactFormMessage[]
  tasks                     Task[]
  files                     File[]
}

We store all user relevant information on the User entity, including identification, subscription, and payment processor information. Meanwhile, Wasp abstracts away all the Auth related entities dealing with passwords, sessions, and socialLogins, so you don't have to worry about these at all in your Prisma schema (if you want to learn more about this process, check out the Wasp Auth Docs).

Payment Processors and Subscriptions

We use payment processor to handle all of our subscription payments. The User entity has a number of fields that are related to payment processors and their ability to access features behind the paywall:

prisma title="schema.prisma" {3-7}
model User {
  // ...
  paymentProcessorUserId    String?         @unique
  lemonSqueezyCustomerPortalUrl String?     // You can delete this if you're not using Lemon Squeezy as your payments processor.
  subscriptionStatus        String?         // 'active', 'cancel_at_period_end', 'past_due', 'deleted'.
  subscriptionPlan          String?         // 'hobby', 'pro'.
  datePaid                  DateTime?
  credits                   Int             @default(3)
  // ...
}
  • paymentProcessorUserId: The payment processor's customer ID. This is created on checkout and used to identify the customer.
  • lemonSqueezyCustomerPortalUrl: Only relevant for Lemon Squeezy. A pernament URL to a user's custoemr portal.
  • subscriptionStatus: The subscription status of a user. This is set by the payment processor and is used to determine whether the user has access to the app or not. By default, we have four statuses: active, past_due, cancel_at_period_end, and deleted.
  • subscriptionPlan: The subscription plan a user is on. This is set by the app and is used to determine what features the user has access to. By default, we have three payment plans: hobby and pro subscription plans, as well as a credits10 one-time purchase plan.
  • credits (optional): By default, a user is given 3 credits to trial your product before they have to pay. You can create a one-time purchase product in Stripe to allow users to purchase more credits if they run out, e.g. the credits10 plan in the template.

Subscription Statuses

In general, we determine if a user has paid for an initial subscription by checking if the subscriptionStatus field is set. This field is set by payment processor within your webhook handler and is used to signify more detailed information on the user's current status. By default, the template handles four statuses: active, past_due, cancel_at_period_end, and deleted.

  • When active the user has paid for a subscription and has full access to the app.
  • When cancel_at_period_end, the user has canceled their subscription and has access to the app until the end of their billing period.
  • When deleted, the user has reached the end of their subscription period after canceling and no longer has access to the app.
  • When past_due, the user's automatic subscription renewal payment was declined (e.g. their credit card expired). You can choose how to handle this status within your app. For example, you can send the user an email to update their payment information:
    tsx title="src/payment/stripe/webhook.ts"
    import { emailSender } from "wasp/server/email";
    //...
    
    if (subscription.status === 'past_due') {
      const updatedCustomer = await context.entities.User.update({
        where: {
          id: customer.id,
        },
        data: {
          subscriptionStatus: 'past_due',
        },
      });
    
      if (updatedCustomer.email) {
        await emailSender.send({
          to: updatedCustomer.email,
          subject: 'Your Payment is Past Due',
          text: 'Please update your payment information to continue using our service.',
          html: '...',
        });
      }
    }

See the client-side authorization section below for more info on how to handle these statuses within your app.

Subscription Plans

The subscriptionPlan field is used to determine what features the user has access to.

By default, we have three plans: hobby and pro subscription plans, as well as a credits10 one-time purchase plan.

You can add more plans by adding more prodcuts to your payment processor and updating environment variables in your .env.server file as well as the relevant code in your app.

See the Payment Integrations Guide of a specific payment processor for more info on how to do this.

User Roles

At the moment, we have two user roles: admin and user. This is defined within the isAdmin field in the User entity:

prisma title="schema.prisma" {5}
model User {
  // ...
  email                     String?         @unique
  username                  String?         @unique
  isAdmin                   Boolean         @default(false)
  //...
}

As an admin, a user has access to the Admin dashboard, along with the user table where they can view and search for users, and edit and update information manually if necessary.

:::tip[Admin Privileges]
If you'd like to give yourself and/or certain users admin privileges, follow the instructions in the Admin Dashboard section.
:::

As a user, a user has access to the user-facing app that sits behind the login, but not the Admin dashboard. You can further restrict access to certain features within the app by following the authorization guide.


Opensaas Sh Blog Src Content Docs 0.23 Start Getting Started (`opensaas-sh/blog/src/content/docs/0.23/start/getting-started.mdx`)


title: Getting Started
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/start/getting-started

import VideoPlayer from '../../../../components/VideoPlayer.astro';

This guide will help you get your new SaaS app up and running.

If you prefer video tutorials, you can watch this walkthrough below which will guide you through most of the setup (installation, authentication, payments, etc.). If you get stuck at any point, you can refer back to these docs for more information.

:::tip[Prefer to learn while you build?]
Open Vibe is our free, open-source curriculum that turns Claude Code (or your agent of choice) into a SaaS-building tutor. Phase 2 walks you through building a real SaaS on top of this Open SaaS template — a great companion to this guide if you want to understand the systems as you go.
:::

Install Wasp

Pre-requisites

You must have Node.js (and NPM) installed on your machine and available in PATH to use Wasp.
Your version of Node.js must be >= 24.14.1 (you can check requirements here).

To switch easily between Node.js versions, we recommend using nvm.

:::note[Installing and using nvm]

text
Need help with nvm?



Install nvm via your OS package manager (`apt`, `pacman`, `homebrew`, ...) or via the [nvm](https://github.com/nvm-sh/nvm#install--update-script) install script.

Then, install a version of Node.js that you need:

```shell
nvm install 24
```

Finally, whenever you need to ensure a specific version of Node.js is used, run:

```shell
nvm use 24
```

to set the Node.js version for the current shell session.

You can run

```shell
node -v
```

to check the version of Node.js currently being used in this shell session.

Check NVM repo for more details: [https://github.com/nvm-sh/nvm](https://github.com/nvm-sh/nvm).

:::

Linux and macOS

Open your terminal and run:

shell
npm i @wasp.sh/[email protected]

Windows

In order to use Wasp on Windows, you need to install WSL2 (Windows Subsystem for Linux) and a Linux distribution of your choice. We recommend using Ubuntu.

You can refer to this article for a step by step guide to using Wasp in the WSL environment. If you need further help, reach out to us on Discord.

:::caution[WSL2 Docker post installation steps]

text
Complete those steps to ensure that PostgreSQL and Docker work correctly with Wasp in WSL2.

It is recommended to complete those post-install steps in WSL, based on the official Docker guide. These work if you are experiencing an error similar to this one.

First, run

bash
sudo groupadd docker

command to create the docker group in case it doesn't exist. If it exists, don't worry, just continue with next steps. After that, add your current user to docker group by running

bash
sudo usermod -aG docker $USER

where $USER is your username. After that, log out and log back in to apply the changes. Finally, run

bash
su -s $USER

:::

Once in WSL2, run the following command in your WSL2 environment:

sh
npm i @wasp.sh/[email protected]

:::caution[WSL2 and file system issues]

text
Are you getting file system issues using WSL2?

If you are using WSL2, make sure that your Wasp project is not on the Windows file system, but instead on the Linux file system. Otherwise, Wasp won't be able to detect file changes, due to this issue in WSL2.

:::

Finalize Installation

Run the following command to verify that Wasp was installed correctly:

shell
wasp version

Also be sure to install the Wasp VSCode extension to get the best DX, e.g. syntax highlighting, code scaffolding, autocomplete, etc.

:::tip[Installing the Wasp VSCode Extension]
You can install the Wasp VSCode extension by searching for "Wasp" in the Extensions tab in VSCode, or by visiting the Wasp VSCode Extension homepage
:::

Setting up your SaaS app

Cloning the Open SaaS template

From the directory where you'd like to create your new project run:

sh
wasp new

Then select option saas from the list of templates after entering the name of your project.
This will clone a clean copy of the Open SaaS template into a new directory! 🎉

Start your database

Before you start your app, you need to have a Postgres Database connected and running. With Wasp, that's super easy!

First, make sure you have Docker installed and running. If not, download and install it here

With Docker running, open a new terminal window/tab and position yourself in the app directory:

sh
cd app

Then run:

sh
wasp start db

This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just make sure to leave this terminal window open in the background while developing. Once you terminate the process, your DB will no longer be available to your app.

Now let's create our very first database migration, to ensure the database has a correct schema. Open a new terminal tab/window and run the following command:

sh
wasp db migrate-dev

This might take a bit since this is the first time you are running it and it needs to install all the
dependencies for your Wasp project.

In the future, you will also want to run wasp db migrate-dev whenever you make changes to your Prisma schema (Entities),
to apply those schema changes to the database.

Additionally, if you want to see or manage your DB via Prisma's DB Studio GUI, run:

sh
wasp db studio

Start your app

At this point, you should be positioned in the app/ directory and have the database running in another terminal session.

Next, copy the .env.server.example file to .env.server.

sh
cp .env.server.example .env.server

.env.server is where API keys for services like payments, email sender, and similar go, and this is where you will want to put them in later.
For now, you can leave it as it is (dummy API keys), this will be enough to run the app.

Then run:

sh
wasp start

This will install all the dependencies (if necessary) and start the app (client and server) for you :)!

If the app doesn't open automatically in your browser, you can open it manually by visiting http://localhost:3000 in your browser.

At this point, you should have:

  • Your database running in one terminal session, likely on port 5432.
  • Your app running in another terminal session, the client likely on port 3000, and the server likely on port 3001.

Run Blog and Docs

This SaaS app comes with a docs and blog section built with the Starlight theme on top of the Astro framework. You can use this as a starting point for your own blog and documentation, if necessary.

If you do not need this, you can simply delete the blog folder from the root of the project.

If you want to run the Starlight docs and blog, first navigate to the blog folder:

sh
cd ../blog

Then run:

sh
npm install

Then start the development server:

sh
npm run dev

Check the instructions in the terminal for the link to open the blog, it will typically be https://localhost:4321/.

What's next?

Awesome! We have our new app ready and we know how to run both it and the blog/docs! Now, in the next section, we'll give you a quick "guided tour" of the different parts of the app we created and understand how it works.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::


Opensaas Sh Blog Src Content Docs Guides Payment Integrations Index (`opensaas-sh/blog/src/content/docs/guides/payment-integrations/index.mdx`)


title: Overview
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import { TabItem, Tabs } from '@astrojs/starlight/components';

This guide will show you how to set up payments for testing and local development with the following payment processors:

  • Stripe
  • Lemon Squeezy
  • Polar
  • Paddle

:::note[Which should I choose?]
Stripe is the industry standard, is more configurable, and has cheaper fees.
Lemon Squeezy, Polar, and Paddle act as a Merchant of Record. This means they take care of paying taxes in multiple countries for you, but charge higher fees per transaction.
:::

Important First Steps

First, go to /src/payment/paymentProcessor.ts and choose which payment processor you'd like to use:

ts title="src/payment/paymentprocessor.ts" ins={8, 10, 12, 14}
import { stripePaymentProcessor } from './stripe/paymentProcessor';
import { lemonSqueezyPaymentProcessor } from './lemonSqueezy/paymentProcessor';
import { polarPaymentProcessor } from './polar/paymentProcessor';
import { paddlePaymentProcessor } from './paddle/paymentProcessor';

// ...

export const paymentProcessor: PaymentProcessor = stripePaymentProcessor;
// or
export const paymentProcessor: PaymentProcessor = lemonSqueezyPaymentProcessor;
// or
export const paymentProcessor: PaymentProcessor = polarPaymentProcessor;
// or
export const paymentProcessor: PaymentProcessor = paddlePaymentProcessor;

Next, go to /src/payment/paymentProcessorClient.ts and choose the same payment processor there.
This is the part of the payment processor that runs in the browser, e.g. it redirects users to the checkout page.
If the two don't match, your app will show an error when a user tries to buy a plan.

ts title="src/payment/paymentprocessorclient.ts" ins={8, 10, 12, 14}
import { stripePaymentProcessorClient } from './stripe/paymentProcessorClient';
import { lemonSqueezyPaymentProcessorClient } from './lemonSqueezy/paymentProcessorClient';
import { polarPaymentProcessorClient } from './polar/paymentProcessorClient';
import { paddlePaymentProcessorClient } from './paddle/paymentProcessorClient';

// ...

export const paymentProcessorClient: PaymentProcessorClient = stripePaymentProcessorClient;
// or
export const paymentProcessorClient: PaymentProcessorClient = lemonSqueezyPaymentProcessorClient;
// or
export const paymentProcessorClient: PaymentProcessorClient = polarPaymentProcessorClient;
// or
export const paymentProcessorClient: PaymentProcessorClient = paddlePaymentProcessorClient;

Then you should delete:

  • The unused payment processor directories at /src/payment/.
  • Any unused environment variables from .env.server (they will be prefixed with the name of the provider you are not using):
    • E.g. STRIPE_API_KEY, LEMONSQUEEZY_API_KEY, POLAR_ORGANIZATION_ACCESS_TOKEN.
  • Update src/env.ts to remove the env schema imports and .merge() calls for the providers you removed.
  • Make sure to also run npm uninstall for providers you didn't use:
    • Stripe: npm uninstall stripe
    • Polar: npm uninstall @polar-sh/sdk
    • Lemon Squeezy: npm uninstall @lemonsqueezy/lemonsqueezy.js
    • Paddle: npm uninstall @paddle/paddle-node-sdk @paddle/paddle-js
  • If you are not using Paddle, remove REACT_APP_PADDLE_CLIENT_TOKEN and REACT_APP_PADDLE_SANDBOX_MODE from .env.client, and the paddleClientEnvSchema from clientEnvValidationSchema in src/env.ts.
  • If you are not using Lemon Squeezy remove the lemonSqueezyCustomerPortalUrl field from the User model in the schema.prisma file.

Now your code is ready to go with your preferred payment processor and it's time to configure your payment processor's API keys, products, and other settings.

Follow the steps for your selected processor:

Deploying

Once you're ready to deploy your app, follow the steps from the deploying guide to set up the production settings for your payment provider.


Opensaas Sh Blog Src Content Docs 0.23 Guides Payment Integrations Index (`opensaas-sh/blog/src/content/docs/0.23/guides/payment-integrations/index.mdx`)


title: Overview
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/payment-integrations

import { TabItem, Tabs } from '@astrojs/starlight/components';

This guide will show you how to set up payments for testing and local development with the following payment processors:

  • Stripe
  • Lemon Squeezy
  • Polar

:::note[Which should I choose?]
Stripe is the industry standard, is more configurable, and has cheaper fees.
Lemon Squeezy and Polar act as a Merchant of Record. This means they take care of paying taxes in multiple countries for you, but charge higher fees per transaction.
:::

Important First Steps

First, go to /src/payment/paymentProcessor.ts and choose which payment processor you'd like to use:

ts title="src/payment/paymentprocessor.ts" ins={7, 9, 11}
import { stripePaymentProcessor } from './stripe/paymentProcessor';
import { lemonSqueezyPaymentProcessor } from './lemonSqueezy/paymentProcessor';
import { polarPaymentProcessor } from './polar/paymentProcessor';

// ...

export const paymentProcessor: PaymentProcessor = stripePaymentProcessor;
// or
export const paymentProcessor: PaymentProcessor = lemonSqueezyPaymentProcessor;
// or
export const paymentProcessor: PaymentProcessor = polarPaymentProcessor;

Then you should delete:

  • The unused payment processor directories at /src/payment/.
  • Any unused environment variables from .env.server (they will be prefixed with the name of the provider you are not using):
    • E.g. STRIPE_API_KEY, LEMONSQUEEZY_API_KEY, POLAR_ORGANIZATION_ACCESS_TOKEN.
  • Update src/env.ts to remove the env schema imports and .merge() calls for the providers you removed.
  • Make sure to also run npm uninstall for providers you didn't use:
    • Stripe: npm uninstall stripe
    • Polar: npm uninstall @polar-sh/sdk
    • Lemon Squeezy: npm uninstall @lemonsqueezy/lemonsqueezy.js
  • If you are not using Lemon Squeezy remove the lemonSqueezyCustomerPortalUrl field from the User model in the schema.prisma file.

Now your code is ready to go with your preferred payment processor and it's time to configure your payment processor's API keys, products, and other settings.

Follow the steps for your selected processor:

Deploying

Once you're ready to deploy your app, follow the steps from the deploying guide to set up the production settings for your prayment provider.


Opensaas Sh Blog Src Content Docs Blog 2023 11 21 Coverlettergpt (`opensaas-sh/blog/src/content/docs/blog/2023-11-21-coverlettergpt.mdx`)


title: How I Built & Grew CoverLetterGPT to 5,000 Users and $200 MRR date: 2023-11-21 tags: ["indiehacker", "saas", "sideproject"] authors: vince

import { Image } from 'astro:assets';

Hey, I’m Vince…

I’m a self-taught developer that changed careers during the Covid Pandemic. I did it because I wanted a better career, enjoyed programming, and at the same time, had a keen interest in IndieHacking.

If you’re not aware, IndieHacking is the movement of developers who build potentially profitable side-projects in their spare time. And there are some very successful examples of IndieHackers and “solopreneurs” out there inspiring others, such as levels.io and Marc Lou.

This thought of being able to build my own side-project that could generate profit while I slept was always attractive to me.

So I’m happy to report that I’ve finally done it with my first software-as-a-service (SaaS) app, CoverLetterGPT.xyz, which I launched in March 2023!

I’ll be the first to admit that the results aren’t spectacular, but they’re still something I’m very proud of:

  • over 5,000 registered users
  • $203 monthly recurring revenue (MRR)

Below, I’m going to share with you how I built it (yes, it’s open-source), how I marketed and monetized it, along with a bunch of helpful resources to help you build your own profitable side-project.

What the heck is CoverLetterGPT?

CoverLetterGPT.xyz was an idea I got after the OpenAI API was released. It’s an app that allows you to upload a PDF of your CV/resumé, along with the job description you’re applying to, and it will generate and edit unique cover letters for you based on this information.

It also lets you save and manage your cover letters per each job, making it easy to make and apply to multiple jobs without having to keep copy and pasting all your important info into ChatGPT!

What’s the Tech Stack?

CoverLetterGPT is entirely open-source, so you can check out the code, fork it, learn from it, make your own, submit a PR (I’d love you forever if you did 🙂)… whatever!

I built it using the Wasp full-stack framework which allowed me to ship it about 10x faster.

Why?

Because Wasp as a framework allows you to describe your app’s core features in a main.wasp config file. Then it continually compiles and “glues” these features into a React-ExpressJS-Prisma full-stack app for you.

All you have to focus on is writing the client and server-side logic, and Wasp will do the boring stuff for you, like authentication & authorization, server config, email sending, and cron jobs.

BTW, Wasp is open-source and free and you can help the project out a ton by starring the repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏

⭐️ Star Wasp on GitHub 🙏

For the UI, I used Chakra UI, as I always do. I like that it’s a component-based UI library. This helps me build UI’s a lot faster than I would with Tailwind or vanilla CSS.

For payments, I used Stripe, (I’ll go into the details of monetization below).

The Server and Postgres Database are hosted on https://railway.app, with the client on Netlify.com’s free tier.

By the way, If you’re interested in building your own SaaS with almost the same stack as above, I also built a free SaaS template you can use that will save you days of work!

How I Marketed It

My biggest take-away from this whole project was that open-sourcing it was the best way to market it!

This seems counter-intuitive, right? Why would making the code available for anyone to see and copy be good for a business? You’re basically rolling out a red carpet for competitors, aren’t you?

Well, not quite.

First of all, the number of people who will realistically spend the time and energy launching a direct competitor is low. Also, most people interested in your open-source code want to learn some aspect of it and apply it to their own ideas, not just copy yours directly.

Secondly, and most importantly, the fact that it’s open-source makes people a lot more receptive to you talking about it.

When you present something you’ve built and give people the opportunity to learn from it, they’re much more welcoming! As a result, they’re more likely to upvote it, share it, use it, and recommend it to others.

This is exactly what happened with CoverLetterGPT! As a result of me sharing the open-source code, it get featured on the IndieHackers.com newsletter (>100k subscribers), shared on blogs, and talked about on social media platforms.

And even though it’s a small, simple product, I tried launching it on Product Hunt, where it also performed considerably well.

So, all together, these initial efforts combined gave my product a good initial marketing presence. To this day, I haven’t really done much else to market it, except some twitter posts (and this post, if you want to consider it marketing 🤑).

How I Monetized It

When I first launched in March 2023, I didn’t really expect anyone to pay for the product, but I wanted to learn how to use Stripe as a payments processor, thinking that the skills might be useful in the future.

So I started simple, and just put a one-time payment link for tips. No paywall, no subscriptions. It was entirely free to use with any tip amount welcome.

To my surprise, tips started coming in, with some as high as $10 dollars!

This encouraged me to force users to login to use the product, and add a paywall after users used up 3 credits.

My initial payment options were:

  • $4.95 for a 3 months access
  • $2.95 for 10 cover letter generations

That went reasonably well until I implemented the ability for users to use GPT to make finer edits to their generated cover letters. That’s when I changed my pricing and that’s when better profits started to come in:

  • $5.95 / month subscription with GPT-4
  • $2.95 / month subscription with GPT-3.5-turbo

Currently, over 90% of my customers choose the more powerful, more expensive plan with GPT-4, even though the 3 trial credits use the GPT-3.5-turbo model.

(I also integrated Bitcoin Lightning payments — check out the repo if you want to learn how — but haven’t received any yet.)

Now, with an MRR of ~$203, my monthly profit of course depends on my costs, which are:

  • Domain Name: $10/year
  • OpenAI bill: ~ $15/month
  • Hosting bill: ~ $3/month

Which leaves me at about ~ $183/month in profits 😀

Future Plans

One of the most surprising aspects about CoverLetterGPT.xyz’s success is that, on the surface, the product is very simple. Also, I’ve done very little in the way of SEO marketing, and haven’t continued to market it much at all. The current growth is mostly organic at this point thanks to my initial marketing efforts.

But I still have some plans to make it better:

  • buy a better top-level domain (TLD), like CoverLetterGPT.ai
  • add more features, like the ability to generate interview questions based on the cover letters
  • improve the UX and make it look more “professional”

If you have any other ideas how I could improve it, drop me a comment, message me on twitter/x, or submit a PR to the repo.

Final Words + More Resources

My intention with this article was to help others who might be considering launching their own SaaS product. So I hope that’s been the case here. If you still have any questions, don’t hesitate to ask.

Here are also the most important links from this article along with some further resources that will help in building and marketing your own profitable side-project:

Oh, and if you found these resources useful, don't forget to support Wasp by starring the repo on GitHub!

⭐️ Thanks For Your Support 🙏



title: 🍪 THE MOST ANNOYING COOKIE BANNER EVER HACKATHON 🤬
date: 2024-10-10
tags:

  • cookie consent
  • saas
  • sideproject
  • hackathon
    hideBannerImage: true
    authors: vince

import { Image } from 'astro:assets';
import wheel from '@assets/cookie-consent/wheel.gif';
import enter from '@assets/cookie-consent/enter.gif';
import keyboard from '@assets/cookie-consent/keyboard.jpg';
import share from '@assets/cookie-consent/image.png';

What kind of hackathon is this?

The goal here is simple. Make THE MOST ANNOYING COOKIE BANNER you can think of.

Cookie consent banners annoy us all. So we thought, why not have some fun with them? Here are a couple examples of what that might look like:

  1. The Cookie Consent Wheel of Fortune:

  2. The “Hit Enter When the Red Ball is Over the Accept Button to Consent” Banner:

Now it’s time for you to get creative. Btw, if you’re looking for some inspiration, check out these Ridiculous Volume Slider UI’s.

Prizes

2 winners will receive a nice mechanical keyboard, an additional annoying gift, as well as a shoutout on our socials.

The 2 winners will be selected by:

  • The Wasp team
  • Our Community (Discord & Twitter)

The community will get a chance to vote in a battle royale style elimination tournament, where two banners will go head-to-head and the winner will advance to the next round.

(The brand/style will depend on the winner's location, but we'll do our best to find one with a Wasp look and feel 😃)

How to participate

  • Fork the Annoying Cookie Banner Stackblitz Template

    • If you prefer to work in your own editor, just click on the Create a repository button after you fork the template
  • When finished with your banner, click on Share in the top left, and in the Embed tab, click Copy URL with the following settings:

  • Next, edit the MOST-ANNOYING-COOKIE-BANNER.md file on the Open SaaS repo.

    • Enter your GitHub username followed by the embed link you copied from Stackblitz
    • Note: after you create a PR, the Wasp team will add the ANNOYING COOKIE BANNER label to it.
  • Make sure you also ⭐️ star the Open Saas repository to be eligible to win!

Deadline & Results

Submit your PR before the 21st of October to be eligible to win!

Be sure to join our Discord or follow us on Twitter/X for updates and the final results!

Let's annoy our users! 🚀

Let's create the most annoying cookie consent banner and have some fun! This hackathon is your chance to show off your creativity and tech skills. We're super excited to see what wild ideas you come up with.

Remember, this is all about having fun and pushing the boundaries of user interface design!

Let the annoyance games begin! 🎉



title: We Made the Most Annoying Cookie Banners Ever
date: 2024-11-26
tags:

  • cookie consent
  • saas
  • sideproject
  • hackathon
    subtitle: and it was totally worth it
    authors: vince

import VideoPlayer from '../../../components/VideoPlayer.astro';
import { Image } from 'astro:assets';
import camblackwood from '@assets/cookie-banner-hackathon/295-camblackwood.mp4';
import gangnam from '@assets/cookie-banner-hackathon/300-lezzz-sound.mp4';
import wheredaway from '@assets/cookie-banner-hackathon/302-fecony-whereda.mp4';
import henryboyd from '@assets/cookie-banner-hackathon/296-henryboyd.mp4';
import wardbox from '@assets/cookie-banner-hackathon/286-wardbox.mp4';
import gangnamwinner from '@assets/cookie-banner-hackathon/285-3umaGH-gangnam.mp4';

We at Wasp just finished off a fun little hackathon where we asked our users to create the most annoying cookie consent banners they could think of (because cookie banners aren’t annoying enough already, right?). Then we let our community pick the winner in an elimination style tournament on X/Twitter.

It was a lot of fun, and the submissions were really creative, so we thought we’d highlight some of our favorites for you, including the community chosen winner. Check ‘em out below. We hope they inspire you... to... not use them on your own sites. :)

The GDPR Quiz

The General Data Protection Regulation (GDPR) is the EU's data privacy law that requires websites to get explicit consent from users before collecting their data through cookies.

GDPR is the reason we have cookie consent banners today.

This submission by Cam Blackwood gets to the core of why cookie consent banners are annoying: they force users to make a decision about something they barely understand (or care about), based on a regulation they probably don't even know exists.

Thanks for the reality check, Cam.

Windows of Time

Do you ever feel like cookie consent banners are UX design pattern from the past? Well, this submission by Lazi confirms that, by sending you through an operating system time machine to a past we'd all like to forget.

Disturbing, yet oddly comforting.

Find all the Cookies

There are some sites that make rejecting cookies a real pain, as if you were trying to find a needle in a haystack, or as if you were looking at a Where's Waldo puzzle.

This submission by Fecony, Wasp community meme lord, is just that. We find it annoying and fun at the same time, but what we're not sure about is if it's even possible to find them all.

Well played, Fecony.

Fresh Batch of Cookies

Most of us probably just smash the "accept" or "reject" button without even reading the fine print. But Henry Boyd is a maniac and with his submission, you have to accept ALLLLL the cookies.

And that's a whole lot of cookies.

What's more annoying than cookie consent banners? Probably job applications. Well [Wardbox], community builder extraordinaire, combines the best (or worst) of both worlds to make the cookie mangament process even more tedious than you could imagine.

Now all we have to do is wait for the rejection email.

Grand Prize Winner: Gangnam Style Beat

Our community-chosen winner's submission pretty much speaks for itself.

Make sure you turn on the sound for this one!

🎤 Eeeeh, sexy cookie. Op! op-op-op! 🎵

## And there you have it!

Thanks to everyone who participated! We had a lot of fun looking at all the submissions and we're glad to see that the community is as creative (and annoying) as ever.

And now for a quick PR announcement:

At Wasp we're working hard to build a modern, open-source full-stack React/NodeJS framework.

The easiest way to show your support is just to star the Wasp repo! 🐝 It helps us spread the word and motivates us to keep building.

⭐️ Star Wasp on GitHub 💪

Thanks for reading, and showing your support!


Opensaas Sh Blog Src Content Docs Blog 2024 12 04 Open Source Saas Boilerplate Vs Paid (`opensaas-sh/blog/src/content/docs/blog/2024-12-04-open-source-saas-boilerplate-vs-paid.mdx`)


title: Should You Use an Open-source SaaS Boilerplate Starter or a $300+ Paid One?
date: 2024-12-04
tags:

  • webdev
  • saas
  • sideproject
    authors: matija

import { Image } from 'astro:assets';
import marc1 from '../../../assets/boilerplate-starters/marc1.png';
import opensaas from '../../../assets/boilerplate-starters/opensaas.png';
import marc2 from '../../../assets/boilerplate-starters/marc2.png';
import daveShipfast from '../../../assets/boilerplate-starters/dave-shipfast-tweet.png';
import osGhStats from '../../../assets/boilerplate-starters/os-gh-stats.png';
import osCommits from '../../../assets/boilerplate-starters/os-commits.png';
import freeUpdates from '../../../assets/boilerplate-starters/free-updates-vs-not.png';
import communityContributions from '../../../assets/boilerplate-starters/community-contributions.png';
import boilerplateLicenses from '../../../assets/boilerplate-starters/boilerplate-licenses.png';

SaaS boilerplate starters became a very popular thing in the web dev community, and also a pathway to a luxury lifestyle for those behind them, sometimes making north of five figure amounts per month.

On the other hand, there's also been a rise of the open-source SaaS boilerplate starters, that cover various stacks and offer similar features as their paid counterparts, but completely for free and with an active community alongside.

So, what's the catch? Why pay $300 or $500 for something that you can simply get for free? Are there any trade-offs you should be aware of, and what are the pros and cons of each option?

As it usually turns out in the real world, the answer isn't completely black and white and depends on what you need (your requirements) but also what you want (your personal preferences).

The goal of this article is to break these further down and give you an objective, simple framework to follow when choosing a boilerplate starter for your next project. So, let's get into it!

Why a sudden craze with all these starters? SaaS-es are not a new thing at all

We have all been building web apps and SaaS-es for decades, you may rightfully observe, so why this became a thing just now? It seems like everybody is making their own starter today and getting a ton of excitement (and money) from the community.

The answer is that the complexity of building a SaaS (or in another words, a web app) in the last ten years increased tenfold. Partly it is due to the evolution of the underlying architecture (we switched from monolithic, server-based approach to “rich client ↔ backend”) which introduced more moving parts into the equation, and partly due to the explosion of options for each part of the stack.

If you were about to build a SaaS fifteen years ago, you pretty much knew you'd go with either Ruby on Rails, Laravel, or Django, depending on which language and community you preferred. These would come as a batteries-included solution, give you their best defaults and you'd be up and running in a matter of hour(s). You got a single, well-tested path to follow and not much decisions to make.

If you sit down and try to do the same today, your head would probably get dizzy after a few hour(s) of merely reading about all the possible options you could go with:

  • What to use for the frontend? Something mainstream as React, Vue or Angular, or something more sexy and bleeding edge like Svelte or Solid?
  • Should I use a React framework e.g. Next or Remix? Or just go with React + Vite?
  • Do I need SSR and SSG? Or should I just stick with CSR?
  • What should I use as an API layer? Good ol' REST, or maybe GraphQL, or maybe even typesafe RPC?
  • What to choose for the backend? Do I use something lightweight like Express.js with Node/Bun/Deno or a full-blown solution such as Nest.js/Django/Rails? Or maybe finally try Phoenix/Livewire combo everybody has been talking about? Do I go serverless or not?
  • What about the database and ORM? Relational or non-relational? Should I write raw queries or use a full-blown ORM such as Drizzle and Prisma? If yes, which one?
  • What are my hosting options? Am I going to get locked in with a single provider? What if I want/need to host my app somewhere else?

These are just some of the questions you need to start thinking about when deciding how to start your SaaS in 2024. As you can see, it's more then enough to make your head spin and even if you're a seasoned developer and makes you feel like you need to be a rocket scientist to figure out the right combination.

This is why people today turn to SaaS boilerplate starters and gladly even pay for it. It means somebody else did the legwork and (hopefully) made a sensible decision on the stack which will remain current and easy to maintain in the years to come.

Now that we gave some context to the sudden rise of SaaS starters, let's back to the original question - why pay for something when there is an open-source, free version of it? Let's take a look at some of the factors that come to play.

With an open-source SaaS starter, you know exactly what you're getting into

By the definition of open-source, you can see and examine the full code of the starter in advance, before committing to using it for your project.

Although it's not likely you will go through every line of code beforehand and try to understand it all (that's why you're looking for a starter in the first place), you can check it out and see how you like it - e.g. the style of the code, readability and how well documented and tested it is.

You can also see the repository's activity stats - number of open and closed issues, features in progress, commit frequency and how fast are things being resolved and new features added.

Paid, closed-source starters, again by definition, offer at best a fraction of these benefits. You can see the value proposition as the author designed it - some hand-picked testimonials, a demo and potentially have a peek at the docs.

With a paid starter, you become a member of an exclusive tribe (aka Air Jordans Effect)

The most popular paid boilerplates today often come from well-known developers, or “indie makers,” who've already built successful products. Buying their boilerplate feels a bit like joining an exclusive club—it's as if you're tapping into their expertise and using the same tools they once used to succeed.

It's like wearing a jersey signed by a famous athlete or a perfume co-created by a pop star. It won't guarantee instant success, but it gives you a sense of connection and inspiration. You're reminded that someone else turned these same tools into something great—and that you could do it too!

In the long run, this mindset might matter even more than the tools themselves. When things get hard, feeling part of that “club” could be what keeps you going, and taking your idea one step further.

Security: in open-source, everyone is a reviewer

Paid boilerplate starters are mostly an effort of a single person. It is the type of project that, past the initial development phase, doesn't require a full-time attention and is more of a seasonal nature (e.g. updating libraries to the latest versions). That makes it a perfect workload for a single person and also makes it much more profitable rather than splitting the margin with the team. If there was a whole team behind, it probably wouldn't cost $500, but rather $2000.

Recently, there was a security incident with one of the popular paid starters that allowed external parties to send unauthorized web hook requests, which caused a lot of ripples in the online community of builders.

It is a good reminder that, while it's important to ship quickly, security isn't something that can be skipped over. And while nobody can guarantee the security of any SaaS starter, be it paid or open-source, the fact is that in open-source projects there are much more people involved in both development and code review. Since the code is freely available, you're also free to review it yourself, use any pen-testing tools on it or ask another expert to check it before committing to it.

With a paid SaaS starter, the bus factor is 1, with open-source you get the full community support!

A paid SaaS starter typically depends on a single maintainer. Since the code is closed source, nobody else has access nor rights to it, and if for any reason the author becomes unable or unwilling to continue working on it, that's the end of the story. No support, updates, nor anybody to turn to with questions.

On the other hand, an open-source boilerplate starter like Open SaaS is a living organism, with a number of contributors behind it. As with any open-source project, there will typically be a smaller core team which does the bulk of the work and steers the project (and that might as well be a single person in the start), but anybody can join at any point, and they will. As the project grows and becomes more used, more and more people will start adding fixes and features they need themselves and take ownership of the specific parts.

Another thing to account for is it takes a long time for SaaS starter business to become more than a side income, and only a fraction of builders will ever come to that point. That means most of boilerplate creators will still have a full-time job or other engagements going on. Which means they will have a limited time for customer support and adding new features.

Open-source SaaS starter === unlimited updates. Closed source? Sometimes.

An another direct benefit of the SaaS starter code being open-source is that you will have an immediate access to all the updates, as soon as they get released. That includes both security patches, version bumps and completely new features.

With closed source, it varies a lot from one starter to another. Some offer updates as an upsell (e.g. basic and pro tier), some offer a limited time updates (e.g. 1-year), and some promise a lifetime of updates.

With a paid SaaS starter, you might need to buy a “license” for every new app

Another thing to be aware of is that, with paid starters, there often might be a limit to the number of apps you are allowed to start with a single starter purchase. It is typically phrased in terms of “licenses”, and if you exceed a limit you're legally required to buy a new one, although you already own the starter code.

Again, this is not the case with all paid starters (some offer unlimited projects with a single purchase), but it is a common pattern worth checking before buying.

With an open-source starter, there naturally isn't any such limit - the full source code is publicly available and you're free to use it in any way you see fit.

With an open-source SaaS starter, you can add new features yourself!

One of the most exciting benefits of the open-source approach is that anybody can contribute! If there is a feature you're missing or want to improve, you can simply do it yourself it and create a pull request. Then, the core maintainers will review it, give advice and point you in the right direction if needed. Once it gets merged, it is available for everyone to use!

Summary

Now that we have gone through the main differences between open-source and paid SaaS starters, let's give it a bird's-eye view:

Cost Lifetime updates Unlimited apps Maintainers Community Air Jordans Effect Easily contribute
Open-source SaaS starter $0 YES YES Many Big, public Rarely YES
Paid starter $300+ Depends Depends Typically one Sometimes, private Often No

This is a useful list to be aware of when making a decision which route to go, but in the end there is no one answer that will fit all. Your decision will depend on what exactly you're looking to build and which tech stack you prefer using.

Also, the factors above will not be equally weighted by everyone - one person might be excited about being a part of a wider community and being able to easily contribute to the project, while other most appreciate the fact there is a strong online personality they can follow and get inspired.

In the end, the only important thing is to take action and successfully ship that application you've been thinking about for so long. Good luck!


Opensaas Sh Blog Src Content Docs Blog 2024 12 10 Turboreel Os Ai Video Generator Built With Open Saas (`opensaas-sh/blog/src/content/docs/blog/2024-12-10-turboreel-os-ai-video-generator-built-with-open-saas.mdx`)


title: "TurboReel: An Open Source AI Video Generator Built With Open SaaS"
date: 2024-12-10
tags:

  • webdev
  • saas
  • sideproject
  • indiehackers
    authors: milica

import { Image } from 'astro:assets';
import landing from '../../../assets/turboreel/landing.webp';
import opensaas from '../../../assets/turboreel/opensaas.mp4';
import reddit100Users from '../../../assets/turboreel/reddit-100-users.webp';
import reddit200Upvotes from '../../../assets/turboreel/reddit-200-upvotes.webp';
import studioInterface from '../../../assets/turboreel/studio-interface.mp4';
import VideoPlayer from '../../../components/VideoPlayer.astro';

Peter is the creator of TurboReel, an open-source platform with a paid SaaS layer, that transforms how creators generate short-form video content. With just a prompt, users can produce polished TikToks and YouTube Shorts in moments.

But like any SaaS founder, Peter faced the challenge of turning his vision into reality without getting bogged down in repetitive technical setup. That's where Wasp's Open SaaS boilerplate came in.

In this post, we'll cover three main things: what inspired Peter to kickstart the project, how he chose the tech stack to build on, and finally, how he made his first $100. Let's dive in!

The Starting Point: Open SaaS Boilerplate

Peter's journey to Open SaaS began with a simple Google search for SaaS boilerplates.

"I was looking for something that could save me time," Peter recalls. "I came across a few options—some were free but basic, and others were paid but didn't feel worth it. Then I found Wasp's Open SaaS boilerplate."

What stood out to Peter wasn't just that it was free, but that it was open source. "I liked the idea of building on something maintained by a community, not locked behind a paywall", he says. Intrigued, Peter explored Wasp further and discovered an engaging community that offered exactly what he needed to start building TurboReel.

Here's a video presenting Open SaaS, generated with TurboReel 🐝

TurboReel's Tech Stack

TurboReel lets users generate short explainer videos with minimal effort. Starting with a single text prompt describing the video's purpose (e.g. “Create a video on building your SaaS with Open SaaS”), you can produce professional grade TikTok and YT shorts without needing any video editing skills.

The platform's open-source foundation unlocks development potential, while the paid SaaS layer helps with funding.

The tech behind TurboReel looks like this:

  • Open SaaS - a free, open-source React & Node.js SaaS starter
    • powered by Wasp, a full stack web framework for JS.
    • Combines React for the frontend and Node.js for backend.
    • Prisma handles the database.
  • OpenAI
    • Used for generating scripts and scenes in the videos.
  • Pollinations
    • Open-source platform for image and text generation.
    • Provides creative assets to enhance video quality.
  • Revideo
    • Library for programmatic video creation.
    • Replaces the previously used MoviePy.

Building faster with Open SaaS boilerplate

"The first thing that impressed me with Open SaaS was how much time it saved, I could start with wasp new saas and immediately have a functioning boilerplate. It gave me the foundation I needed to focus on my product, not the setup."

The boilerplate included everything he needed:

  • Authentication via email, GitHub and Google
  • Running background jobs via pg-boss
  • Database management
  • Frontend-backend communication via a type-safe RPC layer
  • Deployment of the app with a single CLI command

One feature that particularly stood out was Wasp's deployment commands.

"Usually, deployment takes time to set up properly, but with Wasp, it was as simple as running wasp deploy fly deploy."

Here's what Wasp's config file looks like, through which you can define full-stack auth in a Wasp app.

bash
app myApp {
  wasp: {
    version: "^0.15.0"
  },
  title: "My App",
  auth: {
    // 1. Specify the User entity
    userEntity: User,
    methods: {
      // 2. Enable Github Auth
      gitHub: {},
      email: {
        // 3. Specify the email from field
        fromField: {
          name: "My App Postman",
          email: "[email protected]"
        },
        // 4. Specify the email verification and password reset options
        emailVerification: {
          clientRoute: EmailVerificationRoute
        },
        passwordReset: {
          clientRoute: PasswordResetRoute
        },
      },
    },
    onAuthFailedRedirectTo: "/login"
  },
}

⭐️ Star Open SaaS repo and support tools that help you build fast!

Out-of-the-box Stripe integration

Another significant advantage for Peter was how Open SaaS handled third-party integrations. Setting up services like Stripe for payments often requires a lot of effort, but Wasp's Open SaaS streamlined the process - you just need to add your API key and you're good to go.

"Payments are usually a huge headache, but Open SaaS made it so smooth. I didn't have to spend weeks integrating Stripe—it just worked. That gave me more time to focus on TurboReel's core functionality."

The power of open source

Both TurboReel and Wasp share a commitment to open source.

"The video generation space is complex. There aren't many established solutions for what I'm trying to do. By making TurboReel open source, I'm inviting smart people to collaborate and help push the project forward."

Getting first users

Peter found interesting subreddits on Reddit and shared his product with users. He enabled everyone to sign up and create a few videos, to get feedback quite early. Lots of people in the creator community loved it, and based off of their feedback, he iterated furthermore improving the UI and the workflow.

Within a few days, he was able to get first paying customers, which proved that his MVP was going in the right direction. Plans for the future? The sky is the limit!

Ready to Build Your SaaS?

Get started with Wasp today, or explore the Open SaaS boilerplate to see how it can work for you.


Opensaas Sh Blog Src Content Docs Blog 2024 12 16 My Gpt Wrapper (`opensaas-sh/blog/src/content/docs/blog/2024-12-16-my-gpt-wrapper.mdx`)


title: "My GPT Wrapper Makes $550 MRR One Year After Launch"
date: 2024-12-16
tags:

  • gpt
  • saas
  • sideproject
  • indiehacker
    authors: vince

import { Image } from 'astro:assets';
import Tweet from '../../../components/Tweet.astro';
import landingPage from '../../../assets/cover-letter-gpt/coverlettergpt.webp';
import mrrGraph from '../../../assets/cover-letter-gpt/mrr-graph.webp';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import redditPost from '../../../assets/cover-letter-gpt/coverlettergpt-reddit.png';

Hey builders,

I wanted to share my journey building a micro-SaaS, CoverLetterGPT, which now earns $550/month in recurring revenue (MRR)—all while requiring minimal effort and maintenance. Here's how I did it and why I believe small, simple SaaS apps are an underrated way to start as an indie maker.

Quick Stats:

  • Built in 1 week
    • using Wasp, a React, NodeJS, & Prisma framework
    • and Chakra UI for the design system.
  • Runs on autopilot
  • ~$550 MRR after one year
  • Minimal customer support—only 3 Stripe disputes to date
  • Costs ~$15/month to operate (hosting + OpenAI API fees)
  • Deployed on Railway & Netlify

BTW, I built Open SaaS, the free, open-source SaaS template based on what I learned from building and launching CoverLetterGPT.

Because Open SaaS is a community-driven project, it also benefits from community feedback and contributions, so it boasts a clean codebase and a ton of useful features. Check it out and give us a star! We're always improving it.

Small Wins Are Worth It

Many developers think a SaaS has to be big, flashy, or wildly profitable to be worth building. I disagree. For me:

  • $550/month is fantastic as side income.
  • It runs itself, requiring virtually no maintenance.
  • I can balance it easily alongside my full-time job.
  • It's fun and doesn't consume my free time.

Here's why I think you should aim for small, achievable SaaS projects instead of trying to “hit it big” from the start.

Build & Launch Fast

The most important lesson I've learned: speed is everything. The faster you launch, the faster you'll know if your idea works. Here's what worked for me:

  1. Avoid long, drawn-out failures: Build small, execute early.
  2. Use the fastest tools available: I used Wasp because it gives me all the building blocks already set up (auth, database, cron jobs, email sending), letting me focus on the business logic of the app. Paired with Chakra UI, I was able to build the app in about 1 week.
  3. Forget perfection: I didn't worry about making it pretty or perfect—it just had to work.

Keep It Simple

The beauty of micro-SaaS is in its simplicity. Here's why:

  • My app does one thing well: generating cover letters based on résumés and job descriptions.
  • There's no need for a fancy landing page or marketing gimmicks. This is my 🌶 hot take.
  • Users get 3 trial credits—enough to try the app and see value before paying.

One of the biggest perks of micro-SaaS is how low-maintenance it can be. With CoverLetterGPT, I rarely handle customer service thanks to its simplicity.

This means I spend my time on new ideas rather than maintaining old ones.

It's All About Tradeoffs

While I could optimize and grow CoverLetterGPT further, I've chosen to keep it small and simple. For me:

  • Small wins are still wins.
  • I value having a side project that's easy to manage alongside my full-time job.
  • I'd rather have less stress than chase higher profits.

If you're thinking about launching your own SaaS, here are some helpful resources:

Final Thoughts

If you're considering building a SaaS, don't overthink it. Start small, move fast, and treat it as an experiment. Forget the “rules” and focus on launching. Here's what matters most:

  • Keep it simple: Build an app that solves one problem well.
  • Launch fast: Test your idea and iterate based on real feedback.
  • Minimize effort: Aim for maximum reward with minimal maintenance.

For me, $550 MRR isn't just “enough”—it's amazing. It's proof that small, focused apps can succeed, and they're a great way to build confidence and skills as a maker.

Let's stay in touch!

If you found this helpful, check me out on Twitter where I like to build in public and share what I'm learning and building.



Opensaas Sh Blog Src Content Docs Blog 2025 01 30 From 0 To 400 Customers Saas Growth Hacks (`opensaas-sh/blog/src/content/docs/blog/2025-01-30-from-0-to-400-customers-saas-growth-hacks.mdx`)


title: "From 0 to 400+ Customers: SaaS Growth Hacks from a Serial Founder"
date: 2025-01-30
tags:

  • webdev
  • saas
  • sideproject
  • indiehackers
    authors: milica

import { Image } from 'astro:assets';
import articleGeneration from '../../../assets/ricardo-growth-hacks/article-generation.png';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import meetingReminders from '../../../assets/ricardo-growth-hacks/meeting-reminders.png';
import googleAddons from '../../../assets/ricardo-growth-hacks/google-addons.png';

Meet Ricardo - he has successfully launched multiple SaaS products, turning his ideas into revenue-generating apps. If you're looking to build and launch your own product efficiently, we're about to share some of Ricardo's key strategies.

He's a developer with a background in telecom engineering, having held leadership roles at companies like Vodafone and Glovo. But after years of putting fires out in management, he returned to hands-on development, focusing on building apps that solve real problems—fast.

By leveraging Open SaaS, Ricardo was able to ship multiple projects quickly, skipping the usual headaches of setting up authentication, payments, and other things every SaaS needs. In this post, you'll discover what types of SaaS products he launched and the strategies he used to get them off the ground.

Why Ricardo Chose Open SaaS

When searching for frameworks to kickstart his projects, Ricardo stumbled upon Open SaaS, a 100% free, open-source starter for React & Node.js. and. He was drawn to Open SaaS because of its simplicity, community, and modern tech stack. He also liked the fact that the company had Y Combinator seal of approval.

"The fact that Wasp is low-friction and uses a great stack like Prisma, React, Node.js, and TypeScript—made it stand out. Plus, the community is super helpful. You can get started fast without spending hours on setup."

What Ricardo loves most:

  • Pre-built Features: Open SaaS relies on Wasp - a full stack framework for React, Node.js and Prisma. The way Wasp handles routes and authentication was a game-changer.

"Just putting routes in main.wasp makes everything super simple. Auth works seamlessly, too."

  • Focus on Building: By handling repetitive setup tasks like setting up payment integrations or making admin dashboards, Open SaaS allowed Ricardo to focus on core features.
  • Adaptability: regardless of the idea he had - a full-fledged SaaS, or a Google add-on which needed a robust-backend and a dashboard, he was able to build the app with Open SaaS boilerplate starter.

"I didn't feel limited by the boilerplate—it's flexible and gets out of the way."

Ricardo's Projects Built with Wasp

Ricardo started a few projects with Wasp, while working on the third one he started before discovering Open SaaS.

**Article Generator**

  • Built in less than 7 days.
  • 40+ paying customers.

This tool simplifies content creation for businesses by generating SEO-friendly blog posts with AI. Article Generator is competing in a crowded market of AI writing tools, where each tool claims that it's the best one on the market.

Ricardo is using Open SaaS to focus on feature development while testing pricing strategies to differentiate the product from the rest of the market. Integrations with Stripe, Open AI, and similar helped him move faster than he could on his own. His first clients came from Reddit and he has a standard subscription monetization set up.

**Meeting Reminders**

  • Bult in less than 7 days.
  • 400+ paying customers.

This tool is a Google Workspace add-on that reduces no-shows by automating pre-meeting reminders. His competitive edge is that he covers WhatsApp alongside SMS and email reminders. Meeting Reminders app shows how versatile Open SaaS boilerplate is, because it can handle edge cases like this one and integrate into Google's system.

Calls being skipped was a huge pain for Ricardo when he was working at a VC company. His day would include a lot of calls, and the cancellation rate was high. Once he started emailing participants before the call, the number of cancellations reduced significantly. Some time later, he built the tool himself to automate this 😃

In this case, Open SaaS handles backend tasks like subscription checks and authentication. Because of that, this is a lightweight app that solves a niche problem effectively, and doesn't require a lot of maintenance.

The first users were people he knew personally, and he did a bit of promotion on targeted groups on Slack and Discord. Since it's a Google Marketplace app, anyone looking for Meeting Reminder add-on will have a chance to see it.

He also relies on SEO, and guess what, he pushed a couple of blog posts with his first SaaS, AI Article Generator. As he said before, you should make tools that scratch your itch first. 😃

Tips for Builders Launching Products

  1. Validate Before You Build

"Start by searching Reddit or similar platforms to find out if people are already solving the problem. If they are, ask yourself: can I do it better or faster?"

  1. Diversify Launch Strategies
  • Avoid relying solely on Product Hunt

"It's not as effective as it used to be."

  • Explore short-form content like TikTok for quick validation. You can create a company account and post videos that showcase the problem and the solution.
@meetingreminders BOOOM!! no more waiting in meetings - it's called Meeting Reminders #workmeeting #corporate #workfromhome #googlemeet ♬ original sound - Meeting Reminders

"Their algorithm is great for targeting the right audience."

  • Use targeted Reddit ads to reach niche communities.
  1. Start small

"If you're entering a competitive space, start small. Validate your product's unique edge by solving specific pain points and adjust based on user feedback."

  1. Iterate Quickly

"Launch fast, gather feedback, and refine your product. You don't need to build the perfect app on day one—get it out there, see how people use it, and adjust."

**Ready to Build Your SaaS?**

Explore the Open SaaS boilerplate to see how you can kickstart your SaaS today.


Opensaas Sh Blog Src Content Docs Blog 2025 02 26 Incident Report Vulnerability In Open Saas (`opensaas-sh/blog/src/content/docs/blog/2025-02-26-incident-report-vulnerability-in-open-saas.mdx`)


title: "Incident Report: Security Vulnerability in Open SaaS "updateCurrentUser" function"
date: 2025-02-26
tags:

  • incident-report
    authors: martin

import { Image } from 'astro:assets';

On Feb 12th, 2025, we learned about a security vulnerability in the updateCurrentUser function of our Open SaaS template. Users of apps built with Open SaaS can exploit this vulnerability to edit any field in their own User database record, including fields they weren't supposed to have write permissions for, like subscriptionPlan and isAdmin.

If you created your app with the Open SaaS template before Feb 14th of '25, your app potentially suffers from this vulnerability, and you should apply the fix from this report as soon as possible. Check out vulnerability and fix sections.

The vulnerability does not affect the "vanilla" Wasp apps (Wasp apps not built with Open SaaS template) or those that modified the problematic part of the code enough to eliminate the problem.

Since then we fixed the vulnerability in all the versions of the Open SaaS template, did a coordinated vulnerability disclosure (culminating with this report) with the suggested fix, reviewed all the other templates and example apps of ours for similar security vulnerabilities, analyzed what at the first place enabled such omission to happen on our side, and prepared a plan on how to minimize the chance of similar mistakes happening in the future.

We sincerely apologize for the impact and inconvenience caused by our mistake. Caring about code quality is at the center of our culture here at Wasp, but in this instance, we failed to follow up on our standards. We are deeply disappointed by it and will ensure we learn from it, improve, and regain your trust in the code we ship, especially as Wasp is heading from Beta toward 1.0.

The vulnerability

The vulnerability is caused by the updateCurrentUser function in src/user/operations.ts (or in src/server/actions.ts if you used an older version of Open SaaS):

tsx
export const updateCurrentUser: UpdateCurrentUser<Partial<User>, User> = async (user, context) => {
  if (!context.user) {
    throw new HttpError(401);
  }

  return context.entities.User.update({
    where: {
      id: context.user.id,
    },
    data: user,  // <- This is the problem!
  });
};

While this Wasp Action correctly allows the user to modify only data in their own User database record, and not of other users, it does also allow them to potentially change ANY of the fields on their own User db model, including fields like credits, subscriptionPlan, and isAdmin, due to data: user line in User.update() call. Particularly troublesome is the ability to set isAdmin to true, as it gives them further privileges they shouldn't have.

An example of how a bad actor could exploit this is by creating a user account in your app, obtaining their own auth credentials via browser dev tools, and then sending a modified request to the HTTP route of updateCurrentUser Wasp Action with a payload that sets the isAdmin field to true for themselves.

The fix

The fix consists of three main steps:

  1. Refactor updateCurrentUser function to updateCurrentUserLastActiveTimestamp
  2. Implement additional Wasp Action(s) for updating user data if needed
  3. Refactor updateUserById function to updateUserIsAdminById

Refactor `updateCurrentUser` to `updateCurrentUserLastActiveTimestamp`

In the Open SaaS template, as it comes when you create a new project with it, the Wasp Action updateCurrentUser isn't used for anything else but updating the lastActiveTimestamp field on the User model, despite its general nature. Therefore, we recommend the following fix:

  1. Rename the operation updateCurrentUser to updateCurrentUserLastActiveTimestamp. Make sure to update its name in all the places: main.wasp, client code (i.e. src/client/App.tsx), server code (i.e. src/user/operations.ts).

  2. Rewrite the operation updateCurrentUserLastActiveTimestamp in src/user/operations.ts so it receives no arguments and only updates the lastActiveTimestamp field on the User:

    tsx
    export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTimestamp<void, User> = async (_args, context) => {
      if (!context.user) {
        throw new HttpError(401);
      }
    
      return context.entities.User.update({
        where: {
          id: context.user.id,
        },
        data: {
          lastActiveTimestamp: new Date(),
        },
      });
    };

    Notice that also the name of the type of the operation changed, so you will want to update the type import, and we also changed the operation's Input type to void.

  3. Remove all arguments from the call to updateCurrentUserLastActiveTimestamp in src/client/App.tsx:

    tsx
    if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) {
            updateCurrentUserLastActiveTimestamp(); // <- no args anymore
          }

Implement additional Wasp Action(s) for updating user data if needed

If you were using updateCurrentUser in your code beyond just updating lastActiveTimestamp, to allow the user to update some other User fields, we recommend also defining additional, more specialized Wasp Action(s) that will handle this additional usage.

For example, let's say that in your app you additionally defined fullName and address fields on the User model, and you were using updateCurrentUser to allow the user to update those. In that case, we recommend defining an additional Wasp Action called updateCurrentUserPersonalData. It could look something like this:

tsx
export const updateCurrentUserPersonalData: UpdateCurrentUserPersonalData<Pick<User, "fullName" | "address">, User> = async (personalData, context) => {
  if (!context.user) {
    throw new HttpError(401);
  }

  // NOTE: This is also a good place to do data validation if you want to.
  const fullName = personalData.fullName
  const address = personalData.address

  return context.entities.User.update({
    where: {
      id: context.user.id,
    },
    data: { fullName, address }
  });
};

Refactor `updateUserById` to `updateUserIsAdminById`

Finally, while not a security vulnerability, we also recommend updating the related Wasp Action, updateUserById (you can find it next to where the updateCurrentUser function was), in a similar fashion, to ensure it can't do more than we need it to:

  1. Rename from updateUserById to updateUserIsAdminById.
  2. Rewrite updateUserIsAdminById to only allow setting the isAdmin field:
tsx
export const updateUserIsAdminById: UpdateUserIsAdminById<{ id: User['id'], isAdmin: User['isAdmin'] }, User> = async ({ id, isAdmin }, context) => {
  if (!context.user) {
    throw new HttpError(401);
  }
  if (!context.user.isAdmin) {
    throw new HttpError(403);
  }
  if (id === undefined || isAdmin === undefined) {
    throw new HttpError(400);
  }

  return context.entities.User.update({
    where: { id },
    data: { isAdmin },
  });
};

Notice that we modified the shape of the operation input (now it is { id, isAdmin }), so you will also want to update the calls to this operation accordingly.


Additional reading

This second part of the report is not required reading: all you need to know in order to fix the vulnerability is in the "The vulnerability" and the "The fix" sections. But, if you want to learn more about what caused the vulnerability, how we handled it, and what are we doing to prevent similar mistakes from happening in the future, read on!

Coordinated vulnerability disclosure

The challenging part about handling a security vulnerability like this one is that we have to make the knowledge of it public so that all the people with affected apps learn about it and how to fix it, but then at the same time we are also making that knowledge easily available to any bad actors that might want to try to exploit it.

One of the popular approaches is coordinated vulnerability disclosure and it is also what we chose to follow in this instance. We decided to disclose the vulnerability in stages, with 1-week pauses in between:

  1. Stage 1 (private disclosure): We assembled a list of everybody we knew was building and deploying apps with Wasp / Open SaaS, be it from our community on Discord, from online search, or from interacting with them in the past. We privately reached out to everybody on the list and shared the details of the vulnerability and the fix, while also asking them to keep it confidential till we go public with it.
  2. Stage 2 (community disclosure): About a week later, we shared the details of the vulnerability in our Discord community, while again asking people not to share it publicly till we go public with it.
  3. Stage 3 (public disclosure): Finally, a week after the Stage 2, we shared the vulnerability fully publicly.

How did this happen?

TL;DR: Failure in our code review process.

At Wasp, we care a lot about code quality, the code review process, and software craftsmanship in general. PRs get thoroughly reviewed, we do our best to write Clean Code (with a grain of salt), we think a lot about naming, we produce RFCs for any complex feature, our codebase is strongly typed (Haskell and TypeScript), we keep track of all the issues and ideas publicly on our GitHub to not lose sight of them and to also get community input and be transparent.

Commitment to these practices does get tested regularly: Wasp is moving fast and is changing a lot since it is still pre-1.0, so there is always more tech debt going on than one would like, but we always felt like we managed to stay on the good side of our commitment to these practices: they enable us to be efficient but also to enjoy and be proud of our work.

So what happened then, what enabled this vulnerability in Open SaaS?

Open SaaS started as a one-person experiment, a simple template/boilerplate starter for Wasp, so we didn't do the usual thorough code reviewing of every PR at the very start but thought we would do it fully later, once it shaped up a bit. Also, it is just a template, not a library/framework, people can read/modify the code as they like.

Open SaaS did shape up, and not only have people started using it, but it really picked up, more than we ever expected, and we were getting a lot of positive and constructive feedback, feature requests, ideas, bug reports, … . We started reviewing all the new code thoroughly, but we still haven't done the full retroactive review. We have done some of it, for parts of more sensitive modules, and some of it happened naturally through refactoring, but we haven't done it systematically for the whole codebase. We would discuss during every quarterly planning how we should do it this quarter, but there was always something with a higher priority, especially on the Wasp side, and Open SaaS was doing great, if there was anything serious, we would already know about it, we thought.

And then we learned about a function in our codebase that allows a user to set any data, without runtime validation, as a partial update for their User record in the database. This function was barely even used in the Open SaaS codebase at this point: it was used only to update a single field in the User database model, and even that usage should have been refactored into something better already. This function was an obvious code smell, but we never reviewed it properly.

The fact is, we never should have made Open SaaS publicly available without doing a full code review of it first. Once the code is out there, be it just an example app, a template, or a library, we can't guess how it or its usage will evolve, or how will our priorities evolve. Once an exception in the (review) process is made, it is much harder to find the time to catch up on it, than if we did it when we should have done it in the first place.

What we are doing to prevent similar mistakes

  • No code/documentation goes public without a thorough review.
    We have been doing this from the very start for the Wasp framework codebase, but we were more lenient with the templates and example apps. From now on, there will be no exceptions.
  • We checked all our existing templates and example apps for vulnerabilities.
  • We have done a thorough review of the Open SaaS template codebase.
    We have already merged a lot of code quality improvements based on it, and we are in the process of merging the rest.
  • We will make it harder at the Wasp framework level to make a similar mistake.
    The mistake of passing unvalidated/unparsed data is too easy to make - we will, latest for Wasp 1.0, enforce runtime data validation in Wasp, for Operations, APIs, and other externally facing methods. We also have good ideas for advanced access control support in Wasp, which should further make it harder to make these kinds of mistakes.

Timeline

What follows is the timeline of the actions we have taken since we learned about the vulnerability, in order to minimize its impact:

  • Feb 12th, 2025 (Wed), 10 pm CET: we learned about the vulnerability (thanks Ivan Vlahov!)
  • Feb 13th (Thu):
    • Made an action plan on how to handle the incident, including how we will execute the coordinated disclosure.
    • Fixed all the versions of the Open SaaS template, to prevent new projects from being affected.
  • Feb 14th (Fri):
    • Wrote the "Incident Notification" document with a detailed explanation of the problem and the suggested fix.
    • Compiled a list of the people we know are deploying Open SaaS / Wasp apps and privately shared the "Incident Notification" document with them, giving them ~ a week of head start before we go more public with the incident.
    • Reviewed all the other Wasp templates and example apps for similar security issues.
    • Started a deep (re)review of all the Open SaaS code (that will continue into the next week).
  • Feb 17th (Mon):
    • Continued deep review of Open SaaS code.
  • Feb 18th (Tue):
    • Continued deep review of Open SaaS code.
    • Finalized first draft of this Incident Report document.
  • Feb 19th (Wed):
    • Continued deep review of Open SaaS code.
  • Feb 20th (Thu):
    • Continued deep review of Open SaaS code.
    • Notified our Discord community about the incident by sharing the "Incident Notification" document with them, giving them a week of head start before we go fully public with the incident.
  • Feb 21st (Fri):
    • Finalized the deep review of the Open SaaS code (while continuing with the code improvements).
  • Feb 26th (Wed):
    • Went public with the incident by publishing and sharing this Incident Report.

Opensaas Sh Blog Src Content Docs Blog 2025 02 27 Meet Marko Saric Co Founder Of Privacy Friendly Plausible Analytics (`opensaas-sh/blog/src/content/docs/blog/2025-02-27-meet-marko-saric-co-founder-of-privacy-friendly-plausible-analytics.mdx`)


title: "Meet Marko Saric, Co-founder of Privacy-friendly Plausible Analytics"
date: 2025-02-27
tags:

  • webdev
  • saas
    authors: milica

import { Image } from 'astro:assets';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import plausibleCommunity from '../../../assets/plausible/plausible-community.png';

In this interview, Marko Saric shared his thoughts on privacy and running a bootstrapped SaaS business. Plausible integration is already available in Open SaaS as a privacy-friendly alternative to Google Analytics. We hope this interview helps you understand the value of such a product, and the nature of running an open source business.

Here's a few other things we've covered in this interview:

  • Tackling big tech privacy issues.
  • How bootstrapping your business fuels independence and transparency.
  • Real, practical advice for growing your SaaS the smart way.

Let's dive in!

Can you share a bit about your background and what led you to start Plausible?

I'm Marko Saric, co-founder of Plausible Analytics.

My journey with Plausible began with a growing awareness of the privacy issues surrounding Google and its products. For many years, I was a user of Google's services but over time (and thanks to Snowden, Cambridge Analytica and other privacy scandals), I became more aware of the negative aspects of surveillance capitalism. This led me to explore better, more ethical alternatives to the big tech products.

I started sharing these alternatives on my blog which is how I connected with my co-founder Uku. We both had experience in tech and a shared vision of working on a privacy-friendly analytics tool so we decided to work together on Plausible. I'm focused on marketing and communication side of things while Uku is focused on design and development.

For those unfamiliar with Plausible, how would you describe its core mission in just a few sentences?

Plausible Analytics is an easy to use, lightweight, open source and privacy-friendly analytics tool. Our mission is to provide website owners with useful insights while respecting visitor privacy.

We have been working on Plausible for more than 6 years now, have more than 14,000 active subscribers at this point and have counted more than 136 billion pageviews so far.

Here's an interactive demo of Plausible Analytics:

Plausible is bootstrapped and open-source—what made you choose this path instead of taking the more common VC route?

We chose to bootstrap and open source Plausible because we wanted to maintain control and independence while also being more privacy-friendly and transparent.

Both of us have worked at venture funded startups in the past and neither of us had good experiences with investors so going bootstrapped was pretty much the way to do this if we wanted to do things our way.

We're in the privacy niche so open sourcing our product allows us to build trust as people can inspect our code to verify that our actions match our words. People cannot do that with Google Analytics and other competing products.

Just like Plausible, Wasp is an open-source project too! We'd appreciate it if you could star Wasp on GitHub as a sign of support! ⭐️

Do you have any advice for people who are considering bootstrapping their company? Do you have any books or podcasts to recommend?

I think it's a good idea to start bootstrapped even if you do wish to get funded. You should focus on creating a great product that solves a real problem and on spreading the word about it. If you do that well, you'll have investors reaching out to you even if you don't want or need them.

I recommend reading "Rework" by Jason Fried and David Heinemeier Hansson. It offers unconventional but valuable insights into running a startup.

Another good book is "This Is Marketing" by Seth Godin. It's about how many startups confuse marketing with spending money on advertising, spamming, interrupting, being annoying and other hacks and tricks. That's not marketing. Marketing is communication.

How did you get your first customers?

Our first customers came through community engagement and the "build in public" movement. We shared our journey, steps taken and product development openly on our blog, social media and niche communities such as Indie Hackers. That's how we got the early beta users and some of those became our first subscribers too.

What were the biggest challenges you faced while building and growing Plausible?

The first year was pretty challenging in terms of growth. Uku was working alone on Plausible trying to do both development and marketing. This is pretty much an impossible task. The growth was very slow and we made it to about 100 subscribers and $400 MRR some 14 months into the existence of Plausible.

That's when Uku decided to look for a marketing co-founder and that's how we found me. Being two co-founders helped us put more time and effort into marketing and communication. One of the first things we did when I joined was to change our positioning to make it crystal clear and easy to understand what we do, what we stand for and how we compare to Google Analytics (the biggest name in our market). And then we started publishing educational and informative content covering topics such as privacy, open source, bootstrapping and startup marketing .

I have written more about the changes we made in these early days in this post.

Which growth strategies have been the most effective?

We have a boring marketing strategy and we say no to all the growth hacks and other best marketing practices. Content marketing has been our most effective growth strategy. As an example, the first blog post that I published (Why You Should Remove Google Analytics from Your Site) went viral on Hacker News. It drove some good traffic to our site leading to an increase in brand awareness.

What matters is doing quality work and staying consistent with it over a longer period of time so we continued to publish multiple blog posts per week for over a year. Thanks to that work, we've been fortunate enough to achieve the viral moments on Hacker News multiple times over those first 2-3 years.

I have shared more about our early years, marketing steps we've taken, lessons we've learned and things we have achieved in blog posts such as this one. Our analytics dashboard is open to the public so it's possible to see the progress we've made since day one in our stats.

What role has the community played in Plausible's growth? Have there been any surprising or particularly impactful contributions from the community?

The community has helped shape our product and spread the word about our mission.

We have an open roadmap and listen to the product feedback which determines our development prioritization. This is where feature requests and other feedback is very valuable to us. We pretty much pick the most upvoted feature and work on that.

As mentioned earlier, we don't do any traditional marketing as in we don't do any paid advertising nor pay anyone to recommend Plausible. This means that most of our growth comes from people who love using Plausible and who share their experiences with the world. Without people spreading the word about Plausible it would be difficult for us to do what we do. So that's why community contributions is vital for us.

What's next for Plausible? Are there any upcoming features or improvements you're particularly excited about?

We're focused on continuing to improve Plausible and making it even more useful and competitive while staying true to our mission and meeting rigorous standards for stability, security and privacy.

Our developers are currently working on the top two most upvoted feature requests from our public feedback board (scroll depth and saved segments) so that's very exciting. It would be great to release these two big features soon!


Just like Plausible, Wasp is an open-source project too! We'd appreciate it if you could star Wasp on GitHub as a sign of support! ⭐️


Opensaas Sh Blog Src Content Docs Blog 2025 03 12 Going From An Idea To Mvp In Weeks Promptpandas Launches (`opensaas-sh/blog/src/content/docs/blog/2025-03-12-going-from-an-idea-to-mvp-in-weeks-promptpandas-launches.mdx`)


title: "Going from an Idea to MVP in Weeks: PromptPanda's Launch(es)"
date: 2025-03-12
tags:

  • webdev
  • saas
  • sideproject
  • indiehackers
    authors: milica

import { Image } from 'astro:assets';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import plausibleCommunity from '../../../assets/plausible/plausible-community.png';
import interfaceImg from '../../../assets/promptpanda/interface.png';
import meme1 from '../../../assets/promptpanda/meme1.jpg';
import meme2 from '../../../assets/promptpanda/meme2.jpg';
import ph1 from '../../../assets/promptpanda/ph1.png';
import ph2 from '../../../assets/promptpanda/ph2.png';
import listImg from '../../../assets/promptpanda/list.png';

Did you know that most co-founders meet each other through work? Lander Willem met his friend and co-founder Bram Billiet while they were working at the local venture fund. They both shared the love towards LLMs and got the idea to kickstart their SaaS after experiencing the same pain points with managing and versioning prompts.

In this post, you'll learn how they:

  • Shipped their SaaS from idea to MVP in weeks, using modern AI stack
  • Launched and got trending on Product Hunt with 100+ upvotes
  • Successfully onboarded first users

The problem: Managing prompts is messy

Right after OpenAI released their first LLM models, Lander and Bram started exchanging tips on how to get optimal results from prompts. Soon, they learned that managing AI prompts is often chaotic.

People who share prompts usually do so through messaging apps such as Slack, Microsoft Teams or in better cases, shared Google Docs documents. Some of the people they talked to even confessed they were sharing their favorite prompts using screenshots 😅. Although a Google Doc might work initially, people quickly bump into issues regarding versioning and granular access management.

This is how they got the idea to create PromptPanda - a SaaS that allows people to exchange prompts in an easy way. Here's an interactive demo you can click through to see what they've built:

The opportunity: Everyone uses prompts, not just devs

Other AI prompt tools are primarily designed with developers in mind, which leaves out non-technical teams. Those less technical users depend heavily on collaboration, efficiency, and consistency to complete their tasks. This is the market PromptPanda decided to go after.

The tool is designed specifically to help teams centralize their prompts and ensures consistent output quality. Collaboration is painless because of an intuitive web app that also has a Chrome extension.

PromptPanda integrates with major AI providers such as OpenAI, Anthropic, Google, Perplexity, and DeepSeek. Coupled with its built-in Prompt Improver, these integrations allow users to quickly test, iterate, and enhance their prompts, while not imposing any limitations for the end-users.

With this approach they covered a market that other companies overlooked, non-technical users who rely on the biggest LLM providers for their daily tasks.

Launching is unpredictable: Product Hunt hits and flops

As soon as the app was somewhat stable and usable, Lander and Bram decided to launch on ProductHunt.

Their first ProductHunt launch was great in terms of visibility. They were featured by the ProductHunt team which got them a bunch of upvotes and comments. Although there was quite a lot of engagement with the launch, it didn't really end up in sticky, paying customers.

A short while later they relaunched on ProductHunt after processing the feedback from their first launch. Both their product and launch campaign were much better prepared. Weirdly enough, the launch mostly failed as they got almost no upvotes or conversions.

Although their second launch was mostly a flop, it did manage to get them mentioned in a Superhuman (the email app) newsletter. Their user base doubled overnight.

Ever since then they have an active stream of users and new signups coming in.

”My main takeaway is to never stop shipping, and always share your work!”

Lander Willem

Most of their users today have found PromptPanda through organic SEO. They started writing articles about AI Prompt Management which have quickly found traction in search engine algorithms.

Choosing the right stack for developing your SaaS app

PromptPanda's team chose Open SaaS because it significantly streamlined their product development by simplifying backend setup, database management, and built-in authentication. This was crucial as they needed an efficient solution that could save time due to their busy schedules. Wasp's default integration with Fly also enabled rapid deployment, allowing them to quickly validate their product idea without getting bogged down in infrastructure complexities.

Here's a full overview of their tech stack alongside all the tools they rely on to run their SaaS:

Are you ready to ship your SaaS now?

PromptPanda's story proves the best SaaS ideas come from solving your own pain points. Lander and Bram also learned launching isn't predictable—success can come from unexpected places, even failed launches. The takeaway? Keep building, keep shipping, and always share your progress openly.

If you enjoyed this post please make sure to give Open SaaS a star on GitHub, this keeps us going forward and supports our work!


Opensaas Sh Blog Src Content Docs Blog 2025 05 07 You Should Still Launch Your Product On Ph (`opensaas-sh/blog/src/content/docs/blog/2025-05-07-you-should-still-launch-your-product-on-ph.mdx`)


title: "Product Hunt doesn't really work, but you should still use it to launch your product"
date: 2025-05-07
tags:

  • saas
  • webdev
  • sideproject
  • indiehackers
    authors: matija

import VideoPlayer from '../../../components/VideoPlayer.astro';
import { Image } from 'astro:assets';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import ph1 from '../../../assets/ph/1.png';
import ph2 from '../../../assets/ph/2.png';
import ph3 from '../../../assets/ph/3.png';
import ph4 from '../../../assets/ph/4.png';
import ph5 from '../../../assets/ph/5.png';
import ph6 from '../../../assets/ph/6.gif';
import ph7 from '../../../assets/ph/7.png';
import ph8 from '../../../assets/ph/8.png';
import ph9 from '../../../assets/ph/9.png';
import ph10 from '../../../assets/ph/10.png';
import ph11 from '../../../assets/ph/11.png';
import ph12 from '../../../assets/ph/12.png';
import ph13 from '../../../assets/ph/13.png';
import ph14 from '../../../assets/ph/14.png';
import ph15 from '../../../assets/ph/15.png';
import ph16 from '../../../assets/ph/16.png';
import ph17 from '../../../assets/ph/17.png';
import ph18 from '../../../assets/ph/18.png';
import ph19 from '../../../assets/ph/19.png';

Many of us have been launching on Product Hunt for a while, and more and more folks have started questioning whether the audience there is genuine and whether it is still worth launching on their platform.

Being fresh out of our latest launch from a week ago, I wanted to share here our first-hand experience and cover three main things:

  • How does launching on Product Hunt look and feel today
  • What we got from the launch
  • How to make (the best) use of Product Hunt for your product

About us - launched 6x, >2,000 upvotes

We've launched 6 times on Product Hunt in the last 4 years, won "Top Product" awards (#1 and #5 of the day), and collected over 2,000 upvotes in total. Our last launch was with Open SaaS - an open-source alternative to $300+ SaaS starters.

You will find many articles with advice for launching on PH, and winning stories from those who got featured, but almost nobody shares behind-the-scenes knowledge and what it really took to get there. That is the purpose of this post.

I will guide you through the steps of the launch and comment and share our experiences from each of them. Let's get started:

Scheduling your launch and creating a "Coming soon" teaser - "let's exchange upvotes"

Once you schedule your Product Hunt launch, you can create a banner to appear on their "Coming soon" page (https://www.producthunt.com/coming-soon), and this is where your journey starts. This gives PH visitors an opportunity to see what's coming next and to subscribe to get notified once it launches, and it is also the first thing you can use for marketing your launch.

This is also when the PH economy starts - as soon as you publish your launch teaser, you will start receiving offers to exchange upvotes with other people launching their products soon:

This is actually a legitimate strategy (in the sense of shared incentives and not buying votes) that can probably be utilized pretty efficiently via automation. It won't bring any qualified leads (aka people genuinely interested in your product), but it might help with the upvotes, resulting in the increased visibility and reach of your launch.

We haven't used this strategy at all (so I cannot testify to its efficiency), since we published our "Coming soon" page quite late, just a day or two before the launch, and we also didn't have the workflow in place nor manpower to pull it off.

There are also specialized groups on Linkedin, WhatsApp, and other platforms for PH participants to support each other in this way. If you join these, you will, expectedly so, receive even more such messages and requests.

Launch day - unsolicited emails and "buy upvotes" offers

On the launch day, the requests like the one above intensified. I even got several emails from others launching products on the same day asking for an upvote, as they scraped my email and added me to their newsletter.

First 4 hours of the day - hidden upvotes

Product Hunt recently introduced the feature of showing the products in the randomized order, with the upvote count hidden, for the first 4 hours of the day. The idea behind this is to guarantee all products have equal visibility at the start and a fair chance to grab the attention of the audience.

With our latest launch, Open SaaS, we had the best opening ever - 100 upvotes in 4 hours!

We, of course, engaged our network, but also noticed a lot of upvotes and comments from the people we don't personally know. With such a strong start, I was quite confident we secured our place in the top 5 products on the leaderboard.

Being in the top 5 products is an "above the fold" position on Product Hunt's home page, so getting there early is the best way to end up there.

But when the leaderboard was finally revealed, Open SaaS was barely in the top 10 launches of the day!

There was a quite noticeable cut-off between the first five places and the rest, and the product in the first place had almost double the upvotes than the second one. That was fairly demotivating for us as it felt like we had literally zero chance of catching up.

"Hey, wanna buy some upvotes?"

After the leaderboard reveal, we started receiving another type of message - direct offers to buy upvotes. Being still relatively close to being in the top 5 products probably made us a highly qualified lead:

A slight variation of this is having different social media influencers and community owners reach out and offer to market your launch to their followers, promising X upvotes:

Even some of our direct contacts knew "a guy" that could get you to the top of Product Hunt and offered to intro us, so it kinda started feeling like a "public secret", and us being the rare ones who didn't know about it.

The main benefit of our PH launch wasn't the launch itself, but rather the fact we could combine it with other things, like launching Open SaaS on HackerNews, where it ended up being featured for about half a day (and much longer on Show HN tab).

Finally, all that engagement combined allowed us to get trending globally on GitHub, which in turn brought in even more traffic to Open SaaS (today, a week after launching, it has over 2.5k stars).

The resulting traffic

Taking a look at the traffic that was brought to Open SaaS's repo in the last two weeks, here's what we can observe:

HackerNews launch brought in more than 3 times more people than Product Hunt. GitHub brought even fewer people to the actual repo, but my gut feeling is that many more of them starred it without leaving the Trending page.

Open SaaS ended the launch as the #7 product of the day, with about ~400 upvotes. The top 10 products of the day end up in a daily newsletter that has over 500,000 subscribers, according to the Product Hunt.

The newsletter starts with 3 big promotional blocks, so you must scroll quite a bit to reach the top products of the previous day.

For us, it didn't make a huge dent, I think it got us about 20 upvotes. Maybe it was due to the fact we weren't number one, or simply because it's quite a deep funnel (open email → scroll all the way down → check all the products → like Open SaaS → decide to upvote it).

Is it even possible to win #1 of the day without any boosting strategies?

Yes, it is definitely still possible. I've had it confirmed by a couple of contacts that I trust and who won #1 with their products, without any bots or paying for upvotes. But it's also definitely become less common and less predictable.

Most of the times we were launching, the product in the first place exhibited some unusual behavior. Once, it was the company that launched the week before, but they just slightly rebranded the product and the website and re-launched it. Another time, a product received a very sudden spike in upvotes just hours before the end of the launch.

So, what does all this mean? Is it still worth launching on Product Hunt?

It is obvious that today, there are different forces and incentives driving the behavior of Product Hunt users from both sides. Initially, there was a community that wanted to learn about the latest products and express their interest, and there were founders that wanted to connect to that community.

Now, there are also creators who foremost want their product to win, no matter the actual audience engagement, as they believe that will help them with their end goal - e.g., reach, fundraising, or social validation towards other users. And there is obviously a side willing to fulfill that demand without having any real interest in the product.

Why is that possible? Product Hunt is taking a lot of measures to detect and prevent such behavior, but it is hard to do it without severely limiting the network effect (aka being able to share your launch link around) that Product Hunt is going for.

Besides all that, for us it is still worth it to regularly launch on Product Hunt. Here's why.

Product Hunt is an amazing excuse

Product Hunt gives you a unique opportunity to declare an "official" launch of your product. You can decide on which day you want to do it, schedule it, and it's 100% going to be there for everybody to see, and for you to share and invite people to check it out. You get 24 hours, during which it is fully justifiable to contact everyone you know (and beyond) and keep tooting your horn.

You can't do that with other high-reach platforms such as Reddit, and HackerNews. You are, of course, free to share the news about your product at any time, but there is no guarantee that anybody will see it (quite the opposite, actually) unless the collective mind of the community decides so, which is all but deterministic. You could easily spend a week preparing your launch post just for it to get drowned by the algorithm in minutes.

That's why we look at Product Hunt not as the final goal (winning #1) but as simply a part of our overall launch process. It's a great podium to be standing on, and a good excuse to talk about your product, and anything else on top is just a bonus.

We keep it simple

You will find a lot of articles (and paid courses) from "PH gurus", explaining how you should prepare your launch months in advance, warm up your audience, prepare comments they will share, etc. We don't do any of that. We just prepare the content (video + a few screenshots, and an intro comment), and, on the day of the launch, invite everyone we know to support us. Then, during the day, we also post on Reddit, Hackernews, and dev.to.

Sometimes we end up in the top 5, sometimes we don't, but every time, we get a solid uptick in user engagement, and usually something much better follows in the next days/weeks. For example, MAGE, our GPT-powered full-stack app starter, exploded after its PH launch and has been used to create over 30,000 projects in a few months.

We do it often

Our goal is to launch on Product Hunt every 3 months as a part of our Launch Week, and that's what we've done so far. You cannot really launch the exact same product unless 6 months have passed or there's been a significant update, but you are free to launch other (sub)products and features connected to your main product.

💡 Hint: when you submit a launch, you can ask the PH team to "connect" it to your product so it will appear in a list of launches for that product. Often, they do it on their own. It will look like this:

Although our main product is Wasp, a full-stack framework on top of React & Node.js, here's what we launched so far:

It's become a regular part of our launch workflow, and for whatever new feature(s) we introduce in that quarter, we'll look for a good candidate to showcase in the upcoming launch. That allows us to keep talking about what we do, and we also get a lot of good content (e.g., videos, banners) that we can embed in our docs, blog posts, etc.

For example, this is a video showing how auth works in Wasp - first we used it for our Product Hunt launch, and now it lives at the top of our auth docs.

Thanks for reading!

Thanks for reading this far! This turned out to be quite a bit longer post than I initially expected, but I just kept getting more ideas on what to write about. I hope you will find it helpful for planning your next launch and that you will also know a bit better what to expect along the way.

I would also love to get your feedback and hear about your experiences and strategies for launching on Product Hunt.

Happy launching!


Opensaas Sh Blog Src Content Docs Blog 2025 05 21 Saas Cost Marketing Breakdown (`opensaas-sh/blog/src/content/docs/blog/2025-05-21-saas-cost-marketing-breakdown.mdx`)


title: "Here's my SaaS Cost, Profit, and Marketing Breakdown"
subtitle: "An overview of my $550 MRR micro SaaS app since launch."
date: 2025-05-21
tags:

  • gpt
  • saas
  • microsaas
  • sideproject
  • indiehacker
  • marketing
  • saas-marketing
    authors: vince

import { Image } from 'astro:assets';
import VideoPlayer from '../../../components/VideoPlayer.astro';
import demoVideo from '../../../assets/cover-letter-gpt/demo-video.mp4';
import Tweet from '../../../components/Tweet.astro';
import landingPage from '../../../assets/cover-letter-gpt/coverlettergpt-may2025.png';
import mrrRevenue from '../../../assets/cover-letter-gpt/mrr-revenue.png';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import redditPost from '../../../assets/cover-letter-gpt/coverlettergpt-reddit.png';
import coverlettergptPricing from '../../../assets/cover-letter-gpt/coverlettergpt-plans.png';
import openAiCost from '../../../assets/cover-letter-gpt/openai-cost.png';
import openAiRequests from '../../../assets/cover-letter-gpt/openai-requests.png';
import jeep from '../../../assets/cover-letter-gpt/jeep.png';
import coverlettergptPhLaunch from '../../../assets/cover-letter-gpt/coverlettergpt-ph-launch.png';
import coverlettergptIndieHackers from '../../../assets/cover-letter-gpt/coverlettergpt-indiehackers.png';

Hey builders,

I wanted to share my journey building a micro SaaS, CoverLetterGPT, which earns $550/month in recurring revenue (MRR), while requiring minimal effort and maintenance. Here's a breakdown of overall costs, profit, how I got customers, and why I believe small, simple SaaS apps are an underrated way to start as an indie maker.

@hot_town_ Here’s a down of how much it cost me to run my SaaS app which is a simple GPT wrapper for generating cover letters. Overall, it’s been a decent little profit because the app doesn’t cost me much to run. #webdevelopment #sideproject #indiehackers #saas #ai ♬ original sound - Vinny

At a glance

CoverLetterGPT is a GPT wrapper that generates personalized cover letters based on the user's uploaded CV and job description. The function that seperates it from just using chatGPT is that users can edit cover letters inline with AI assistance, as well as manage all the different cover letters they've generated. It's super simple, and it's even open-source! I launched it in August 2023 and it's been a steady source of passive income since.

Here are some quick numbers:

  • Built in 1 week
    • using Open SaaS, a free, open-source React, NodeJS, SaaS boilerplate template with tons of features.
  • Runs on autopilot
    • ~1hr/month of maintenance
  • ~$550 MRR
  • Minimal customer support
    • Only 3 Stripe disputes to date
  • Costs ~$16/month
    • ~$12 for hosting
    • ~$3 for OpenAI API fees
    • $11.82/year for the .xyz domain
  • $0 paid ads
    • Just SEO and Social Media/Reddit

Costs Breakdown

Cost Type Monthly Cost Notes
App Hosting $12 Railway.com
OpenAI API $3 ~1,500 requests / 1.5m tokens
Domain $0.98 $11.82/year via Porkbun
Stripe Fees ~$45 ~3% + 30¢/transaction + ~2% currency conversion

As you can see, Stripe fees are the biggest "cost" in the end, becuase most of my customers are abroad, so I get hit with extra cross-border fees. But that's just the cost of doing business. Plus, I don't really see these fees, so they dont bother me much.

Meanwhile, using the OpenAI API isn't as expensive as you might think, and the models keep getting cheaper and better. I'm currently using the gpt-4o-mini model for the standard plan, and gpt-4.1 for the pro plan.

At about 1,500 requests/month, which equals roughly 1.5m input and output tokens, this costs me around $3/month.

Besides that, my hosting bill is about $12/month on Railway, and I pay $11.82/year for the .xyz domain via Porkbun.

Revenue & Profit Breakdown

Metric Value Notes
Avg. Monthly Revenue $615 Past 8 months, converted from €543
Total Net Revenue $9,912 Since launch, after Stripe fees
Total Costs to Date $345 $15/month × 23 months
Avg. Monthly Profit $416 $9,567 ÷ 23 months
Total Profit to Date 🎉 $9,567 Net revenue minus costs

At current exchange rates, my average monthly recurring revenue of €543 over the past 8 months equals

  • an average of $615 MRR for the past 8 months

My total revenue, minus ~$45/month Stripe fees, disputes, and refunds, since launch has been €8756. At current exchange rates, this equals

  • $9,912 total net revenue

My costs since launch 23 months ago have been ~$15/month. So 15 * 23 equals

  • $345 monthly costs

This brings my total profit since launch to

  • $9,567 total profit
    or
  • $416/month average profit

That's enough to afford a lease on a nice Jeep Wrangler.

Not bad for a side project that I built in 1 week!

Marketing Breakdown

Channel Effort Level Return Result / Notes
Product Hunt High Medium Initial launch, created marketing assets, got early traction
Reddit Medium-High High Posted in dev/entrepreneur/job subreddits, boosted SEO, some bans risk
Indie Hackers Easy Medium Shared open-source story, featured in newsletter, good feedback
Twitter Hard Low Shared updates, videos, and journey.
TikTok Medium (Ongoing) High Shared updates, videos, and journey, helps maintain MRR.
Paid Ads None -- Did not run any paid ads

By far the question I get asked the most by other curious builders is how I got customers. Many ask if I paid for ads, and I didn't.

Here's what I did do:

Initial Product Hunt Launch

A lot of people aren't sure if Product Hunt is worth it these days (spoiler: it still is!), but I'd say launch there anyway.

I wasn't sure where to go with my app after I first built it, so Product Hunt seemed like a good start. The benefit of launching there, regardless of how the launch performed on the platform, was that it forced me to do a few important things.

First, I had to create good marketing materials for the launch, like videos, images, and marketing copy.

Then, with the launch coming up, I felt that I had to start telling people about it. I mean, there's no point in launching and then not trying to get some upvotes and support, right?

This all lead to giving me a jump start on spreading the word about the app. It did decently well on product hunt, and gained some initial traction, but with that material I also went to other platforms to share it.

Reddit Posts and Comments

Posting on Reddit can be tricky. If you're too forward, it gets seen as spam and you can get banned.

Luckily, I left the app open-source because I wasn't anticipating much success from it, and a good side effect of this was that I was able to openly post about it on developer and entrepreneur subreddits. To this day, I think this had a good effect on SEO, but I'm not certain.

Besides that, I found job search subreddits and left comments on posts where people were asking about using AI to generate cover letters.

Indie Hackers

I also posted about it in the IndieHackers community and got great feedback, mainly due to the fact that it was open-source and starting to get good signup numbers.

This led to a getting featured in their newsletter--again, thanks to the app being open-source--and this led to a lot more buzz.

Twitter & tiktok

I also took to twitter and tiktok to share fun videos about the app, my journey building it, and my thoughts about it.

Doing this periodically and consistently throughout the past couple years has probably helped keep MRR consistent

What I've learned

This experience has been pretty interesting. I've defintiely had a bit of luck and good timing on my side, but I've also learned a lot about building a SaaS, marketing, and what tends to be good and bad advice out there.

So here's my take on things.

Small, Persistent Wins Are Worth It

Many developers think a SaaS has to be big, flashy, or wildly profitable to be worth building. I disagree. For me:

  • $550/month is fantastic as side income.
  • Revenue has been surprisingly stable.
  • It runs itself, requiring virtually no maintenance.
  • I can balance it easily alongside my full-time job.
  • It's fun and doesn't consume my free time.

I would encourage anyone who wants to build a SaaS to go for it, but to aim for small, achievable SaaS projects instead of trying to “hit it big” from the start.

Build & Launch Fast

The most important lesson I've learned: speed is everything. The faster you launch, the faster you'll know if your idea works. Here's what worked for me:

  1. Avoid long, drawn-out failures: Build small, execute early.
  2. Use the fastest tools available: I used Open SaaS because it gives me all the building blocks already set up (auth, Stripe payments, OpenAI API examples, email sending, etc), letting me focus on the business logic of the app.
  3. Forget perfection: I didn't worry about making it pretty or perfect—-it just had to work.

Keep It Simple

The beauty of a simple, "micro SaaS" is in its simplicity. Here's why:

  • My app does one thing well: generating cover letters based on résumés and job descriptions and allows users to edit them inline with AI assistance.
  • There's no need for a fancy landing page or marketing gimmicks. This is my 🌶 hot take. I mean, my landing page is my app! Users land on it and can instantly try it out.
  • Users get 3 trial credits-—enough to try the app and see value before paying.

One of the biggest perks of small SaaS is how low-maintenance it can be. With CoverLetterGPT, I rarely handle customer service thanks to its simplicity and the low, but consistent number of users (~100).

This means I spend my time on new ideas rather than maintaining old ones.

It's All About Tradeoffs

While I could optimize and grow CoverLetterGPT further, I've chosen to keep it small and simple. For me:

  • Small wins are still wins.
  • I value having a side project that's easy to manage alongside my full-time job.
  • I'd rather have less stress than chase higher profits.

Final Thoughts

If you're considering building a SaaS, don't overthink it. Start small, move fast, and treat it as an experiment. Forget the “rules” and focus on launching. Here's what matters most:

  • Keep it simple: Build an app that solves one problem well.
  • Launch fast: Test your idea and iterate based on real feedback.
  • Minimize effort: Aim for maximum reward with minimal maintenance. If you're spending months on it before people can try it, you're probably working on the wrong initial idea.

For me, $550 MRR isn't just “enough”—it's amazing. It's proof that small, focused apps can succeed, and they're a great way to build confidence and skills as a maker.


Opensaas Sh Blog Src Content Docs Blog 2025 07 29 Open Saas Version 2 (`opensaas-sh/blog/src/content/docs/blog/2025-07-29-open-saas-version-2.mdx`)


title: 'Open SaaS v2.0 -- ShadCN UI, LLM-friendly, MoRs, and more.'
subtitle: 'A summary of the changes and improvements in the latest version of Open SaaS.'
date: 2025-07-29
tags:

  • gpt
  • saas
  • microsaas
  • sideproject
  • indiehacker
  • marketing
  • saas-marketing
    authors: vince

import { Image } from 'astro:assets';
import VideoPlayer from '../../../components/VideoPlayer.astro';
import openSaasV1 from '../../../assets/open-saas-v2/open-saas-v1.mp4';
import openSaasV2 from '../../../assets/open-saas-v2/open-saas-v2.mp4';
import Tweet from '../../../components/Tweet.astro';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import redditPost from '../../../assets/cover-letter-gpt/coverlettergpt-reddit.png';
import polar from '../../../assets/open-saas-v2/polar.webp';
import llmsFull from '../../../assets/ai/llm-txt-chat.webp';
import openSaasBanner from '../../../assets/open-saas-v2/open-saas-banner-light.png';
import waitTheresMore from '../../../assets/open-saas-v2/wait-theres-more.jpg';
import playwrightUi from '../../../assets/open-saas-v2/playwright-ui.png';

{/* */}

What could make a free, open-source, full-featured SaaS boilerplate even better? Probably a fresh new look on top the hottest UI library out there, plus a slew of new features.

Well, that's exactly what we've done with Open SaaS v2.0.

Oh, and we're launching it on Product Hunt, so go show your support for open-source SaaS dopeness! 🙏

<a
href='https://www.producthunt.com/products/open-saas?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-open-saas-2'
target='_blank'
style='position: relative; display: flex; justify-content: center; align-items: center; height: 200px; width: 100%;'

The most significant change is a complete redesign using Shadcn UI, providing a more modern, consistent, and customizable experience out of the box. This update also includes key framework upgrades, new integrations, and significant codebase improvements to help you build your SaaS faster than ever.

You're welcome.

New Look with Shadcn UI

People really love Open SaaS, but we knew from the start that if there were a way to improve it, than it would be to offer a better look and feel.

So we've done just that.

🧐 Here's what it looked like before:

🔥 And here's what it looks like now:

You probably noticed that the design is cleaner, more consistent, and offers more interactive elements. But it's also completely redesigned on top of Shadcn UI (which is also open source!), so you get a modern, composable, and highly customizable experience out of the box.

On top of that, we've also made the styles easily themable, and in the future we'll be offering a slick way to find and use a great looking theme, as well as a range of different Landing Page templates for you to choose from. So stay tuned for that!

Now on to the new features!

New Features

Lemon Squeezy, Polar, and Paddle Merchants of Record

Merchants of Record (MoR) are great. They're a layer on top of payment processors like Stripe that take the pain out of the legalities and tax compliance of being a merchant.

We've added support for Lemon Squeezy with Polar integration in the works as we speak! Choose the payment processor that best fits your SaaS if Stripe is too granular for your needs.

And while we're working on Polar right now, next up we'll be adding support for Paddle as a MoR to satisfy the needs of the broader international indiehacker community.

LLM-friendly Docs and Rules

One of the best use cases for LLM-assisted coding is to get precise answers to your questions about a codebase or library. You can pass it the docs, ask your questions, and get context-aware answers.

So we've added an LLM-friendly version of the Open SaaS docs, as well as the Wasp framework docs, to make it easier for you to use in your AI coding environments.

Just add the following urls to your AI coding environment such as Cursor, Copilot, etc, and mention it in your prompt to get precise answers to your Open SaaS and Wasp framework questions.

Open SaaS AI-friendly docs:

text
https://docs.opensaas.sh/llms-full.txt

Wasp framework AI-friendly docs:

text
https://wasp.sh/llms-full.txt

In addition, we've also got a full set of project-specific Cursor rules files that you can use to make sure your AI assistants stay on the right track. These can be adapted to use with Claude Code, Windsurf, and more.

End-to-End Testing with Playwright

One thing I hate about building a SaaS is that I'm constantly worried it might break after adding a new feature. To give you peace of mind, we've integrated a full suite of end-to-end tests using Playwright. These tests act like a robot user, clicking through your app to verify that everything from user sign-ups to critical payment flows is working exactly as it should.

But we didn't just write tests for ourselves. We've provided the entire setup, including a CI/CD workflow file for GitHub Actions. This means you can automatically run these checks on every code change, ensuring you never accidentally break your production app. Ship with confidence, knowing your core features are easily protected, and use our test suite as a foundation for your own custom test cases.

More Refactors & Improvements

On top of the headlining features, we've been busy shipping a ton of other improvements to make your life easier. We've squashed bugs, upgraded major dependencies like Node.js and the Wasp framework, and completely refactored the codebase for better organization. We also threw in some handy new integrations like enhanced file uploads with progress bars and a cookie consent banner. Here's a quick rundown of other key changes:

  • New Features & Integrations 🚀:

    • Enhanced File Uploads: The file upload functionality has been improved with features like progress bars and validation. The S3 CORS configuration was tightened for improved security.
    • Cookie Consent: A cookie consent banner has been implemented.
  • Major Dependency and Framework Upgrades 🛠️:

    • Wasp Framework: There have been multiple upgrades to the Wasp framework, moving from version 0.13.0 up to 0.16.0 and beyond. This indicates active maintenance and adoption of new Wasp features.
    • SDKs: The AWS and OpenAI SDKs have been updated to their latest versions.
    • Node.js: The environment has been updated to Node.js version 20.
  • Codebase Refactoring and Organization 🗂️:

    • Vertical Reorganization: A major refactoring effort was undertaken to organize the codebase "vertically". Functionality for features like auth, payments, users, and file-upload are now grouped together, improving modularity.
    • User Management: Significant refactoring was done on the user management and administration pages.
  • Documentation and Content 📚:

    • A substantial number of commits are related to adding new blog posts, updating documentation, and creating other content. This reflects a strong focus on community engagement and developer resources.

Help Us, Help You!

We're not done yet. We're going to keep listening to our commonunity and improving Open SaaS.

So if you've got ideas on how to improve it, please create a GitHub issue or submit a pull request.

Got a cool landing page variation? Submit a PR!
Found a bug? Create an issue!

Want to support us? Star the repo on GitHub!

More Landing Page Templates

We're working on a ton of new landing page templates for you to choose from. We'll be adding more templates in the coming weeks and months, so keep an eye out for updates.


Opensaas Sh Blog Src Content Docs Blog 2025 11 19 Gemini 3 Open Saas (`opensaas-sh/blog/src/content/docs/blog/2025-11-19-gemini-3-open-saas.mdx`)


title: "Building a SaaS with Gemini 3 and Open SaaS"
date: 2025-11-19
tags:

  • gemini
  • cursor
  • ai
  • tutorial
    authors: vince

import VideoPlayer from '../../../components/VideoPlayer.astro';
import { Image } from 'astro:assets';
import chatInterface from '../../../assets/gemini-3/chat.webp';
import landingPage from '../../../assets/gemini-3/landing-page.webp';

I'm always skeptical of demos whenever a new model gets released. So I had to test Google's new Gemini 3 model myself.

I wanted to give a more unfiltered look at some real-life SaaS development tasks with Gemini 3, Cursor and our very own Open SaaS boilerplate, so I recorded a quick video of the process.

The results were impressive and it was able to handle 3 tasks of increasing complexity. I'll definitely be using this as my new go-to model for feature development in future projects!

Check out the full video below to see it in action:

Refactoring the Landing Page

I started with a fresh copy of the Open SaaS template and gave Gemini 3 a simple prompt: look at the demo AI app and update the landing page to reflect it.

Using Cursor's "Plan Mode," Gemini 3 proposed three different design styles. I chose an "interactive product-led demo" approach. The model generated a plan and executed it on the first try, building a custom interactive component and integrating it seamlessly into the existing landing page hero section.

This was a cool surprise, as I hand't thought about building an interactive component to showcase the app's features. A nice win!

Refactoring with Structured Outputs

Next, I ramped up the difficulty. The demo app originally used OpenAI's function calling to generate a daily schedule. I asked Gemini 3 to refactor this to use OpenAI's "Structured Outputs" feature instead.

The model:

  1. Identified the need for a Zod schema to define the response format.
  2. Switched the model to gpt-4o-mini.
  3. Replaced the legacy tool definitions with the new response_format parameter.
  4. Updated the parsing logic to handle the structured response.

This refactor modernized the codebase without breaking existing functionality. Nice. This saved me a ton of time reading the docs and figuring out how to use the new feature myself.

Adding a Collaborative Chat Interface

For the final test, we asked for a major feature addition: a chat interface to collaborate with the AI on the creating a daily plan (with subtasks) before committing to it.

Gemini 3 broke this down into a multi-step plan:

  • Backend: Define new schemas and actions to handle the chat history and plan proposals.
  • Frontend: Implement a chat component using shadcn/ui components (specifically the ScrollArea).
  • Integration: Connect the chat interface to the planning logic, allowing users to refine tasks (e.g., "prioritize filming the video") before accepting the final schedule.

The result was a functional chat interface where the AI intelligently adjusted the schedule based on user feedback, embedded directly into the app. I loved how it injected a nice styled component into the chat window, something that I wanted (it read my mind!) and would have taken me hours to build in the past.

I was really happy with the results of this challenge!

Conclusion

The combination of Gemini 3, Cursor, and Open SaaS proved to be pretty powerful. The model didn't just generate code; it understood the project structure (Open SaaS organizes code vertically by feature), followed architectural patterns, and successfully implemented complex refactors and new features with minimal human intervention.

Overall, I was very impressed and I think other SaaS builders would be smart to give this stack a go.

⭐️ Star Open SaaS repo and start building your next idea today!


Opensaas Sh Blog Src Content Docs Blog 2025 11 21 Open Saas Public Roadmap (`opensaas-sh/blog/src/content/docs/blog/2025-11-21-open-saas-public-roadmap.mdx`)


title: "The Public Development Roadmap for Open SaaS"
date: 2025-11-21
tags:

  • roadmap
  • wasp
  • open-source
  • development
    authors: vince

import { Image } from 'astro:assets';
import openSaasUsedBy from '../../../assets/roadmap/open-saas-used-by.webp';
import openSaasRoadmap from '../../../assets/roadmap/open-saas-roadmap.webp';
import epicExample from '../../../assets/roadmap/open-saas-epic.webp';
import epicDetailExample from '../../../assets/roadmap/open-saas-epic-detail.webp';
import aiReady from '../../../assets/roadmap/open-saas-ai-ready.webp';
import subscribeGithubButton from '../../../assets/roadmap/subscribe-github.webp';
import Tweet from '../../../components/Tweet.astro';
import { Aside } from '@astrojs/starlight/components';

We created Open SaaS to help developers launch full-featured SaaS products faster.

But launching fast only works when you're working with clear direction and a good foundation. As an open-source project powered by Wasp, and with our users in mind, we thought it was important to add two things to this project:

  1. A solid roadmap so that development is focused.
  2. Making that roadmap public, to give the community a clear view of what's coming next and how they can get involved.

So we created just that.

Behold, the Open SaaS public roadmap!


Why a roadmap?

Open SaaS is becoming a beast of its own. At 13k GitHub stars and growing, and with constant feature additions and improvements, we thought it was time to make future development more organized, transparent, and community-driven.

With tons of SaaS apps being deployed with Open SaaS, the needs of its users are also becoming more diverse. It would be impossible to address all of them, but we needed a way to track and prioritize the most important features and improvements.

Up until now, it was just me basically deciding which issues to work on and when. This was pretty inefficent to begin with, and then with more contributors joining the project, it became even more so.

The roadmap is thus a great way to make sense of all the different possible directions we can go in, help focus our efforts, and make development and contributions more meaningful.

How it works

Open SaaS currently has about 100 open GitHub issues, so it would be crazy to organize and track them all in the roadmap.

So to make things overseeable, the roadmap is a GitHub project with a collection of "Epics". An Epic is just a GitHub issue with a bunch of sub-issues. Each Epic has a status and a progress bar to show you how many of its sub-issues are completed.

No regular issues are allowed in it, only epics, of which we don't want too many. This way, its easy to understand the current themes and priorities of the project, as well as what's currently underway.


Guiding themes

When creating epics within the roadmap, there are a few categories we're focusing on at the moment. These guiding themes inform the epics we create, and help us make sure we're addressing the most important needs of our users.

1. Real-world launch readiness

Open SaaS should not just be a polished demo, or a flimsy template. It should be something you can deploy and scale. We want to make sure Open SaaS is a production-ready starter that can be used to build real-world SaaS products.
This means having a solid architecture, a clean codebase, and a set of features that are robust and ready to be used in production.

2. Modularity & flexibility

Different auth methods, billing providers, file uploads, AI integrations, analytics, and more. We want to make sure Open SaaS is a flexible and modular platform that can be tailored to the needs of (almost) any SaaS product.
This means having the most essential features you need to launch your SaaS quickly, and being able to easily remove the things you don't.

3. AI-first developer experience

Whether you use Cursor, VSCode + Copilot, Claude, or others, Open SaaS should have top-notch code quality and be able to “explain itself” well to AI-driven coding tools, making it enjoyable to develop with in 2026 and beyond.

We also want to provide demos and templates for AI-powered SaaS products, so you can get started building SaaS apps that leverage the newest technologies to create powerful and innovative products.

4. Community-powered evolution

We want Open SaaS to be shaped by real feedback, real PRs, and real use cases. Roadmap ideas are only valuable when they reflect what people actually need.
So your feedback and contributions are important to us!


What's in the roadmap

We just got finished with a major UI redesign, which we dubbed "Open SaaS v2.0". Take a look at the announcement below.

With the new UI, based on Shadcn UI, finished and tucked away, we've turned back towards adding highly-requested features, and improving the codebase with smaller fixes.

1. Payment Provider Expansion

Today, Open SaaS supports Stripe, Polar, and Lemon Squeezy as payment providers. It's super easy to get started with one -- just plug and play.

Upcoming features:

  • Polar.sh integration (merged in #461)
  • Paddle, and additional gateways (especially regional-friendly options)
  • Supporting up- or down-grading between different billing plans and models (proration)

2. AI-First Workflows & Templates

Open SaaS already includes a simple OpenAI integration example, but we’re looking to expand and improve it.

Coming soon:

  • Exploring using Vercel's AI SDK within our demo app (issue #567).
  • Cursor / Claude prompt presets for smoother onboarding (issue #579)
  • Creating better docs and guides for AI-powered SaaS development

3. Core Development Improvements

We want to keep Open SaaS relevant and up-to-date with the latest trends, as well as with the latest Wasp (the framework it's built on) features and improvements .

Efforts underway:

  • Refactors, galore! E.g. removing any and as from the codebase, extracting SVGs, etc. (issue #568)
  • Improving OpenSaaS.sh development workflows (issue #552)
  • Improving e2e tests (issue #558)

How to Participate

So that's just a quick overview of the current state of the roadmap. But we're not done yet!

Open SaaS is community-driven — and we’d love your help.

⭐ Star the repo

This is super easy and helps a ton!

https://github.com/wasp-lang/open-saas

🧩 File issues or feature requests

Especially around real-world SaaS requirements you’re running into.

https://github.com/wasp-lang/open-saas/issues

🔧 Contribute code

If you’re interested in payments, dashboarding, AI workflows, or dev-experience improvements — we’ll happily support and guide.

https://github.com/orgs/wasp-lang/projects/6

🚀 Build with it

The best feedback always comes from people deploying real products.
Drop us a line on Discord or Twitter and let us know what you're building!


Roadmap Updates

We’ll keep updating this roadmap alongside major releases and aim for a quarterly revision cycle.

You can follow progress in the GitHub Project board:

👉 https://github.com/orgs/wasp-lang/projects/6

See you there!


Opensaas Sh Blog Src Content Docs Blog 2026 03 16 Best Way To Vibe Code Saas 2026 (`opensaas-sh/blog/src/content/docs/blog/2026-03-16-best-way-to-vibe-code-saas-2026.mdx`)


title: "What's the Best Way to Vibe Code a SaaS in 2026?"
date: 2026-03-16
tags:

  • ai
  • saas
  • vibe-coding
  • webdev
    keywords:
  • "vibe coding saas 2026"
  • "best ai coding tool for saas"
  • "claude code saas development"
  • "open source saas boilerplate"
  • "ai native platforms vs ai coding tools"
  • "replit vs claude code"
  • "lovable bolt alternatives"
  • "llms.txt documentation"
  • "free saas starter template"
  • "ai assisted web development"
  • "saas boilerplate starter kit"
  • "vibe coding with claude code"
    authors: vince
    hideBannerImage: false

import { Image } from 'astro:assets';
import { Aside } from '@astrojs/starlight/components';
import VideoPlayer from '../../../components/VideoPlayer.astro';
import Testimonial from '../../../components/Testimonial.astro';
import StarOpenSaaSCTA from '../../../components/StarOpenSaaSCTA.astro';
import FAQSection from '../../../components/FAQSection.astro';
import llmsFull from '../../../assets/ai/llm-txt-chat.webp';

You've probably seen the tweets, the YouTube tutorials, and the "I built a SaaS in 24 hours with AI" posts. But if you've actually tried to vibe code a SaaS, you know the reality is much messier than that.

Luckily, there are a bunch of tools, SaaS boilerplate starters, and workflows that you can use to build a profitable (side) business with, as long as you know how to apply them correctly.

So let's take a look at what's out there, and what really works.

TL;DR

  • AI-native platforms (Replit, Lovable, Bolt) are great for prototyping but hit a ceiling as complexity grows. Code is coupled to their infra, and it quickly becomes expensive and difficult to maintain.
  • AI coding tools (Claude Code, Cursor, Codex) give you great code and full control, but they need the right foundation and you'll need some fundamental understanding of the underlying tools to work well with them.
  • The winning combo: Claude Code paired with a well-structured SaaS boilerplate like Open SaaS is the best way to vibe code a SaaS in 2026, giving you coherent code generation, along with better control as your app grows.
  • Two key techniques for effective vibe coding: (1) LLM-friendly docs (llms.txt and beyond), (2) full-stack debugging visibility.

{/* truncate */}

The Vibe Coding Landscape in 2026

AI-assisted coding tools come in different flavors, and understanding their differences is key to picking the right approach.

In the table below, we quickly compare the two approaches. In the sections below that, we go into more detail about each approach.

AI-Native Platforms AI Coding Tools
Examples Replit, Lovable, Bolt.new Claude Code, Cursor, Codex
Setup Zero-config, browser-based Local install, terminal/code-focused
Best for Quick prototypes and demos Production apps and SaaS businesses
Code control Limited — AI makes structural decisions Full — you own and control everything
Vendor lock-in High — coupled to platform infra None — works with any stack
Scalability Scalability degrades as complexity grows Scales with your codebase. No limitations.
Deployment Platform-hosted (limited options) Deploy anywhere (Railway, Fly.io, VPS)
Cost at scale Monthly fee + additional costs for debugging, storage, hosting, etc. Cheaper — monthly fee + choose your favorite hosting solution

AI-Native Platforms

Tools like Replit Agent, Lovable, Bolt.new, and v0 aim for a "zero-config", purer "vibe coding" approach, where you describe what you want and get a working app. They aim to remove setup, config, and having to touch the code and/or terminal as much as possible. They're impressive for demos and fast prototyping.

The catch here is that most of these apps are then locked-in to their hosting, auth, and storage solutions. This isn't always bad, but things can easily get complicated and expensive as your app grows:

  • The "70% problem": these tools get you roughly 70% of the way to a working product. The last 30% — auth hardening, error handling, performance, deployment config — requires significant manual effort. One estimate puts the cost of making Bolt.new output production-ready at $5,000-$20,000 in professional dev work.
  • Infrastructure coupling: the code you export is tightly coupled to a specific service stack. Lovable, for example, generates code deeply wired into Supabase, a separate service for auth, storage, realtime, edge functions, etc. Leaving Lovable/Supabase means rewriting all of those integrations. Replit similarly relies on its default Auth, Storage, and database options.
  • Code quality degrades at scale: Bolt.new projects with 15-20+ components experience severe AI context degradation — duplicated code, forgotten patterns, and inconsistencies. Replit Agent has been flagged for generating "non-idiomatic or messy" code that becomes harder to maintain as projects grow. Multiple developers report spending $1,000+ in tokens just debugging AI-generated bugs.
  • Limited architectural control: the AI makes structural decisions for you, and those decisions compound. Data models get rigid, logic gets tightly coupled, and by the time you want to refactor, you're looking at a near-rewrite anyway.

In the end, the most common feedback from users of these tools is that they allow you to move fast and to easily build initial prototypes, but maintainence and upgrades quickly become complex and expensive.

To solve this, working on code locally, instead of a platform, is the first step.

AI Coding Tools

Tools like Claude Code, Cursor, GitHub Copilot, and Codex work directly with your local files, and are better at generating production-ready code that you control.

The pros here are: full control, no lock-in, works with any stack, and the added bonus is that it will be cheaper. You can version it, test it, deploy it anywhere, and switch tools whenever you want.

But these tools require a bit more work to get the most out of them. You'll have to be comfortable using the terminal, install a few more underlying tools, and its good to have a general understanding of the fundamentals when working with them. But, luckily, these tools can help you each step of the way, and if you set things up right (which we will show you how to do in this article) you can go all the way with them.

In comparison to the AI-native platforms, if you're serious about building something substantial these tools give better results at cheaper prices.

We'll use Claude Code as the driving example throughout this post, but the same principles apply to Cursor, Codex, and others.

The Winning Combo: Claude Code + SaaS Boilerplate

Most of the tedious parts of building a SaaS are routine, boilerplate tasks like setting up authentication, payments, email sending, file uploads, admin dashboard, etc. These are the things that are time-consuming to set up, aren't very creative or interesting, but are 100% necessary for a functional SaaS.

Luckily, tons of SaaS boilerplate starter kits exist, giving you a solid foundation to build your SaaS on top of. They provide all the necessary integrations, plumbing, and glue code, along with some nice extras, so you can just focus on building the fun part of your app.

A well-structured boilerplate has some key benefits: it allows you to start working on the actual business logic of your SaaS from day one. It also gives the AI patterns, structure, and conventions to follow, making it easier for tools like Claude Code to generate clean, consistent code.

It's like giving a skilled carpenter a well-organized workshop instead of an empty shed. The tools are there, the materials are laid out, and there's a clear plan to follow.

You keep the full control, but you skip the part where you (or your AI) have to make a hundred decisions about project structure, auth patterns, payment integration, and deployment config. That's all solved for you already by the starter kit.

But, because so many boilerplate starters exist, choosing the right one can be a pain.

Choosing Your SaaS Boilerplate

There's no shortage of SaaS boilerplates out there. The real question isn't just "which one has the best features?", it's which one sets you (and your AI coding tool) up for long-term success. A few factors matter more than you'd think: cost, deployment flexibility, community support, and how well the codebase structure plays with AI.

In the table below, we quickly compare some popular, battle-tested starters. In the sections below that, we go into more detail about each of them.

Paid Boilerplates (ShipFast, Supastarter) Open SaaS
Price $250-$800+ Free, open-source
Auth Third-party providers (Clerk, Supabase auth, Better Auth) Built-in (email/password, Google, GitHub, Microsoft, Slack, etc.)
Payments Stripe, Lemon Squeezy, or Polar.sh, etc. Stripe, Lemon Squeezy, or Polar.sh
Deployment Vercel + Supabase (typically) Railway, Fly.io, or any VPS
Community 1-2 maintainers 13,000+ GitHub stars, active open-source community
LLM-friendly docs llms.txt llms.txt
Codebase structure Multiple apps/packages with separate configs and third-party integrations wired together Single app, feature-based organization with fullstack code colocated

Options like ShipFast ($250) and Supastarter (starting at $299) are popular choices. They're packed with lots of features and have a strong history of adoption and support.

But there are a few things to consider:

  • Upfront cost: $250-$800+ before you've written a line of business logic
  • Vendor-leaning stacks: many are built on top of Vercel + Supabase + third-party auth services. That's more vendor lock-in and recurring hosting costs on top of the purchase price. You're not just paying for the boilerplate, but you're also committing to a specific infrastructure stack.
  • Code Quality: most paid boilerplates are maintained by a one or two developers. An open-source project, on the other hand, is maintained by many more people, and the community input is much larger, making it more robust and reliable.
  • Stack fragmentation: ShipFast is a flat Next.js app where auth, payments, email, and database are each handled by separate third-party libraries you wire together yourself. Supastarter is a Turborepo monorepo with 4 separate Next.js apps and 8+ shared packages, each with its own config. Both approaches mean more files for an AI tool to navigate, more indirection to follow, and more context needed to make a single cross-cutting change.

None of this means paid boilerplates are bad. They can absolutely save you time. But it's worth understanding what you're signing up for beyond the purchase price.

Open SaaS -- a free, open-source, production-ready SaaS boilerplate starter

Open SaaS is free, and open-source. It's powered by Wasp — a batteries-included full-stack framework for React, Node.js, and Prisma apps.

Here's what you get out of the box:

  • Auth: email/password, Google, GitHub, all pre-configured and without third-party providers
  • Payments: Stripe, Lemon Squeezy, or Polar.sh. Take your pick.
  • Email, file uploads, admin dashboard, analytics, e2e tests: the full SaaS toolkit.
  • Deploy anywhere: Railway, Fly.io, or any VPS. No vendor lock-in.
  • 13,000+ GitHub stars and an active open-source community backing it.

Because Open SaaS is built on Wasp, everything lives under one roof. React frontend, Node.js backend, and Prisma database layer are all managed by a single framework, with a declarative config (main.wasp or main.wasp.ts) that defines your routes, pages, operations, auth, and background jobs. The codebase is organized by feature (e.g. auth, payments, file uploads) with backend logic and frontend components sitting side by side. When an AI coding tool reads main.wasp, it immediately understands the full app structure without having to jump between separate apps, packages, or third-party service abstractions. One framework, one config, one mental model for the AI to follow.

Effectively Vibe Coding Your SaaS

So we've got our AI-assisted coding tool and our SaaS boilerplate starter, but to get the most out of AI-assisted development, you need two more things working together. Note that these tips come from real-world experience building full-stack apps with Claude Code and Wasp, and we covered them in depth on the Wasp blog.

1. LLM-Friendly Documentation

LLMs don't always get things right, and may sometimes be out-of-date with the latest features of the tools you're using. For example, if you're using a framework that shipped a new version last week, your AI tool probably doesn't know about it, unless you give it the docs.

But how you give it docs matters a lot, because LLMs don't have a long-term memory, rather they have context windows, which is like a limited memory of only the most recent conversations. So with each new session, you have to remind them of what you're working on, with, and what you want to do.

One of the best solutions is to use llms.txt files. These are structured docs in a single, machine-readable plain text file that's purpose-built for LLMs.

Open SaaS and Wasp both provide these:

Just add these URLs to your AI coding tool's chat session or "memory" (e.g. CLAUDE.md, AGENTS.md). When the AI needs to, for example, understand how Wasp auth works or how to set up Stripe payments in Open SaaS, it has the full, up-to-date docs right there.

And it's not just Open SaaS and Wasp. Most developer tools and integrations have adopted the llms.txt standard. Try appending /llms.txt to any tool's docs URL and see what comes back. You'd be surprised how many services already support it. When you're implementing a new integration, pass its llms.txt to your coding agent so it has good guardrails for development.

Another pro tip: when you're working on a specific feature, search the tool's docs for the relevant guide and look for a "Copy for LLMs" button. Many doc sites now offer this. Or just try adding .md to the end of a specific URL to get an LLM-friendly version of that page. For example, Stripe's guide on SaaS subscriptions is available at https://docs.stripe.com/get-started/use-cases/saas-subscriptions.md. Pass that to your agent and it'll have the knowledge it needs to implement Stripe subscriptions correctly.

2. Full-Stack Debugging Visibility

AI can write code all day, but if it can't tell whether the code actually works, you're constantly stuck in the middle, running the app, checking the browser, copying error messages back and forth.

The fix is giving your AI tool full-stack visibility into what's happening when the code runs, so it can see the errors, and fix them without you having to always intervene.

  • Background dev server: Run your dev server as a background task so the AI can see server logs, compilation errors, and runtime exceptions in real time. In Claude Code, you can use background tasks (Ctrl+B) to keep wasp start running while you work, or just tell it to "start the dev server as a background task".
  • Browser automation: Tools like Chrome DevTools MCP or Vercel's agent browser let AI see what's actually rendering in the browser -- console errors, network requests, DOM state -- without you having to screenshot anything. The AI can verify that the button it just added actually appears on the page and handles clicks correctly.

Together, these create a tight feedback loop: AI writes code → sees the result → spots the error → fixes it → verifies the fix. This improves the quality of the code it generates, and makes the entire process less tedious and more efficient.

Getting Started: From Zero to Vibe Coding

Ready to try it? Here's how to go from nothing to vibe coding a SaaS in a few minutes.

Step 1: Install Wasp and Get the Template

bash
npm i @wasp.sh/wasp-cli
wasp new my-saas -t saas
cd my-saas

This scaffolds a new Open SaaS project with auth, payments, email, file uploads, an admin dashboard, a landing page, and more — all pre-configured and ready to go.

Step 2: Set Up Claude Code with the Wasp Plugin

After installing Claude Code, add the Wasp plugin so that it has deep knowledge of the framework, as well as skills and instructions on how to properly use full-stack debugging visibility, llms.txt docs, and more:

bash
claude plugin marketplace add wasp-lang/wasp-agent-plugins
claude plugin install wasp@wasp-agent-plugins --scope project

:::note[Other coding agents]
If you're using a coding agent tool other than Claude Code, like Codex, OpenCode, or Cursor, you can get the same set of agent skills by running:

bash
npx skills add wasp-lang/wasp-agent-plugins

:::

Then, in a new session, run the plugin initialization skill command to initialize the plugin with Wasp best practices for your project:

bash
/wasp-plugin-init

Step 3: Get Integrations Working (Stripe, Email Sender, OpenAI, etc)

Next, you'll want to get your payment, email sender, and other integrations working (Stripe, Email Sender, OpenAI, etc) before you start building features.

Luckily for you, we've got a complete walkthrough video that guides you step-by-step through creating accounts, adding your API keys, and getting each integration working:

Or if you want your agent to help guide you through the process, you can ask it to fetch the "getting started" guide from the Open SaaS LLMs.txt docs and follow the instructions step-by-step.

Step 4: Start Building

Fire up your database and dev server:

bash
wasp start db
wasp start

Or just tell Claude Code to run start the dev server as a background task and it will run the correct commands for you :)

Now it's finally time to give Claude Code a real feature request — "add a user settings page where users can update their profile" — and watch it reference the Wasp config file(s) to understand your app's structure, reference the right patterns from the boilerplate, and implement the feature correctly.

Because Wasp is opinionated, Claude won't have to guess where to put files, how to wire up routes, or which patterns to follow. It reads the config file(s), sees the existing conventions, and follows them. You get consistent, working code on the first try — or pretty close to it.

FAQ

<FAQSection items={[
{
question: "What's the difference between AI-native platforms and AI coding tools?",
answer: "AI-native platforms like Replit, Lovable, and Bolt.new aim for a zero-config experience where you describe what you want and get a working app. AI coding tools like Claude Code, Cursor, and Codex work directly with your codebase, giving you full control and no vendor lock-in. AI-native platforms are great for quick prototypes but hit a ceiling as complexity grows, while AI coding tools require more setup but produce more maintainable, production-ready code, and can actually be cheaper in the long run."
},
{
question: "Can I export my code from Replit, Lovable, or Bolt.new?",
answer: "Yes, all three platforms let you export your code via GitHub sync or ZIP download. However, the exported code is often tightly coupled to their infrastructure — for example, Lovable generates code wired into Supabase, and Replit uses proprietary auth, storage, and database solutions. This means significant refactoring is needed if you want to deploy elsewhere or switch services."
},
{
question: "What is llms.txt and why does it matter for vibe coding?",
answer: "llms.txt is a structured documentation file in plain text format, purpose-built for LLMs. It gives AI coding tools like Claude Code up-to-date knowledge about the frameworks and tools you're using, reducing hallucinations and outdated code suggestions. Most developer tools now support it -- just append /llms.txt to any tool's docs URL to check."
},
{
question: "What is the best free SaaS boilerplate starter for AI-assisted development?",
answer: "Open SaaS is a free, open-source SaaS boilerplate for React, Node.js, and Prisma apps powered by the Wasp framework. It comes with auth, payments (Stripe, Lemon Squeezy, or Polar.sh), email sending, file uploads, an admin dashboard, and more, all pre-configured. Because it's built on Wasp, the entire stack is managed by a single framework with a declarative config file. The codebase is organized by feature with backend and frontend code colocated, so AI tools can understand the full app structure from one file instead of navigating multiple apps, packages, and third-party integrations."
},
{
question: "Does codebase structure matter for AI-assisted development?",
answer: "Yes, significantly. AI coding tools work best when they can quickly understand the full picture of your app. A codebase organized by feature with colocated fullstack code — like Open SaaS, where backend logic and frontend components sit side by side — gives the AI one clear mental model to follow. In contrast, boilerplates that split code across multiple apps, packages, and third-party integrations require the AI to navigate more files and follow more indirection, which increases the chance of inconsistencies and errors."
},
{
question: "How do I set up Claude Code for full-stack SaaS development?",
answer: "Install Claude Code, then add the Wasp plugin for deep framework knowledge. Create a new Open SaaS project with 'wasp new my-saas -t saas', set up your integrations (Stripe, email, etc.), and run 'wasp start' as a background task so Claude Code can see server logs and errors in real time. Pair this with browser automation tools like Chrome DevTools MCP for full-stack debugging visibility."
}
]} />

Conclusion

The best way to vibe code a SaaS in 2026 isn't about finding the flashiest AI tool, it's about giving your AI tool the right foundation to work with.

We suggest using Claude Code and giving it three things: a strong SaaS boilerplate starter kit, like Open SaaS, that gives AI a solid foundation to work with, LLM-friendly docs that help AI stay grounded without burning context, and full-stack debugging visibility that closes the feedback loop.

Open SaaS + Claude Code gives you all of this: a free, open-source, production-ready SaaS foundation, the structure and conventions AI needs to generate clean, consistent code, complete LLM-friendly docs for both the template and the framework, and the tools for full-stack debugging visibility. And because it's all free and open source there's no upfront cost, no vendor lock-in, and you can deploy wherever you want.

You spend your time on business logic and product, not the boring stuff.


Opensaas Sh Blog Src Content Docs Blog 2026 04 13 Good Specs Right Tools Pm Built Emergency Saas Opensaas (`opensaas-sh/blog/src/content/docs/blog/2026-04-13-good-specs-right-tools-pm-built-emergency-saas-opensaas.mdx`)


title: "Good Specs, Right Tools, Weeks to Production: How a PM Built an Emergency Preparedness SaaS with Open SaaS"
date: 2026-04-13
tags:

  • wasp
  • opensaas
  • user-interview
  • fullstack
  • chroni
    keywords:
  • "opensaas user interview"
  • "wasp framework saas"
  • "claude code saas development"
  • "pm built saas"
  • "non-engineer build saas"
  • "emergency preparedness saas"
  • "full-stack saas boilerplate"
  • "ai assisted saas development"
  • "open source saas starter kit"
  • "chroni emergency planning"
    authors: matija

import { Image } from 'astro:assets';
import chroniSignup from '../../../assets/chroni/Chroni-sign-up.png';
import VideoPlayer from '../../../components/VideoPlayer.astro';

Piotr Ciechowicz is a Product Manager at PepsiCo, where his day job is PowerBI dashboards, data pipelines, and enormous Excel files. On the side, he builds. His latest project, Chroni, is a multi-language emergency preparedness SaaS built entirely on Open SaaS and Wasp. He has a detailed emergency plan, and thinks your family should have one too.


Tell us about yourself, and what actually pushed you to build something.

My day job is PowerBI dashboards, data pipelines, enormous Excel files. I love it, but it doesn't scratch the itch of building something I actually want.

That itch started when I became a dad. I realized that comfort and safety means being prepared for anything. Diapers? Check. Band-aids? Check. But the list kept growing: pandemic, blackout, heat wave. My inventory stockpile got larger, things started expiring, so I introduced a kanban-like monitoring system for stock rotation.

Then I came across emergency handbooks, the kind that tell you what to do when things go south. In the US, there's a whole ecosystem of tools. In Europe, this meant mostly PDFs buried on a government website. As a parent, that bothered me. That's how Chroni became a thing.


What did you actually build, and what's under the hood?

Chroni is a browser-first emergency preparedness SaaS, multi-language, built for European families who want to be actually ready, not just have a PDF somewhere.

The content foundation is official emergency recommendations. I sourced handbooks from agencies across countries, BBK in Germany, RCB in Poland, and others, and extracted the core: what inventory you need, what emergency scenarios are covered, and how to act when they happen. The tricky part was harmonisation: BBK says one thing about water storage, RCB says something slightly different. I had to find universal best practices while preserving country-specific nuances, critical both for the guides and the inventory templates.

On top of that, I built:

  • Emergency guides and quizzes
  • Inventory management with a template of over 90 items, including emergency bag contents
  • A family emergency plan
  • Full multi-language support, currently English, German, and Polish

You're a PM, not an engineer. How did you pick a stack, and why Open SaaS?

Being a PM actually helped. I specced things out before touching any tooling, without falling in love with ideas or technology.

What I needed was clear: browser-first SaaS with auth and user management, payments, full-stack without having to stitch frontend, backend, and auth myself, which would have driven me mad before shipping. And it had to be AI-friendly. I used Claude Code throughout.

I looked at a few boilerplates. They felt messy. Then I found Open SaaS and Wasp.

Wasp describes itself as a full-stack React + Node.js + Prisma framework with superpowers. That's accurate, but for me the biggest differentiator was simpler: it thinks in products, not in files. And it met every criterion I had.

Open SaaS took it even further. From the first git clone, I immediately had:

  • Authentication, email and Google OAuth out of the box
  • Stripe integration, subscription management and webhook handling, done
  • Admin dashboard with user management
  • Landing page, ready to modify
  • Transactional email, configured and ready
  • Background jobs, for scheduled reminders, expiration date tracking, anything I'd need later

I didn't have to build any of that. The only task was replacing the demo content with Chroni. That saved me weeks, maybe months, of trial and error.


Did you consider no-code tools like Lovable or Replit?

I tried both. I just didn't want to argue with a middleman, because that's basically what it is. With Replit or Lovable it felt like I'm writing to an agency, like some sales guy trying to sell me things. Something is happening, and then I can chat with a chatbot that delivers code. I didn't feel like I could really affect anything. With Replit specifically, it hallucinated a lot, created things I didn't ask for, and the end result didn't feel secure.

The problem was that I didn't feel like I'm in control of things, I just didn't want to argue with a middleman, because that's basically what it is.

With Claude Code it's different, I feel like I have a coworker. A junior, probably. But a coworker. Depending on how I phrase what I want to build, with what constraints and guardrails, it will most likely deliver what I wanted.

Were there specific limitations that pushed you away?

The biggest one was user journey management. I could explain exactly what I needed and in the end get something similar, but not that thing. On top of that, loads of placeholders and phantom pages everywhere. Something that looks nice by the reference but isn't actually there. And role-based access, specific user roles, scoped access, that just wasn't possible, or at least it didn't feel like it was.

There was also a more deliberate reason I stepped away: I didn't want to use no-code tools, because I wanted to learn how to actually operate AI agents on a real project. Wasp with Claude Code kept me in the loop. I understood what was being built and where it lived.


Walk us through how the development actually worked.

My process was probably different from typical. I didn't start with code, I started with documents. Typical PRDs, specs, problem statements. I figured that if a vague prompt produces a vague response, well-structured requirements might produce something I could actually ship.

So the workflow was: define what I wanted to build, talk it through with Claude, brainstorm, expand with more context, review and iterate. The end result was a spec I agreed with and knew was buildable.

With specs done, the next challenge wasn't even code, it was content. Harmonising a dozen emergency handbooks turned out to be a serious data problem I totally underestimated.

On the coding side, Wasp's structure made a real difference for AI-assisted work. Claude Code had a clear pattern to follow, it understood where an action should go, how to define it, how to wire it up. The .wasp config file gives the model a map of the entire app. That's not a small thing when context is the precious resource.

The mindset of preparing specs before coding means the coding becomes the easy part.


Where did it actually hurt?

A few places. AI-generated technical debt was real: hardcoded values, placeholders, TODO comments scattered through the codebase. Where my personal discipline fell short, Wasp's structure picked up the slack.

Deployment on Railway was trickier than expected, and honestly that was my bad. At first I tried to deploy with Claude Code, but Claude by default didn't know how to do this correctly, which caused bundling: initial builds were packing everything into a single service instead of separating them properly. Once that part was sorted, the builds still failed frequently for different reasons. What worked for me in the end was a GitHub Actions pipeline: pushes to the production repo trigger consistent builds and deploys. It became a reusable template across projects, though I know now there are even better and easier ways to deploy safely.

Auth pages don't support multi-language out of the box. Chroni is a three-language product, so I had to work around that with CSS, which introduced some UI awkwardness that Claude Code wasn't great at fixing either.


What would you say to another PM or non-engineer considering Wasp?

If you're a PM, a designer, anyone who thinks in products rather than in code, I totally recommend Wasp. Paired with Open SaaS, it makes the whole 0 → 1 journey a lot faster. You can go from idea to deployed SaaS remarkably fast. Not "build a landing page" fast. End-to-end app fast.

Wasp's structure makes AI-assisted development actually work. Heck, even I understood what was what.

Prepare your specs before you write a line of code. The coding becomes the easy part. And lean on Open SaaS, the fact that auth, payments, and the admin dashboard are already there means you spend your time on the one thing only you can build.


Piotr Ciechowicz is a Product Manager and emergency preparedness advocate based in Berlin. You can find Chroni at chroni.eu.


Opensaas Sh Blog Src Content Docs Blog 2026 05 29 Seo And Ai Discoverability For Your Saas (`opensaas-sh/blog/src/content/docs/blog/2026-05-29-seo-and-ai-discoverability-for-your-saas.mdx`)


title: "How to Improve your App's AI Discoverability in 2026"
subtitle: "Three tips that will bring more human and AI traffic"
date: 2026-05-29
tags:

  • seo
  • ai
  • webdev
  • saas
    authors: vince
    keywords:
  • "ai discoverability for saas"
  • "llms.txt for web apps"
  • "json-ld for saas landing page"
  • "schema markup for saas"
  • "prerendering react app for seo"
  • "seo for single page apps"
  • "getting indexed by chatgpt"
  • "perplexity discoverability"
  • "open saas seo"
  • "wasp prerendering"
  • "ai crawler optimization"
  • "structured data for ai search"

import { Image } from 'astro:assets';
import lighthouseDevtools from '../../../assets/seo/lighthouse-devtools.png';
import googleBot from '../../../assets/seo/google-bot.png';

Most SaaS apps still treat discoverability like it's 2015. You set a <title>, you set a meta description, you check Google Search Console once a quarter, and you call it done. But in today's AI dominated landscape, a growing slice of product discovery traffic now comes from agents and LLMs (ChatGPT search, Perplexity, Claude), and those tools look for slightly different signals than Googlebot ever did.

The good news is that the fixes aren't hard. There are basically three things to add, and once they're in place your app is in much better shape for both classic search and the AI side of things.

TL;DR

Three building blocks every SaaS app should have in 2026:

  1. Prerendered HTML on marketing pages (e.g. landing pages) so crawlers see your content without running JavaScript.
  2. JSON-LD structured data so search engines and LLMs understand what your app actually does.
  3. An llms.txt file in your so AI tools have a curated map of your site.

We just added all three to Open SaaS, our free, open-source SaaS boilerplate starter built on Wasp. Snippets below show exactly what we ship, and they're easy to copy into any stack.

{/* truncate */}

Why the old playbook isn't enough

If your app is a Single Page Application (which most React, Next.js, Vue, and Svelte apps are by default), the initial HTML the server returns is mostly empty. The real content gets injected by JavaScript after the page loads it in the browser.

Googlebot (Google's site indexer) is actually decent at getting around this problem now. It runs JS, waits for the page to settle, and indexes what it finds. The problem is that most other crawlers are not so patient:

  • Bingbot, DuckDuckBot, and other classic crawlers either skip JS or render unreliably.
  • AI-side crawlers (OAI-SearchBot, PerplexityBot, ClaudeBot, and others) have inconsistent and generally limited JS rendering compared to Googlebot, so you shouldn't assume your client-rendered content is reachable.
  • LLMs answering a user's question often pull from cached snapshots or curl requests that were taken without JS execution at all.

So if the only place your value prop, pricing, and FAQ live is inside a React component that mounts client-side, you're basically invisible to a large chunk of the traffic you want.

Here are the three things you can do to fix that, in order of impact:

  1. Get your valuable content into the HTML the server sends.
  2. Tell crawlers what kind of app/site you have using structured data.
  3. Give LLMs a clean, curated index of your site.

1. Prerender your marketing pages

Prerendering means generating the real HTML of a page at build time, then letting React (or whatever) hydrate it in the browser. Visitors and crawlers both get the finished page on the first request, no JS execution required. As a bonus, the page feels a lot faster to load.

Run a Lighthouse audit (Chrome DevTools → Lighthouse tab) before and after turning on prerendering to see the load and paint improvements for your own page.

You only want this for public, content-heavy pages: the landing page, pricing, FAQ, blog index, that kind of thing. Don't prerender anything behind auth, because there's nothing useful to prerender as it's all user-specific content.

The mechanism depends on your stack, but every popular framework has a well-trodden path for it:

  • Next.js: static generation via generateStaticParams, or a full static export with output: 'export'.
  • Astro: it's a static-site generator by default, so you're already getting this for free.
  • Remix / React Router: server-render or prerender routes on demand.
  • Vite + React: add a prerender plugin like vite-plugin-ssr or vike.
  • Wasp (what Open SaaS uses): add prerender: true to a route and it will write out the static HTML for you.

The specific tool matters much less than the outcome: your landing page returns real content on the first byte, and your client framework still hydrates and takes over so links, modals, and any interactive bits keep working the same way.

2. Add JSON-LD structured data

Meta tags tell crawlers about a page. Structured data tells them what the page is. It's the difference between "this page has the word 'pricing' in the title" and "this is a SoftwareApplication with the following offers".

Meta tags versus structured data Meta tags give crawlers loose keywords to guess from, while JSON-LD structured data tells them exactly what the page is. META TAGS pricing SaaS fast app ? Sees words. Has to guess.

vs

JSON-LD { "@type": "SoftwareApplication", "name": "Acme", "offers": { "price": "0" } }

Knows exactly what it is.

Most developers already know the first half of this: the meta tags in your HTML <head>. Those are your <title>, <meta name="description">, and the Open Graph and Twitter card tags that decide how your links look when they're shared. They're worth getting right, and Open SaaS already fills in the important ones for you, so you mostly just swap in your own copy. You can see how they're wired up in the SEO & Performance guide.

Structured data is the half most people skip. It's far less common than meta tags but just as important today, especially now that LLMs lean on it to figure out and recommend what your product actually is.

Schema.org is the vocabulary, and JSON-LD is the format Google (and most LLMs) prefer. You drop a <script type="application/ld+json"> into your HTML, and that's it. There's no rendering impact, no layout shift, nothing to style.

For a SaaS landing page, the types worth including are:

  • SoftwareApplication: name, description, URL, category, image, pricing, license. Use the more specific WebApplication subtype if your app runs entirely in the browser.
  • WebSite: site-level identity and links to social profiles.
  • Organization: who's behind the app.
  • FAQPage: turns your FAQ section into rich snippets in Google, and gives LLMs clean Q&A pairs to cite when someone asks about your product.

LLMs love this stuff because it's unambiguous. Instead of inferring what your product is from marketing copy, they can read a structured record that says exactly what it is, what it costs, and who makes it. It's basically a machine-readable elevator pitch.

Here's a complete example you can drop into any React app. It's the trimmed SchemaMarkup component we ship in Open SaaS, but there's nothing framework-specific about it. The same JSON object works as a plain <script> tag in any HTML page:

tsx title="src/landing-page/components/schemamarkup.tsx"
const schema = {
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "SoftwareApplication",
      "@id": "https://your-saas-app.com/#software",
      name: "Your Open SaaS App",
      description: "Your app's main description and features.",
      url: "https://your-saas-app.com",
      applicationCategory: "BusinessApplication",
      operatingSystem: "Cross-platform",
      image: "https://your-saas-app.com/public-banner.webp",
      offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
    },
    {
      "@type": "WebSite",
      "@id": "https://your-saas-app.com/#website",
      url: "https://your-saas-app.com",
      name: "Your Open SaaS App",
      description: "Your app's main description and features.",
    },
  ],
};

export function SchemaMarkup() {
  return <script type='application/ld+json'>{JSON.stringify(schema)}</script>;
}

You import it once into your landing page and forget about it. Because the landing page is prerendered (see step 1), the script ends up inlined in the static HTML, which is exactly where crawlers and LLMs want to see it.

If your landing page has an FAQ section, add a FAQPage entry too. It's one of the highest-leverage additions for both Google rich snippets and AI answers, because Q&A is already the shape LLMs want to quote.

When you're done, validate with Google's Rich Results Test and the Schema.org validator before deploying.

:::tip[Don't want to wire this up yourself?]
All three of these techniques now ship with the Open SaaS starter, pre-configured and ready to customize. If you're building on it, the prerendered routes, schema markup, and llms.txt are already in place. You can read the rest of this as background and skip to what's new.
:::

3. Publish an llms.txt file

llms.txt is a fairly new convention. It's a markdown file at the root of your site that gives LLMs a curated index of the pages and resources you actually want them to know about.

It's intentionally not a sitemap. A sitemap is exhaustive and aimed at crawlers. llms.txt is short, hand-picked, and aimed at language models trying to answer questions about you. Think of it as a README for your site, written for an AI.

The minimum useful version is:

md title="public/llms.txt"
# Your SaaS App

> One-line description of what your SaaS does and who it's for.

A longer paragraph giving context: the problem your app solves, the kind of
user it serves, and the key value it delivers.

## Core pages
- [Home](https://your-saas-app.com): Landing page with features, pricing, and FAQ
- [Pricing](https://your-saas-app.com/pricing): Subscription plans and pricing

## Documentation

- [Docs](https://docs.your-saas-app.com): Product documentation and guides

## Resources

- [Blog](https://your-saas-app.com/blog): Updates, tutorials, and announcements
- [GitHub](https://github.com/your-org/your-repo): Open source code

Drop it at public/llms.txt and it's served at https://your-saas-app.com/llms.txt.

If you've got a docs site or a blog, it's worth generating the file at build time so it stays current. We do that for opensaas.sh with a small Node script at scripts/generate-llms-txt.mjs. It reads the blog frontmatter and writes out an updated llms.txt before each deploy. About 90 lines, no dependencies, easy to lift into any project. There are also some plugins and npm packages that do this automatically, but I find that having custom control over what goes in the file is better.

Keep the file short and intentional. Think of it more like an index or a map so LLMs and agents know where to go to find the most relevant content. You can check out the Open SaaS llms.txt for a proper example in production.

How the three fit together

Here's a quick recap on why these three tips fit together:

  • Prerendering makes your content broadly visible to all crawlers and AI bots and improves site performance (e.g. load speed).
  • JSON-LD makes that content understandable to search engines and AI.
  • llms.txt makes your site easy for AI tools to navigate.

It's important to note that without prerendering, your structured data will just sit in a JS bundle that classic and AI crawlers might never execute. So my advice would be to get prerendering for your landing page in place first, then layer the other two on top.

What's new in Open SaaS

We just shipped all three of these building blocks in the Open SaaS template, so if you're using it you don't have to wire any of this up from scratch:

  • prerender: true is on by default for the landing page and pricing page.
  • A JSON-LD SchemaMarkup component lives at src/landing-page/components/SchemaMarkup.tsx and is already rendered on the landing page.
  • A placeholder llms.txt ships at template/app/public/llms.txt.
  • Docs, like the SEO & Performance, the guided tour, plus deploying guides all walk you through customizing all of it before you ship:
    • The head meta tags in the main config file (title, description, OG tags).
    • The schema object in SchemaMarkup.tsx (name, URL, description, FAQ entries).
    • The public/llms.txt file (replace placeholder URLs and copy).

It's about 15 minutes of work and your landing page is in good shape for both search engines and the AI side of search.

Wrapping up

If you only do one of these, do prerendering. It fixes the foundational problem (content not in HTML) that everything else depends on. Once that's in, JSON-LD and llms.txt are both small, isolated additions with outsized payoff.

If you want a working reference for all of it, Open SaaS is free and open-source, and the GitHub repo has the actual code for the schema component, the llms.txt generator, and the prerendered routes. If you build something cool with it, drop in our Discord and let us know.

And if you found this useful, a star on GitHub helps more devs find the project. Thanks for reading.


Opensaas Sh Blog Src Content Docs General Admin Dashboard (`opensaas-sh/blog/src/content/docs/general/admin-dashboard.mdx`)


title: Admin Dashboard
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import adminDashboard from '@assets/admin/admin-dashboard.png';
import dbStudio from '@assets/stripe/db-studio.png';
import { Image } from 'astro:assets';

This is a reference on how the Admin dashboard, available at /admin, is set up.

Permissions

The Admin dashboard is only accessible to users with the isAdmin field set to true.

prisma title="schema.prisma" {7}
model User {
  id                        String          @id @default(uuid())
  createdAt                 DateTime        @default(now())

  email                     String?         @unique
  username                  String?         @unique
  isAdmin                   Boolean         @default(false)
  //...

To give yourself administrator priveledges, make sure you add your email addresses to the ADMIN_EMAILS environment variable in .env.server file before registering/logging in with that email address:

sh title=".env.server"
[email protected]

// or add many admins with a comma-separated list

[email protected],[email protected],[email protected]

Or if you've already logged in with an email address that you want to give admin priveledges to, you can run the following command in a separate terminal window to update the user's isAdmin field manually:

sh
wasp db studio

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Admin Dashboard Pages

Analytics Dashboard

The Admin analytics dashboard is a single place for you to view your most important metrics and perform some admin tasks. At the moment, it pulls data from:

  • Payment Processor:
    • Total revenue.
    • Revenue for each day of the past week.
  • Google or Plausible:
    • Total number of page views (non-unique).
    • Percentage change in page views from the previous day.
    • Top sources/referrers with unique visitor count (i.e. how many people came from that source to your app).
  • Database:
    • Total number of registered users.
    • Daily change in number of registered users .
    • Total number of paying users.
    • Daily change in number of paying users.

These metrics are aggregated within the background job calculateDailyStatsJob, which by default is run every hour. You can change the frequency of this job by modifying its cron field:

ts title="src/analytics/analytics.wasp.ts" {8}
import { job, type Spec } from "@wasp.sh/spec";
import { calculateDailyStatsJob } from "./stats" with { type: "ref" };

export const analyticsSpec: Spec = [
  job(calculateDailyStatsJob, {
    executor: "PgBoss",
    schedule: {
      cron: "0 * * * *", // Every hour. useful in production.
      // cron: "* * * * *" // Every minute. useful for debugging.
    },
    entities: ["User", "DailyStats", "Logs", "PageViewSource"],
  }),
];

For more info on Wasp's recurring background jobs, check out the Wasp Jobs docs.

For a guide on how to integrate these services so that you can view your analytics via the dashboard, check out the Payment Integrations and Analytics guide of the docs.

:::note[Help us improve]
We're always looking to improve the Admin dashboard. If you feel something is missing or could be improved, consider opening an issue or submitting a pull request
:::

Users

The Users page is where you can view all your users and their most important details. You can also search and filter users by:

  • Email address.
  • Subscription/payment status.
  • Admin status.

Opensaas Sh Blog Src Content Docs General Privacy Policy (`opensaas-sh/blog/src/content/docs/general/privacy-policy.mdx`)


title: Privacy Policy sidebar: hidden: true

OpenSaaS.sh

Last updated: 2026-01-13

OpenSaaS.sh ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website (https://opensaas.sh).

Please read this privacy policy carefully. If you do not agree with the terms of this privacy policy, please do not access the site.


What Data Do We Collect?

OpenSaaS.sh collects the following types of personal data:

  • IP address
  • browser type
  • pages visited
  • time spent on pages

How Do We Collect Your Data?

You directly provide OpenSaaS.sh with most of the data we collect. We collect data and process data when you:

  • Use or view our website via your browser's cookies.

How Will We Use Your Data?

OpenSaaS.sh collects your data so that we can:

  • Analyze website traffic
  • Improve our website
  • Understand how visitors use our site

Third-Party Services

We use third-party services to help us operate our website, conduct our business, and provide services to you. These services may collect information about your visit to our website.

We use the following third-party services:

  • Google Analytics
  • REO.dev

These third-party service providers have their own privacy policies addressing how they use your information. We encourage you to review their privacy policies.

We only share information with these parties as necessary to provide our services to you, and they are obligated to keep your information confidential and secure.

How Do We Store Your Data?

OpenSaaS.sh securely stores your data using industry-standard security measures.

Security Measures

We implement the following security measures to protect your data:

  • SSL/TLS encryption
  • Secure cloud infrastructure

Data Retention

OpenSaaS.sh will keep your personal data for as long as necessary to fulfill the purposes outlined in this privacy policy. Once this period has expired, we will delete your data securely.

Marketing

OpenSaaS.sh would like to send you information about products and services of ours that we think you might like.

If you have agreed to receive marketing, you may always opt out at a later date. You have the right at any time to stop OpenSaaS.sh from contacting you for marketing purposes.

If you no longer wish to be contacted for marketing purposes, please contact us at [email protected].

Your Data Protection Rights

OpenSaaS.sh would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:

The right to access – You have the right to request copies of your personal data. We may charge you a small fee for this service.

The right to rectification – You have the right to request that we correct any information you believe is inaccurate. You also have the right to request that we complete information you believe is incomplete.

The right to erasure – You have the right to request that we erase your personal data, under certain conditions.

The right to restrict processing – You have the right to request that we restrict the processing of your personal data, under certain conditions.

The right to object to processing – You have the right to object to our processing of your personal data, under certain conditions.

The right to data portability – You have the right to request that we transfer the data that we have collected to another organization, or directly to you, under certain conditions.

If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us at:

Cookies

Cookies are text files placed on your computer to collect standard Internet log information and visitor behavior information. When you visit our website, we may collect information from you automatically through cookies or similar technology.

For further information, visit allaboutcookies.org.

How Do We Use Cookies?

OpenSaaS.sh uses cookies in a range of ways to improve your experience on our website, including:

  • Functional cookies: Keeping you signed in and remembering your preferences
  • Analytics cookies: Understanding how you use our website to improve our services

Managing Cookies

You can set your browser not to accept cookies, and the website allaboutcookies.org tells you how to remove cookies from your browser. However, in a few cases, some of our website features may not function as a result.

Privacy Policies of Other Websites

Our website may contain links to other websites. Our privacy policy applies only to our website, so if you click on a link to another website, you should read their privacy policy.

Changes to Our Privacy Policy

OpenSaaS.sh keeps its privacy policy under regular review and places any updates on this web page. This privacy policy was last updated on 2026-01-13.

How to Contact Us

If you have any questions about OpenSaaS.sh's privacy policy, the data we hold on you, or you would like to exercise one of your data protection rights, please do not hesitate to contact us at:

How to Contact the Appropriate Authority

Should you wish to report a complaint or if you feel that OpenSaaS.sh has not addressed your concern in a satisfactory manner, you may contact your local data protection supervisory authority.


This privacy policy is based on the GDPR privacy policy template from GDPR.eu.


Opensaas Sh Blog Src Content Docs Guides Analytics (`opensaas-sh/blog/src/content/docs/guides/analytics.md`)


title: Analytics
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


This guide will show you how to integrate analytics for your app. You can choose between Google Analytics and Plausible.

Google Analytics is free, but uses cookies, so you'll probably want/need to implement the Cookie Consent Modal when using it.

Plausible is an open-source, privacy-friendly alternative to Google Analytics. You DO NOT have to use the cookie consent modal with Plausible, as it does not use cookies. It's also easier to use than Google if you use their hosted service, but be aware it is a paid feature. It is completely free if you want to self-host it, although this comes with some additional setup steps.

If you're looking to add analytics to your blog, you can follow the Adding Analytics to your Blog section at the end of this guide.

Plausible

Hosted Plausible

Sign up for a hosted Plausible account here.

Once you've signed up, you'll be taken to your dashboard. Create your site by adding your domain. Your domain is also your PLAUSIBLE_SITE_ID in your .env.server file. Make sure to add it.

sh
PLAUSIBLE_SITE_ID=<your domain without www>

After adding your domain, you'll be taken to a page with your Plausible script tag. Copy and paste this script tag into the app's head config, defined in the src/client/head.wasp.ts file.

ts title="src/client/head.wasp.ts" {5}
import { type App } from "@wasp.sh/spec";

export const head: App["head"] = [
  // ...
  "<your plausible script tag here>",
];

Go back to your Plausible dashboard, click on your username in the top right, and click on the Settings tab. Scroll down, find your API key and paste it into your .env.server file under the PLAUSIBLE_API_KEY variable.

:::note[No Cookies]
Plausible does not use cookies, so you don't need to add it to your Cookie Consent Modal, hence the script can be added directly to the app's head config in the src/client/head.wasp.ts file.
:::

Self-hosted Plausible

Plausible, being an open-source project, allows you to self-host your analytics. This is a great option if you want to keep your data private and not pay for the hosted service.

coming soon...

...until then, check out the official documentation.

:::tip[Contribute!]
If you'd like to help us write this guide, click the "Edit page" button at the bottom of this page

As a completely free, open-source project, we appreciate any help 🙏
:::

Google Analytics

First off, head over to src/analytics/stats.ts and switch out the Plausible Provider for Google Analytics so that your background (cron) job fetches the data from Google Analytics for your Admin Dashboard:

ts ins={3} del={2} title="stats.ts"
//...
import { getDailyPageViews, getSources } from './providers/plausibleAnalyticsUtils';
import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';

export const calculateDailyStats: DailyStatsJob<never, void> = async (_args, context) => { 
  //...
}

Next, make sure you sign up for Google analytics, then go to your Admin panel in the bottom of the left sidebar and then create a "Property" for your app.

Once you've created a new Property, some Installation Instructions will pop up. Select install manually where you should see a string that looks like this:

sh title=""
"">https://www.googletagmanager.com/gtag/js?id=<your-google-analytics-id>

and copy and paste the Google Analytics ID into your .env.client file to get it working with the Cookie Consent Modal provided with this template:

sh title=".env.client"
REACT_APP_GOOGLE_ANALYTICS_ID=<your-google-analytics-id> # e.g. G-1234567890

:::tip[noscript]
In the Installation Instructions, Google Tag Manager might also instruct you to paste the noscript code snippet immediately after the opening `` tag.
You should skip this step because this snippet is activated only if users try to browse your app without JavaScript enabled, which is very rare and React needs JS anyway.
:::

Then, set up the Google Analytics API access by following these steps:

  1. Set up a Google Cloud project: If you haven't already, start by setting up a project in the Google Cloud Console.

  2. Enable the Google Analytics API for your project: Navigate to the "Library" in the Google Cloud Console and search for the "Google Analytics Data API" (for Google Analytics 4 properties) and enable it.

  3. Create credentials: Now go to the "Credentials" tab within your Google Cloud project, click on + credentials, and create a new service account key. First, give it a name. Then, under "Grant this service account access to project", choose viewer.

  4. Create Credentials: When you go back to Credentials page, you should see a new service account listed under "Service Accounts". It will be a long email address to ends with @your-project-id.iam.gserviceaccount.com. Click on the service account name to go to the service account details page.

    • Under "Keys" in the service account details page, click "Add Key" and choose Create new key.

    • Select "JSON", then click "Create" to download your new service account's JSON key file. Keep this file secure and don't add it to your git repo as it grants access to your Google Analytics data.

  5. Update your Google Anayltics Settings: Go back to your Google Analytics dashboard, and click on the Admin section in the left sidebar. Under Property Settings > Property > Property Access Management Add the service account email address (the one that ends with @your-project-id.iam.gserviceaccount.com) and give it Viewer permissions.

  6. Encode and add the Credentials: Add the client_email and the private_key from your JSON Key file into your .env.server file. But be careful! Because Google uses a special PEM private key, you need to first convert the key to base64, otherwise you will run into errors parsing the key. To do this, in a terminal window, run the command below and paste the output into your .env.server file under the GOOGLE_ANALYTICS_PRIVATE_KEY variable:

    sh
    echo -n "-----BEGIN PRIVATE KEY-----\nMI...A++eK\n-----END PRIVATE KEY-----\n" | base64
  7. Add your Google Analytics Property ID: You will find the Property ID in your Google Analytics dashboard in the Admin > Property > Property Settings > Property Details section of your Google Analytics property (not your Google Cloud console). Add this 9-digit number to your .env.server file under the GOOGLE_ANALYTICS_PROPERTY_ID variable.

Adding Analytics to your Blog

To add your analytics script to your Astro Starlight blog, all you need to do is modify the head property in your blog/astro.config.mjs file.

Below is an example of how to add Google Analytics to your blog:

js
export default defineConfig({
  site: 'https://opensaas.sh',
  integrations: [
    starlightBlog({ 
      // ...
    }),
    starlight({
      //...
       head: [
        {
          tag: 'script',
          attrs: {
            src: 'https://www.googletagmanager.com/gtag/js?id=<YOUR-GOOGLE-ANALYTICS-ID>',
          },
        },
        {
          tag: 'script',
          content: `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
        
          gtag('config', '<YOUR-GOOGLE-ANALYTICS-ID>');
          `,
        },
      ],

Opensaas Sh Blog Src Content Docs Guides Authentication (`opensaas-sh/blog/src/content/docs/guides/authentication.md`)


title: Authentication
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


Setting up your app's authentication is easy with Wasp. In fact, it's already set up for you in the src/auth/auth.wasp.ts file:

ts title="src/auth/auth.wasp.ts"
export const authConfig: Auth = {
  userEntity: "User",
  methods: {
    email: emailAuthMethod,
    google: googleAuthMethod,
    gitHub: gitGubAuthMethod,
    discord: discordAuthMethod,
  },
  onAuthFailedRedirectTo: "/login",
  onAuthSucceededRedirectTo: "/demo-app",
};

The great part is, by defining your auth config in the Wasp Spec, Wasp manages most of the Auth process for you, including the auth-related database entities for user credentials and sessions, as well as auto-generated client components for your app on the fly (aka AuthUI -- you can see them in use in the src/auth folder).

Email Verified Auth

email method is the default auth method in Open SaaS.

Since it needs to send emails to verify users and reset passwords, it requires an email sender provider: a service it can use to send emails.
A "email sender" provider is configured via the app's emailSender field, defined in the src/server/emailSender.wasp.ts file.

:::caution[Dummy Email Provider]
To make it easy for you to get started, Open SaaS initially comes with the Dummy "email sender" provider, which does not send any emails, but instead logs all email verification links/tokens to the server's console!
You can then follow these links to verify the user and continue with the sign-up process.

ts title="src/server/emailsender.wasp.ts"
import { type EmailSender } from "@wasp.sh/spec";

export const emailSender: EmailSender = {
  provider: "Dummy", // Logs all email verification links/tokens to the server's console.
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]",
  },
};

You can not use the Dummy provider in production and your app will not build until you move to a production-ready provider, such as SendGrid. We outline the process of migrating to SendGrid below.
:::

In order to use the email auth method in production, you'll need to switch from the Dummy "email sender" provider to a production-ready provider like SendGrid:

  1. First, set up your app's emailSender in the src/server/emailSender.wasp.ts file by following this guide.
  2. Add your SENDGRID_API_KEY to the .env.server file.
  3. Make sure the email address you use in the fromField object is the same email address that you configured your SendGrid account to send out emails with. In the end, your config should look something like this:
ts title="src/auth/auth.wasp.ts" {3,5}
const emailAuthMethod: EmailAuthConfig = {
  fromField: {
    name: "Open SaaS App",
    // When using SendGrid, you must use the same email address that you configured your account to send out emails with!
    email: "[email protected]",
  },
  // ...
};
ts title="src/server/emailsender.wasp.ts" del={2} ins={3}
export const emailSender: EmailSender = {
  provider: "Dummy",
  provider: "SendGrid",
  defaultFrom: {
    name: "Open SaaS App",
    // When using SendGrid, you must use the same email address that you configured your account to send out emails with!
    email: "[email protected]",
  },
};

And that's it. Wasp will take care of the rest and update your AuthUI components accordingly.

Check out the Wasp Auth docs for more info.

Google, GitHub, & Discord Auth

We've also customized and pre-built the Google, Discord and GitHub auth flow for you. To start using them, you just need to uncomment out the methods you want in your src/auth/auth.wasp.ts file and obtain the proper API keys to add to your .env.server file.

To create a Google OAuth app and get your Google API keys, follow the instructions in Wasp's Google Auth docs.

To create a GitHub OAuth app and get your GitHub API keys, follow the instructions in Wasp's GitHub Auth docs.

To create a Discord OAuth app and get your Discord API keys, follow the instructions in Wasp's Discord Auth docs

Again, Wasp will take care of the rest and update your AuthUI components accordingly.


Opensaas Sh Blog Src Content Docs Guides Authorization (`opensaas-sh/blog/src/content/docs/guides/authorization.md`)


title: Authorization
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


This guide will help you get started with authorization in your SaaS app.

Authorization refers to what users can access in your app. This is useful for differentiating between users who have paid for different subscription tiers (e.g. "hobby" vs "pro"), or between users who have admin privileges and those who do not.

Authorization differs from authentication in that authentication refers to the process of verifying that a user is who they say they are (e.g. logging in with a username and password).

To learn more about the different types of user permissions built into this SaaS template, including Stripe subscription tiers and statuses, check out the User Overview Reference.

Also, check out our blog post to learn more about authorization (access control) in web apps.

Client-side Authorization

Open Saas starts with all users having access to the landing page (/), but only authenticated users having access to the rest of the app (e.g. to the /demo-app, or to the /account).

To control which pages require users to be authenticated to access them, you can set the authRequired property of the corresponding page definition in your Wasp Spec:

ts title="src/user/user.wasp.ts" {5}
import { page, route, type Spec } from "@wasp.sh/spec";
import { AccountPage } from "./AccountPage" with { type: "ref" };

export const userSpec: Spec = [
  route("AccountRoute", "/account", page(AccountPage, { authRequired: true })),
];

This will automatically redirect users to the login page if they are not logged in while trying to access that page.

:::caution[Client-side authorization is just for the looks]
Users can manipulate the client code as they wish, meaning that client-side authorization (access control) serves the purpose of ergonomics/user experience, not the purpose of restricting access to sensitive data.

This means that authorization in the client code is a nice-to-have: it is here to make sure users don't get lost in the part of the app they can't work with because data is missing due to them not having access, not to actually restrict them from doing something.

Actually ensuring they don't have access to the data, that is on the server to ensure, via server-side logic that you will implement for authorization.
:::

If you want more fine-grained control over what users can access, there are two Wasp-specific options:

  1. When you define the authRequired: true property on the page definition, Wasp automatically passes the User object to the page component. Here you can check for certain user properties before authorizing access:
tsx title="examplepage.tsx" "{ user }: { user: user }"
import { type User } from "wasp/entities";

export function ExamplePage({ user }: { user: User }) {
  if (user.subscriptionStatus === 'past_due') {
    return (<span>Your subscription is past due. Please update your payment information.</span>)
  }
  if (user.subscriptionStatus === 'cancel_at_period_end') {
    return (<span>Your susbscription will end on 01.01.2024</span>)
  }
  if (user.subscriptionStatus === 'active') {
    return (<span>Thanks so much for your support!</span>)
  }
};
  1. Or you can take advantage of the useAuth hook and check for certain user properties before authorizing access to certain pages or components:
tsx title="examplecomponent.tsx" {1, 4}
import { useAuth } from "wasp/client/auth";

export function ExampleComponent() {
  const { data: user } = useAuth();

  return (
    <h1> Hi {user.email || 'there'} 👋 </h1>
  )
}

Server-side Authorization

Authorization on the server-side is the core of your access control logic, and determines what users actually can or can't do (unlike client-side authorization logic which is there merely for UX).

You can authorize access to server-side operations by adding a check for a logged-in user on the context.user object which is passed to all operations in Wasp:

tsx title="src/server/actions.ts" "context.user"
export const someServerAction: SomeServerAction<...> = async (args, context) => {
  if (!context.user) {
    throw new HttpError(401); // Throw an error if user is not logged in.
  }

  if (context.user.subscriptionStatus === 'past_due') {
    throw new HttpError(403, 'Your subscription is past due. Please update your payment information.');
  }
  //...
}


title: Cookie Consent Modal
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import cookieBanner from '@assets/cookie-consent/cookiebanner.png';
import preferences from '@assets/cookie-consent/preferences.png';
import { Image } from 'astro:assets';

Cookie consent banners are annoying, we know. But they are legally required in many countries, so we have to deal with them.

This guide will help you dynamically add or remove cookies from your app via the cookie consent modal that comes with this template.

This is needed for non-essential cookies that are not necessary for the basic functionality of your app, such as analytics cookies or marketing cookies.

The cookie consent modal can be found at app/src/client/components/cookie-consent/ and contains two main files:

  1. Banner.tsx - the component that displays the banner at the bottom of the page.
  2. Config.ts - the configuration file that contains the cookies/scripts that will be dynamically added.

The Banner.tsx component is imported in app/src/client/App.tsx and is rendered at the bottom of the page, while all the changes to the banner itself are done within the Config.ts file, which we explain below.

Configuration

We decided to use the vanilla-cookieconsent library to handle the cookie consent. We've set it up to give you some basic functionality, using mostly the default settings. For a full list of options, you can check the official documentation.

Below, we will guide you through the necessary steps to get the cookie consent modal set up for your app.

Google Analytics

What's impotant to note for this template is that we are simply using the onAccept callbacks to dynamically add or remove our Google Analytics cookies from the page. In order for it to work correctly with your app, you need to add your Google Analytics ID to your .env.client file.

sh title=".env.client"
REACT_APP_GOOGLE_ANALYTICS_ID=G-1234567890

And that's it! The cookie consent modal will now dynamically add or remove the Google Analytics cookies based on the user's choice.

To check if it's working correctly, you can open the browser's developer tools and check the cookies tab. You should see the following cookies being added or removed based on the user's choice:

sh
_ga
_ga... # Google Analytics cookies.
cc_cookie # Cookie Consent cookie. The name of this cookie can be changed in the config file.

Plausible Analytics

If you decide to go with Plausible Analytics, you DO NOT need to ask users for their consent to use cookies because Plausible, as a privacy-first analytics provider, does not use cookies. Instead, It collects website usage data anonymously and in aggregate form only, without any personally identifiable information

By avoiding cookies, Plausible Analytics avoids the need for cookie consent banners.

Your Terms / Privacy Policy

You should also add a link to your terms and privacy policy within consentModal section of config.language:

ts title="config.ts" {10,11}
language: {
      default: 'en',
      translations: {
        en: {
          consentModal: {
            title: 'We use cookies',
            // ...
            // TODO: Add your own privacy policy and terms and conditions links below.
            footer: `
            <a href="<your-url-here>" target="_blank">Privacy Policy</a>
            <a href="<your-url-here>" target="_blank">Terms and Conditions</a>
                    `,
          },
        },
      },
    }

Allowing Users to Control Certain Cookies (OPTIONAL)

If you've added more than just Google Analytics cookies to your app, you can allow users to control which cookies they want to accept or reject. For example, if you've added marketing cookies, you can add a button to the modal that allows users to reject them, while accepting analytics cookies.

To do that, you can change the preferencesModal.sections property in config.language. Any section that you add to preferencesModal.sections must match a linkedCategory in the config.categories property. Make sure you also add a showPreferencesBtn property to consentModal (highlighted below).

Below is an example of what your config might look like if you want to give users the option to control over multiple cookie preferences:

text
/* Detailed source-code truncated for AI context efficiency. */

For more information on how to do that, check the official documentation.


Opensaas Sh Blog Src Content Docs Guides Deploying (`opensaas-sh/blog/src/content/docs/guides/deploying.mdx`)


title: Deploying
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import stripeListenEvents from '@assets/stripe/listen-to-stripe-events.png';
import npmVersion from '@assets/stripe/npm-version.png';
import stripeSigningSecret from '@assets/stripe/stripe-webhook-signing-secret.png';
import { Image } from 'astro:assets';

Because this SaaS app is a React/NodeJS/Postgres app built on top of Wasp, Open SaaS can take advantage of Wasp's easy, one-command deploy or manual deploy to any provider of your choice.

The simplest and quickest option is to take advantage of Wasp's one-command deploy to Fly.io or Railway.

Or if you prefer to deploy to a different provider, or your frontend and backend separately, you can follow the Deploying Manually section below.

Steps for Deploying

These are the steps necessary for you to deploy your app. We recommend you follow these steps in order.

Each of these steps is covered in more detail below.

Prerequisites

AWS S3 CORS configuration

If you're storing files in AWS S3, ensure you've listed your production domain
in the bucket's CORS configuration under AllowedOrigins. Check the File
uploading guide
for details.

Env Vars

Make sure you've got all your API keys and environment variables set up before you deploy.

Payment Processor Vars

In the Payments Processor integration guide, you set up your API keys using test keys and test product ids. You'll need to get the live/production versions of those keys. To get these, repeat the instructions in the Integration Guide without being in test mode. Add the new keys to your deployed environment secrets.

Other Vars

Many of your other environment variables will probably be the same as in development, but you should double-check that they are set correctly for production.

Here are a list of all of them (some of which you may not be using, e.g. Analytics, Social Auth) in case you need to check:

General Vars

  • DATABASE_URL
  • JWT_SECRET
  • PORT (most hosting providers set this for you)
  • WASP_WEB_CLIENT_URL
  • WASP_SERVER_URL

Open AI API Key

  • OPENAI_API_KEY

Sendgrid API Key

  • SENDGRID_API_KEY

Social Auth Vars

  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • GITHUB_CLIENT_ID
  • GITHUB_CLIENT_SECRET

Analytics Vars

  • REACT_APP_PLAUSIBLE_ANALYTICS_ID (for client-side)
  • PLAUSIBLE_API_KEY
  • PLAUSIBLE_SITE_ID
  • PLAUSIBLE_BASE_URL
  • REACT_APP_GOOGLE_ANALYTICS_ID (for client-side)
  • GOOGLE_ANALYTICS_CLIENT_EMAIL
  • GOOGLE_ANALYTICS_PROPERTY_ID
  • GOOGLE_ANALYTICS_PRIVATE_KEY
    (Make sure you convert the private key within the JSON file to base64 first with echo -n "PRIVATE_KEY" | base64. See the Analytics docs for more info)

AWS S3 Vars

  • AWS_S3_IAM_ACCESS_KEY
  • AWS_S3_IAM_SECRET_KEY
  • AWS_S3_FILES_BUCKET
  • AWS_S3_REGION

:::tip[Deployed? Get some swag! 👕]

Do you have an Open SaaS app running in production? If yes, we'd love to send some swag your way! All you need to do is fill out this form and we'll make it happen.

:::

Adding Server Redirect URL's to Social Auth

After deploying your server, you need to add the correct redirect URIs to the credential settings. For this, refer to the guides from the Wasp Docs.

Payment Providers Production Settings

Stripe

Now you need to set up your stripe webhook for production use. Below are some important steps and considerations you should take as you prepare to deploy your app to production.

Stripe API Versions

When you create your Stripe account, Stripe will automatically assign you to their latest API version at that time. This API version is important because it determines the structure of the responses Stripe sends to your webhook, as well as the structure it expects of the requests you make toward the Stripe API.

Because this template was built with a specific version of the Stripe API in mind, it could be that your Stripe account is set to a different API version.

:::note

ts title="stripeapi.ts"
const STRIPE_API_VERSION = "2025-04-30.basil"; // or e.g. 2023-08-16 if using older format

export const stripeApi = new Stripe(env.STRIPE_API_KEY, {
  apiVersion: STRIPE_API_VERSION,
});

When you specify a specific API version in stripeApi, the requests you send to Stripe from your server, along with their responses, will match that API version. On the other hand, Stripe will send all other events to your webhook that didn't originate as a request sent from your server, like those made after a user completes a payment on checkout, using the default API version of the API.

This is why it's important to make sure the API version in stripeApi also matches the API version in your Stripe account, and to thoroughly test any changes you make to stripeApi before deploying to production.
:::

To make sure your app is consistent with your Stripe account, here are some steps you can follow:

  1. You can find your default API version in the Stripe dashboard under the Developers section.
  2. Check that the API version in your /src/payment/stripe/stripeApi.ts file matches the default API version in your dashboard:
    ts title="stripeapi.ts" {1}
    const STRIPE_API_VERSION = "2025-04-30.basil"; // or e.g. 2023-08-16 if using older format
    
    export const stripeApi = new Stripe(env.STRIPE_API_KEY, {
      apiVersion: STRIPE_API_VERSION,
    });
  3. If they don't match, you can upgrade/downgrade your Stripe NPM package in package.json to match the API version in your dashboard:
  • If your default version on the Stripe dashboard is also the latest version of the API, you can simply upgrade your Stripe NPM package to the latest version.
  • If your default version on the Stripe dashboard is not the latest version, and you don't want to upgrade to the latest version, because e.g. you have other projects that depend on the current version, you can find and install the Stripe NPM package version that matches your default API version by following these steps:
    • Find and note the date of your default API version in the developer dashboard.

    • Go to the Stripe NPM package page and hover over Published date column until you find the package release that matches your version. For example, here we find the NPM version 13.x.x matches the API version of 2023-08-16.

    • Install the correct version of the Stripe NPM package by running:

      sh
      npm install [email protected] # e.g. npm install [email protected]
  1. Test your app thoroughly to make sure that the changes you made to stripeApi are working as expected before deploying to production.

Creating Your Production Webhook

  1. Go to https://dashboard.stripe.com/webhooks

  2. Click on + add endpoint

  3. Enter your endpoint url, which will be the url of your deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook

  4. Select the events you want to listen to. These should be the same events you're consuming in your webhook which are handled in src/payment/stripe/webhook.ts:

  5. After that, go to the webhook you just created and reveal the new signing secret.

  6. Add this secret to your deployed server's STRIPE_WEBHOOK_SECRET environment variable. If you've deployed to Fly.io, you can do that easily with the following command:

    sh
    wasp deploy fly cmd --context server secrets set STRIPE_WEBHOOK_SECRET=whsec_...

Lemon Squeezy

Creating Your Production Webhook

To set up your Lemon Squeezy webhook, you'll need the URL of you newly deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook.

With the webhook url ready, go to your Lemon Squeezy Webhooks Dashboard:

  1. Click the + button.
  2. Add the webhook forwarding url to the Callback URL section.
  3. Give your webhook a signing secret (a long, random string).
  4. Add this signing secret to your server's production environment variables under LEMONSQUEEZY_WEBHOOK_SECRET.
  5. Make sure to select at least the following updates to be sent:
    • order_created
    • subscription_created
    • subscription_updated
    • subscription_cancelled
  6. Click save.

Polar

Turning off the Sandbox mode

Make sure to turn off the sandbox mode in your server's production environemnt:

ini title=".env.server"
POLAR_SANDBOX_MODE=false

Creating Your Production Webhook

To set up your Polar webhook, you'll need the URL of your newly deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook.

With the webhook URL ready, in your Polar Dashboard:

  1. Navigate to Settings > Webhooks.
  2. Click the Add Endpoint button.
  3. In the URL field, paste your production forwarding address.
  4. Set the Format to "Raw".
  5. Select the following events to listen for:
    • order.paid
    • subscription.updated
  6. Click Save.
  7. Copy the generated webhook secret and set it as the POLAR_WEBHOOK_SECRET environment variable in production.

Paddle

Turning off the Sandbox mode

Make sure to turn off the sandbox mode in your production environment, for both the server and the client:

ini title=".env.server"
PADDLE_SANDBOX_MODE=false
ini title=".env.client"
REACT_APP_PADDLE_SANDBOX_MODE=false

In production, Paddle requires a default payment link before it can open a checkout. It must be an https URL on a domain you've had approved by Paddle.

In your Paddle Dashboard:

  1. Navigate to Checkout > Checkout configuration.
  2. Set the Default payment link to your deployed app's URL, e.g. https://opensaas.sh.
  3. Click Save.

Creating Your Production Webhook

To set up your Paddle webhook, you'll need the URL of your newly deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook.

With the webhook URL ready, in your Paddle Dashboard:

  1. Navigate to Events > Notifications.
  2. Click the + New destination button.
  3. Fill in the Description (e.g. "Open SaaS Production").
  4. Set the Notification Type to Webhook.
  5. In the URL field, paste your production webhook URL.
  6. Set the Usage type to Both.
  7. Select the following events to listen for:
    • transaction.completed
    • subscription.updated
    • subscription.canceled
  8. Click Save.
  9. Click on your newly created destination in the Events > Notifications list to open it.
  10. Click copy on the Secret key and set it as the PADDLE_WEBHOOK_SECRET environment variable in production.

Customizing SEO and Discoverability

Before deploying, make sure your app's meta tags, JSON-LD schema, and llms.txt reflect your production URL and content so search engines and AI/LLM tools can index and recommend your app correctly. See the SEO guide for details.

Deploying your App

Deploying to Fly.io

Fly.io is a platform for running your apps globally. It's a great choice for deploying your SaaS app because it's free to get started, can host your entire full-stack app in one place, scales well, and has one-command deploy integration with Wasp.

Wasp provides the handy wasp deploy command to deploy your entire full-stack app (DB, server, and client) in one command.

To learn how, please follow the detailed guide for deploying to Fly via the Wasp CLI from the Wasp documentation. We suggest you follow this guide carefully to get your app deployed.

:::caution[Setting Environment Variables]
Remember, because we've set certain client-side env variables, make sure to pass them to the wasp deploy commands so that they can be included in the build:

sh
REACT_APP_CLIENT_ENV_VAR_1=<...> REACT_APP_CLIENT_ENV_VAR_2=<...> wasp deploy

The wasp deploy command will also take care of setting the following server-side environment variables for you so you don't have to:

  • DATABASE_URL
  • PORT
  • JWT_SECRET
  • WASP_WEB_CLIENT_URL
  • WASP_SERVER_URL

For setting the remaining server-side environment variables, please refer to the Deploying with the Wasp CLI Guide.
:::

Deploying Manually

If you prefer to deploy manually, your frontend and backend separately, or just prefer using your favorite provider you can follow Wasp's Manual Deployment Guide.

:::caution[Client-side Environment Variables]
Remember to always set additional client-side environment variables, such as REACT_APP_STRIPE_CUSTOMER_PORTAL by appending them to the build command, e.g.

sh
REACT_APP_CLIENT_ENV_VAR_1=<...> npm run build

:::

:::note[Server Health Checks]
If your provider runs health checks against your server, point them at the /up endpoint (e.g. https://your-server-url.com/up). Wasp doesn't serve anything at / in production.
:::

Deploying your Blog

Deploying your Astro Starlight blog is a bit different than deploying your SaaS app. As an example, we will show you how to deploy your blog for free to Netlify. You will need a Netlify account and Netlify CLI installed to follow these instructions.

Make sure you are logged in with Netlify CLI:

  • You can check if you are logged in with netlify status.
  • You can log in with netlify login.

Position yourself in the blog directory and run the following command:

sh
npm run build

This will build your blog into the blog/dist directory. Now you can deploy your blog to Netlify with the following command:

sh
netlify deploy

Select the dist directory as the deploy path.

Finally, if the deployment looks good, you can deploy your blog to production with the following command:

sh
netlify deploy --prod

Opensaas Sh Blog Src Content Docs Guides Email Sending (`opensaas-sh/blog/src/content/docs/guides/email-sending.mdx`)


title: Email Sending
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import { TabItem, Tabs } from '@astrojs/starlight/components';

This guide explains how to use the integrated email sender and how you can integrate your own account in this template.

Sending Emails

The `Dummy` Email Provider (for Local Dev Only)

By default we've set up the email sender to use the Dummy provider. This is for local development only and no emails will actually be sent out!
To obtain an email verification token/link, you must check the server logs on initial sign up. You can click this link to verify your email and continue with the sign up process.

ts title="src/server/emailsender.wasp.ts"
import { type EmailSender } from "@wasp.sh/spec";

export const emailSender: EmailSender = {
  provider: "Dummy",
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]",
  },
};

Note that your app will not build if using the Dummy provider and you must switch to a production-ready provider in order to do so.

Using a Production-Ready Email Provider (e.g. SendGrid)

To change your email provider to a production-ready one, such as SendGrid, you'll want to configure your emailSender like so:

ts title="src/server/emailsender.wasp.ts"
import { type EmailSender } from "@wasp.sh/spec";

export const emailSender: EmailSender = {
  provider: "SendGrid",
  defaultFrom: {
    name: "Open SaaS App",
    // When using SendGrid, you must use the same email address that you configured your account to send out emails with!
    email: "[email protected]",
  },
};

This means that you can send emails from your app using the send function from the email modul provided by Wasp:

tsx title="src/server/webhooks.ts"
import { emailSender } from "wasp/server/email";

//...

  if (subscription.cancel_at_period_end) {
    await emailSender.send({
      to: customer.email,
      subject: 'We hate to see you go :(',
      text: 'We hate to see you go. Here is a sweet offer...',
      html: 'We hate to see you go. Here is a sweet offer...',
    });
  }

In the example above, you can see that we're sending an email to the customer when we receive a cancel subscription event within the Stripe webhook.

This is a powerful feature and super simple to use.

Integrate your email sender

To set up your email sender, you first need an account with one of the supported email providers.

- Register at SendGrid.com and then get your [API KEYS](https://app.sendgrid.com/settings/api_keys). - Copy yours to the `.env.server` file under the `SENDGRID_API_KEY` variable.
text
Make sure to change the `defaultFrom` email address in the `src/server/emailSender.wasp.ts` file to use the same email address that you configured your account to send out emails with!

```ts title="src/server/emailSender.wasp.ts" {5}
export const emailSender: EmailSender = {
  provider: "SendGrid",
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]", // Same email address you configured your SendGrid account to send emails with!
  },
};
```
- Go to [Mailgun](https://mailgun.com) and create an account. - Go to [API Keys](https://app.mailgun.com/settings/api_security/api_keys?onboardingTask=api-key) and create a new API key. - Copy the API key and add it to your .env.server file under the `MAILGUN_API_KEY` variable. - Go to [Domains](https://app.mailgun.com/mg/sending/new-domain?onboardingTask=add-verify-domain) and create a new domain. - Copy the domain and add it to your .env.server file as `MAILGUN_DOMAIN`.
text
Make sure to change the `defaultFrom` email address in the `src/server/emailSender.wasp.ts` file to use the same email address that you configured your account to send out emails with!

```ts title="src/server/emailSender.wasp.ts" {5}
export const emailSender: EmailSender = {
  provider: "Mailgun",
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]", // Same email address you configured your Mailgun account to send emails with!
  },
};
```

If you want more detailed info, or would like to use SMTP, check out the Wasp docs.


Opensaas Sh Blog Src Content Docs Guides File Uploading (`opensaas-sh/blog/src/content/docs/guides/file-uploading.mdx`)


title: File Uploading
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import cors from '@assets/file-uploads/cors.png';
import createBucket from '@assets/file-uploads/create-bucket.png';
import defaultSettings from '@assets/file-uploads/default-settings.png';
import findS3 from '@assets/file-uploads/find-s3.png';
import keys from '@assets/file-uploads/keys.png';
import newBucket from '@assets/file-uploads/new-bucket.png';
import permissions from '@assets/file-uploads/permissions.png';
import username from '@assets/file-uploads/username.png';
import { Image } from 'astro:assets';

This guide will show you how to set up file uploading in your SaaS app.

There are two options we recommend:

  1. Using AWS S3 with presigned URLS for secure file storage.
  2. Using Multer middleware to upload files to your own server.

We recommend using AWS S3 as it's a scalable, secure option, that can handle a large amount of storage.

If you're just looking to upload small files and don't expect your app to grow to a large scale, you can use Multer to upload files to your app's server.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Using AWS S3

How presigned URLs work

Presigned URLs are URLs that have been signed with your AWS credentials and can be used to upload files to your S3 bucket. They are time-limited and can be generated on the server and sent to the client to upload files directly to S3.

The process of generating a presigned URL is as follows:

  1. The client sends a request to the server to upload a file.
  2. The server generates a presigned URL using its AWS credentials.
  3. The server sends the presigned URL to the client.
  4. The client uses the presigned URL to upload the file directly to S3 before the URL expires.

We use this method to upload files to S3 because it is more secure than uploading files directly from the client to S3. It also allows us to keep our AWS credentials private and not expose them to the client.

To use presigned URLs, we'll need to set up an S3 bucket and get our AWS credentials.

Create an AWS Account

Before you begin, you'll need to create an AWS account. AWS accounts are free to create and are split up into:

  1. Root account.
  2. IAM users.

You'll need to first create a root account, and then an IAM user for your SaaS app before you can start uploading files to S3.

To do so, follow the steps in this external guide: Creating IAM users and S3 buckets in AWS.

Create an AWS S3 Bucket

Once you are logged in with your IAM user, you'll need to create an S3 bucket to store your files:

  1. Navigate to the S3 service in the AWS console.

  2. Click on the Create bucket button.

  3. Fill in the bucket name and region.

  4. Leave all the settings as default and click Create bucket.

Change the CORS settings

Now we need to change some permissions on the bucket to allow for file uploads from your app:

  1. Click on the bucket you just created.

  2. Click on the Permissions tab.

  3. Scroll down to the Cross-origin resource sharing (CORS) section and click Edit.

  4. Insert the correct CORS configuration and click Save changes. You can
    copy-paste most of the config below, but you must edit the
    AllowedOrigins field
    to fit your app. Include http://localhost:3000 for
    local development, and https://<your domain> for production.

    If you don't yet have a domain name, just list http://localhost:3000 for
    now. We'll remind you to add your domain before deploying to production in
    the Deployment docs.

    json {11,12}
    [
        {
            "AllowedHeaders": [
                "*"
            ],
            "AllowedMethods": [
                "POST",
                "GET"
            ],
            "AllowedOrigins": [
                "http://localhost:3000",
                "https://<your-domain>"
            ],
            "ExposeHeaders": []
        }
    ]

Get your AWS S3 credentials

Now that you have your S3 bucket set up, you'll need to get your S3 credentials to use in your app:

  1. Click on your username in the top right corner of the AWS console and select Security Credentials.

  2. Scroll down to the Access keys section.

  3. Click on Create Access Key.

  4. Select the Application running on an AWS service option and create the access key.

  5. Copy the Access key ID and Secret access key and paste them in your src/app/.env.server file:

    sh
    AWS_S3_IAM_ACCESS_KEY=ACK...
      AWS_S3_IAM_SECRET_KEY=t+33a...
      AWS_S3_FILES_BUCKET=your-bucket-name
      AWS_S3_REGION=your-region // (e.g. us-west-2)

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Using and Customizing File Uploads with S3 in your App

With your S3 bucket set up and your AWS credentials in place, you can now start uploading files in your app using presigned URLs by navigating to localhost:3000/file-upload and uploading a file.

To begin customizing file uploads, is important to know where everything lives in your app. Here's a quick overview:

  • schema.prisma:
    • The File model can be found here. Here you can modify the fields to suit your needs.
  • src/file-upload/file-upload.wasp.ts:
    • The file upload routes, queries, and actions are declared here in the Wasp Spec.
  • src/file-upload/FileUploadPage.tsx:
    • The FileUploadPage component is where the file upload form lives. It also allows you to download the file from S3 by calling the getDownloadFileSignedURL based on that files key in the app DB.
  • src/file-upload/operations.ts:
    • The createFileUploadUrl action lives here and calls the getUploadFileSignedURLFromS3 within it using your AWS credentials before passing it to the client. This function stores the files in the S3 bucket within folders named after the user's ID, so that each user's files are stored separately.
    • The getAllFilesByUser fetches all File information uploaded by the user. Note that the files do not exist in the app database, but rather the file data, its name and its key, which is used to fetch the file from S3.
    • The getDownloadFileSignedURL query fetches the presigned URL for a file to be downloaded from S3 using the file's key stored in the app's database.

Cleaning up "orphaned" files in S3

In the current logic, files are first deleted from the app's database before attempting to delete them from S3. If, for some reason, the S3 deletion were to fail, the file would remain in S3 and not in the app's database and be orphaned:

ts title="src/file-upload/operations.ts"
export const deleteFile: DeleteFile<DeleteFileInput, File> = async (
  rawArgs,
  context,
) => {
  // ...
  const deletedFile = await context.entities.File.delete({
    where: {
      id: args.id,
      user: {
        id: context.user.id,
      },
    },
  });

  try {
    await deleteFileFromS3({ s3Key: deletedFile.s3Key });
  } catch (error) {
    console.error(
      `S3 deletion failed. Orphaned file s3Key: ${deletedFile.s3Key}`,
      error,
    );
  }
};

To clean up these orphaned files, you can add a cleanup job (that runs at an interval of your choosing) to:

  1. Fetch all file keys from S3.
  2. Fetch all file keys from the app's database.
  3. Compare the two lists and delete any files from S3 that are not in the database.

Here's an example of how you could implement this:

ts title="src/file-upload/file-upload.wasp.ts"
import { job, type Spec } from "@wasp.sh/spec";
import { cleanUpOrphanedFilesS3Job } from "./workers" with { type: "ref" };

export const fileUploadSpec: Spec = [
  // ...
  job(cleanUpOrphanedFilesS3Job, {
    executor: "PgBoss",
    schedule: {
      cron: "0 5 * * 0", // Every week on Sunday at 5am.
    },
    entities: ["File"],
  }),
];
text
/* Detailed source-code truncated for AI context efficiency. */

Using Multer to upload files to your server

If you're looking to upload files to the app server, you can use the Multer middleware to handle file uploads. This will allow you to store files on your server and is a good option if you need a quick and dirty, free solution for simple file uploads.

To set up file uploads using Multer, follow the Wasp integration guide.


Opensaas Sh Blog Src Content Docs Guides Seo Performance (`opensaas-sh/blog/src/content/docs/guides/seo-performance.mdx`)


title: SEO & Performance
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import { Image } from 'astro:assets';

This guides explains how to improve the SEO and performance for of your app.

Landing Page Meta Tags

Wasp gives you the ability to add meta tags to your landing page HTML via the app's head property, defined in the src/client/head.wasp.ts file:

ts title="src/client/head.wasp.ts" {4-8}
import { type App } from "@wasp.sh/spec";

export const head: App["head"] = [
  "<meta property='og:type' content='website' />",
  "<meta property='og:url' content='https://opensaas.sh' />",
  "<meta property='og:title' content='Open SaaS' />",
  "<meta property='og:description' content='Free, open-source SaaS boilerplate starter for React & NodeJS.' />",
  "<meta property='og:image' content='https://opensaas.sh/public-banner.webp' />",
  // ...
];

Change the above highlighted meta tags to match your app. Wasp will inject these tags into the HTML of your index.html file, which is the Landing Page (app/src/client/landing-page/LandingPage.tsx), in this case.

Prerendering Routes

By default, a Wasp app is a Single Page Application (SPA). When someone visits your site, their browser first downloads a mostly empty page and then runs JavaScript to fill it in with the actual content. This works well for pages behind a login, but it has two downsides for pages like your landing page:

  • Visitors wait longer before they see anything.
  • Search engines and AI crawlers sometimes struggle to see your real content, which hurts your ranking.

Prerendering fixes both. You can turn it on for any route by adding prerender: true to its route definition:

ts title="main.wasp.ts"
route("LandingPageRoute", "/", page(LandingPage), { prerender: true }),

Now, when you run wasp build, Wasp builds a complete HTML version of that page ahead of time. Visitors (and Google) see your real content right away, and React still takes over in the background so buttons, links, and other interactions keep working as normal.

For a content-heavy landing page, this typically makes the page feel several times faster to load.

See the Wasp docs for the full reference and caveats: Prerendering.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Structured Data (JSON-LD)

In addition to meta tags, Open SaaS includes a SchemaMarkup component at src/landing-page/components/SchemaMarkup.tsx that embeds JSON-LD structured data into your landing page. This helps search engines and LLMs understand what your app is, who makes it, and what it offers, improving how it appears in search results, AI answers, and rich snippets.

tsx title="src/landing-page/components/schemamarkup.tsx"
const schema = {
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "SoftwareApplication",
      name: "Your Open SaaS App",
      description: "Your apps main description and features.",
      url: "https://your-saas-app.com",
      // ...
    },
  ],
};

Customize the schema object to match your product:

  • Update name, description, and url
  • Set applicationCategory / applicationSubCategory (see schema.org/SoftwareApplication)
  • Add an Organization entry to associate the app with a company
  • Add a FAQPage entry if your landing page has an FAQ section

The component is rendered inside LandingPage.tsx, and Wasp's prerendering inlines its output into the static HTML so crawlers see the structured data without needing to execute JavaScript.

AI / LLM Discoverability

Beyond traditional search engines, AI assistants and LLM-powered search tools (ChatGPT, Perplexity, Claude, etc.) are an increasingly important source of discovery. The llms.txt convention provides a curated, markdown index of your site that these tools can use to understand and recommend your app.

The template ships a placeholder llms.txt at template/app/public/llms.txt, which gets served at https://your-saas-app.com/llms.txt:

md title="public/llms.txt"
# Your Open SaaS App

> One-line description of what your SaaS does and who it's for.

A longer paragraph giving context: the problem your app solves, the kind of
user it serves, and the key value it delivers.

## Core pages
- [Home](https://your-saas-app.com): Landing page with features, pricing, and FAQ
- [Pricing](https://your-saas-app.com/pricing): Subscription plans and pricing
- [Sign up](https://your-saas-app.com/signup): Create a new account

Customize it before deploying:

  • Replace your-saas-app.com with your production URL
  • Update the name, description, and longer paragraph to match your product
  • Curate the list of links — include only your most important public pages
  • Add or remove sections as needed (e.g. ## API, ## Integrations)

Keep the file short and intentional. It should be a concise index of your site that AI crawlers and LLMs can digest easily.

Docs & Blog Meta Tags

Astro, being a static-site generator, will automatically inject relevant information provided in the blog/astro.config.mjs file, as well as in the frontmatter of .md files into the pages HTML:

yaml
---
title: 'My First Blog Post'
pubDate: 2022-07-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
    url: 'https://docs.astro.build/assets/full-logo-light.png'
    alt: 'The full Astro logo.'
tags: ["astro", "blogging", "learning in public"]
---

Improving your SEO is as simple as adding these properties to your docs and blog content!


Opensaas Sh Blog Src Content Docs Guides Tests (`opensaas-sh/blog/src/content/docs/guides/tests.md`)


title: Tests
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


This guide will show you how to use the included end-to-end (e2e) tests for your Open SaaS application.

The Tests Directory

In the root of your project, you'll find an e2e-tests directory which contains the Playwright tests for your Open SaaS application:

text
.
├── e2e-tests/
│   ├── tests/                  # Directory containing the test files
│   ├── README.md               # Instructions on how to run the tests
│   ├── playwright.config.ts    # Playwright configuration
│   ├── package.json
│   ├── ...

To run the tests locally, or in a CI pipeline, follow the instructions in the README.md file in the e2e-tests directory.

Using Tests in CI with GitHub Actions

Although the Open SaaS template does not come with an example GitHub workflow, you can find out how Open SaaS itself set it up in .github/workflows/e2e-tests.yml at Open SaaS repo.

You can copy and paste the .github/ directory containing the e2e-tests.yml workflow into the root of your own repository to run the tests as part of your CI pipeline.

:::caution[WASP_VERSION]
Please make sure to update the WASP_VERSION environment variable in the e2e-tests.yml file to match the version of Wasp you are using in your project.
:::

In order for these tests to run correctly on GitHub, you need to provide the environment variables mentioned in the e2e-tests.yml file within your GitHub repository's "Actions" secrets so that they can be accessed by the tests.


Opensaas Sh Blog Src Content Docs Guides Updating Opensaas (`opensaas-sh/blog/src/content/docs/guides/updating-opensaas.md`)


title: How (Not) to Update Your Open SaaS App
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


:::danger[We advise against merging the latest template changes into your app]
If you've already started building your app, we generally advise against merging the latest template changes into your app.

Below we outline our reasoning why, and provide a basic guide to help you update your app if you decide to do so anyway.
:::

Why you probably shouldn't include the latest template changes in your app

We generally advise against updating your Open SaaS-based applications after initial setup.

Why?

Because your codebase will naturally diverge from the template as you build your unique application, and any updates we may make to the template may not be compatible with your modified codebase, or your version of Wasp.

Even if you really want to include a new feature from the template in your app, proceed with caution and thoroughly consider the following:

  • Changes to the template may be tightly coupled. Implementing one change without related ones could cause unexpected issues.
  • Updates might not be compatible with your version of Wasp.
  • The more your codebase has diverged, the more challenging the update will be.

If you still decide to update your app

If you read above, considered the risks, and still need specific improvements, we recommend that you manually merge the changes.

To do this, you should can either 1) merge new Open SaaS template changes into your current project, or 2) merge project changes into a fresh Open SaaS template.

  1. Merge new Open SaaS template changes into your current project by:

    • reviewing the latest commits,
    • understanding what happened,
    • being mindful of the Wasp version you're using,
    • and then fitting those changes into your own codebase.
  2. Merge your project changes into a fresh Open SaaS template by:

    • starting a new, fresh project with the latest Open SaaS template,
    • and then copying over the logic from your existing project that you want to keep.

The method you choose is up to you and will largely depend on the complexity of the changes you need to make.


Opensaas Sh Blog Src Content Docs Guides Vibe Coding (`opensaas-sh/blog/src/content/docs/guides/vibe-coding.mdx`)


title: "AI-assisted Coding / Vibe Coding with Open SaaS"
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import { Image } from 'astro:assets';
import vibeBoi from '@assets/ai/vibe-boi.png';

If you're looking to use AI coding tools to help build your SaaS app, this guide is for you.

:::tip[New to building SaaS? Learn while you build with Open Vibe]
Open Vibe is our free, open-source curriculum that turns Claude Code (or your agent of choice) into a SaaS-building tutor. It pairs interactive diagrams with hands-on lessons — and Phase 2 builds a real SaaS on top of this very Open SaaS template.
:::

Coding with AI, Open SaaS, & Wasp

Wasp is the perfect match for AI-assisted coding due to its central config file which gives LLMs immediate context about the entire full-stack app, as well as its ability to manage boilerplate code so AI doesn't have to.

To make it even easier, here are some resources to help you use Wasp & Open SaaS with AI as effectively as possible.

Agent Skills and Rules

We've put some agent skills and rules into the template to help you get started using Open SaaS with your favorite AI-assisted coding tools (Claude Code, Cursor, VS Code/Copilot, Codex, Gemini CLI, OpenCode, etc.).

Skills are found in the app/.agents (and app/.claude) directory of the Open SaaS template, and follow the Agent Skills standard.
See your AI-assisted coding tool's documentation for more details on how to add Agent Skills if they don't load automatically.

Rules, found within the AGENTS.md (and CLAUDE.md), is another open standard used by many popular AI-assisted coding tools to give instructions to the agents when coding.

:::tip[These are just a starting point!]
Use these skills and rules to get your project up and running.

Run the add-wasp-skills skill to add Wasp framework-specific skills to your project for even more power.
:::

LLM-Friendly Documentation

Note: the AGENTS.md has specific instructions on how to automatically fetch and use the LLM-friendly documentation but you can also paste the following links in chat sessions if needed.

More AI-assisted Coding Learning Resources

Here's a list of articles and tutorials we've made:


Opensaas Sh Blog Src Content Docs Start Guided Tour (`opensaas-sh/blog/src/content/docs/start/guided-tour.md`)


title: Guided Tour
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


Awesome, you now have your very own SaaS app up and running! But, first, here are some important things you need to know about your app in its current state:

  1. When signing up with a new user, you will get a message to check your email for a verification link. But, in development, these emails are simply written to your terminal. So, to continue with the registration process, check your server logs after sign up!
sh title="server logs"
[ Server ] ╔═══════════════════════╗
[ Server ] ║ Dummy email sender ✉️  ║
[ Server ] ╚═══════════════════════╝
[ Server ] From:    Open SaaS App <[email protected]>
[ Server ] To:      [email protected]
[ Server ] Subject: Verify your email
[ Server ] ═════════ Text ═════════
[ Server ] Click the link below to verify your email: http://localhost:3000/email-verification?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InZpbm55QHdhc3Auc2giLCJleHAiOjE3MTg5NjUyNTB9.PkRGrmuDPuYFXkTprf7QpAye0e_O9a70xbER6LfxGJw
[ Server ] ═════════ HTML ═════════
[ Server ] <p>Click the link below to verify your email</p>
[ Server ] <a href="http://localhost:3000/email-verification?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InZpbm55QHdhc3Auc2giLCJleHAiOjE3MTg5NjUyNTB9.PkRGrmuDPuYFXkTprf7QpAye0e_O9a70xbER6LfxGJw">Verify email</a> 
[ Server ] ════════════════════════
  1. Your app is still missing some key configurations (e.g. API keys for Payment Processors, OpenAI, AWS S3, Auth, Analytics). These services won't work at the moment, but don't fear, because we've provided detailed guides in these docs to help you set up all the services in this template.
  2. If you want to get a feel for what your SaaS could look like when finished, check out OpenSaaS.sh in your browser. It was built using this template! So make sure to log in, play around with the demo app, make a test payment, and check out the admin dashboard.

In the sections below, we will take a short guide through the codebase and the app's main features. Then at the end of this tour, we also prepared a checklist of likely changes you will want to make to the app to make it your own.

We're looking forward to seeing what you build!

Getting acquainted with the codebase

Now that you've gotten a first look at the app, let's dive into the codebase.

At the root of our project, you will see three folders:

sh
.
├── app
├── blog
└── e2e-tests

app contains the Wasp project files, which is your full-stack (React + NodeJS + Prisma) app along with a Wasp Spec file, main.wasp.ts, which will be explained in more detail below.

blog contains the Astro Starlight theme for the blog and documentation section.

e2e-tests contains the end-to-end tests using Playwright, which you can run to test your app's functionality.

App File Structure

We've structured this full-stack app template vertically (by feature). That means that most directories within app/src contain both the React client code and NodeJS server code necessary for implementing its logic.

Let's check out what's in the app folder in more detail:

sh
.
├── main.wasp.ts           # Wasp Spec file. You define your app structure here.
├── schema.prisma          # Prisma schema file. You define your database models here.
├── .wasp/                 # Output dir for Wasp. DON'T MODIFY THESE FILES!
├── public/                # Public assets dir, e.g. `www.yourdomain.com/public-banner.webp`.
├── src/                   # Your code goes here.
│   ├── admin/             # Admin dashboard related pages and components.
│   ├── analytics/         # Logic and background jobs for processing analytics.
│   ├── auth/              # All auth-related pages/components and logic.
│   ├── client/            # Shared components, hooks, landing page, and other client code (React).
│   ├── demo-ai-app/       # Logic for the example AI-powered demo app.
│   ├── file-upload/       # Logic for uploading files to S3.
│   ├── landing-page/      # Landing page related code
│   ├── payment/           # Logic for handling payments and webhooks.
│   ├── server/            # Scripts, shared server utils, and other server-specific code (NodeJS).
│   ├── shared/            # Shared constants and util functions.
│   └── user/              # Logic related to users and their accounts.
├── .env.server            # Dev environment variables for your server code.
├── .env.client            # Dev environment variables for your client code.
├── .prettierrc            # Prettier configuration.
├── package.json
├── package-lock.json
└── .wasproot

The Wasp Config file

This template at its core is a Wasp project, where Wasp is a full-stack web app framework that let's you write your app in React, NodeJS, and Prisma and will manage the "boilerplatey" work for you, allowing you to just take care of the fun stuff!

Wasp's secret sauce is its use of a config file, the Wasp Spec (main.wasp.ts), and compiler which takes your code and outputs the client app, server app and deployment code for you.

Because the Wasp Spec is just TypeScript, we've split the config vertically as well: each feature folder has its own *.wasp.ts file (e.g. src/auth/auth.wasp.ts, src/payment/payment.wasp.ts) which exports a Spec, and main.wasp.ts combines them all in the app definition:

ts title="main.wasp.ts"
import { app, page, route } from "@wasp.sh/spec";

import { LandingPage } from "./src/landing-page/LandingPage" with { type: "ref" };

import { authConfig, authSpec } from "./src/auth/auth.wasp";
import { paymentSpec } from "./src/payment/payment.wasp";
// ...

export default app({
  name: "OpenSaaS",
  wasp: { version: "^0.26.0" },
  title: "My Open SaaS App",
  auth: authConfig,
  // ...
  spec: [
    route("LandingPageRoute", "/", page(LandingPage), { prerender: true }),
    authSpec,
    paymentSpec,
    // ...
  ],
});

In this template, we've already defined a number of things in the Wasp Spec, including:

By defining these things in the config file, Wasp continuously handles the boilerplate necessary with putting all these features together. You just need to focus on the business logic of your app.

Wasp abstracts away some things that you would normally be used to doing during development, so don't be surprised if you don't see some of the things you're used to seeing.

:::note
It's possible to learn Wasp's feature set simply through using this template, but if you find yourself unsure how to implement a Wasp-specific feature and/or just want to learn more, a great starting point is the intro tutorial in the Wasp docs which takes ~20 minutes.
:::

Client

The src/client folder contains any additional client-side code that doesn't belong to a feature:

sh
.
└── client
    ├── components         # Your shared React components.
    ├── hooks              # Your shared React hooks.
    ├── static             # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'.
    ├── App.tsx            # Main app component to wrap all child components. Useful for global state, navbars, etc.
    ├── Main.css           # Main CSS file. Includes the Tailwind CSS into the project.
    └── utils.ts

Server

The src/server folder contains any additional server-side code that does not belong to a specific feature:

sh
└── server
    ├── scripts            # Scripts to run via Wasp, e.g. database seeding.
    └── validation.ts

Main Features

Auth

This template comes with a fully functional auth flow out of the box. It takes advantages of Wasp's built-in Auth features, which do the dirty work of rolling your own full-stack auth for you!

ts title="src/auth/auth.wasp.ts"
export const authConfig: Auth = {
  userEntity: "User",
  methods: {
    email: emailAuthMethod,
    google: googleAuthMethod,
    gitHub: gitGubAuthMethod,
    discord: discordAuthMethod,
  },
  onAuthFailedRedirectTo: "/login",
  onAuthSucceededRedirectTo: "/demo-app",
};

By defining the auth structure in the Wasp Spec, Wasp manages all the necessary code for you, including:

  • Email verified login with password reset.
  • Social login with Google and/or GitHub.
  • Auth-related database entities for user credentials, sessions, and social logins.
  • Custom-generated AuthUI components for login, signup, and password reset.
  • Auth hooks for fetching user data.

We've set the template up with Wasp's email, google, discord and gitHub methods, which are all battle-tested and suitable for production.

You can get started developing your app with the email method right away!

:::caution[Dummy Email Provider]
Note that the email method relies on an emailSender (configured in the src/server/emailSender.wasp.ts file), a service which sends emails to verify users and reset passwords.

For development purposes, Wasp provides a Dummy email sender which Open SaaS comes with as the default. This provider does not actually send any confirmation emails to the specified email address, but instead logs all email verification links/tokens to the console! You can then follow these links to verify the user and continue with the sign-up process.

ts title="src/server/emailsender.wasp.ts"
export const emailSender: EmailSender = {
  provider: "Dummy", // Logs all email verification links/tokens to the server's console.
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]",
  },
};

:::

We will explain more about these auth methods, and how to properly integrate them into your app, in the Authentication Guide.

Subscription Payments with Stripe, Lemon Squeezy or Polar

No SaaS is complete without payments, specifically subscription payments. That's why this template comes with a fully functional Stripe, Lemon Squeezy and Polar integration.

Let's take a quick look at how payments are handled in this template.

  1. A user clicks the BUY button and a checkout session is created on the server.
  2. The user is redirected to the payment processor's checkout page where they enter their payment info.
  3. The checkout session is completed and the user is redirected back to the app.
  4. Payment processor sends a webhook event to the app's server with the payment info.
  5. The app server's webhook handler handles the event and updates the user's subscription status.

The payment processor you choose (Stripe, Lemon Squeezy, Polar or Paddle) and its related functions can be found at src/payment/paymentProcessor.ts. The PaymentProcessor object holds the logic for creating checkout sessions, webhooks, etc. Its client-side counterpart, PaymentProcessorClient in src/payment/paymentProcessorClient.ts, opens the checkout session in the browser.

The logic for creating the Checkout session is defined in the src/payment/operation.ts file. Actions are a type of Wasp Operation, specifically your server-side functions that are used to write or update data to the database. Once they're defined in the Wasp Spec, you can easily call them on the client-side:

a) define the action in the src/payment/payment.wasp.ts file

ts title="src/payment/payment.wasp.ts"
import { action, type Spec } from "@wasp.sh/spec";
import { generateCheckoutSession } from "./operations" with { type: "ref" };

export const paymentSpec: Spec = [
  // ...
  action(generateCheckoutSession, { entities: ["User"] }),
];

b) implement the action in the src/payment/operations file

ts title="src/payment/operations.ts"
export const generateCheckoutSession: GenerateCheckoutSession<
  GenerateCheckoutSessionInput,
  CheckoutSession
> = async (rawPaymentPlanId, context) => {
  // ...
};

c) call the action on the client-side

js title="src/client/app/subscriptionpage.tsx"
import { generateCheckoutSession } from "wasp/client/operations";

async function handleBuyNowClick(paymentPlanId: PaymentPlanId) {
  // ...
  const checkoutSession = await generateCheckoutSession(paymentPlanId);
  // ...
};

The webhook handler is defined in the src/payment/webhook.ts file. Unlike Actions and Queries in Wasp which are only to be used internally, we define the webhook handler in the Wasp Spec as an API endpoint in order to expose it externally to the payment processor.

ts title="src/payment/payment.wasp.ts"
import { api, type Spec } from "@wasp.sh/spec";
import { paymentsMiddlewareConfigFn, paymentsWebhook } from "./webhook" with { type: "ref" };

export const paymentSpec: Spec = [
  // ...
  api("POST", "/payments-webhook", paymentsWebhook, {
    entities: ["User", "ProcessedWebhookEvent"],
    middlewareConfigFn: paymentsMiddlewareConfigFn,
  }),
];

Within the webhook handler, we look for specific events that the payment processor sends us to let us know which payment was completed and for which user. Then we update the user's subscription status in the database.

Payment processors can deliver the same event more than once, for example when they retry after a timeout. The webhook handler records the id of every processed event in the ProcessedWebhookEvent entity and skips the events it has already seen, so a retried event can't grant purchased credits twice. You can find this logic in src/payment/processedWebhookEvent.ts.

To learn more about configuring the app to handle your products and payments, check out the Payment Integrations guide.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Analytics and Admin Dashboard

Keeping an eye on your metrics is crucial for any SaaS. That's why we've built an administrator's dashboard where you can view your app's stats, user data, and revenue all in one place.

To do that, we've leveraged Wasp's Jobs feature to run a cron job that calculates your daily stats. The app stats, such as page views and sources, can be pulled from either Plausible or Google Analytics. All you have to do is create a project with the analytics provider of your choice and import the respective pre-built helper functions!

ts title="src/analytics/analytics.wasp.ts"
import { job, type Spec } from "@wasp.sh/spec";
import { calculateDailyStatsJob } from "./stats" with { type: "ref" };

export const analyticsSpec: Spec = [
  // ...
  job(calculateDailyStatsJob, {
    executor: "PgBoss",
    schedule: {
      cron: "0 * * * *", // Every hour. useful in production.
      // cron: "* * * * *" // Every minute. useful for debugging.
    },
    entities: ["User", "DailyStats", "Logs", "PageViewSource"],
  }),
];

For more info on integrating Plausible or Google Analytics, check out the Analytics guide.

SEO & Discoverability

Open SaaS ships SEO-friendly out of the box. Three pieces work together so your app shows up in search results and AI answers:

  1. Meta tags in the app's head config (src/client/head.wasp.ts) — title, description, Open Graph, Twitter cards — get injected into your landing page HTML.
  2. JSON-LD structured data (Schema Markup) in src/landing-page/components/SchemaMarkup.tsx tells search engines and LLMs what your app is, who makes it, and what it offers — feeding rich snippets and AI-generated answers.
  3. Prerendering turns your React marketing pages (e.g. landing page, pricing, FAQ, etc.) into static HTML at build time, so crawlers see your content (including the schema markup) without needing to execute JavaScript.

Customize the meta tags in src/client/head.wasp.ts and the schema object in SchemaMarkup.tsx to match your product. See the SEO guide for details.

App Customization Walkthrough

General Considerations

When you first start your Open SaaS app straight from the template, it will run, but many of the services won't work because they lack your own API keys. Here are list of services that need your API keys to work properly:

  • Auth Methods (Google, Discord, GitHub).
  • Payment processor (Stripe, Lemon Squeezy or Polar).
  • OpenAI (ChatGPT API).
  • Email Sending (Sendgrid) -- you must set this up if you're using the email auth method.
  • Analytics (Plausible or Google Analytics).
  • File Uploading (AWS S3).

Now would be a good time to decide which features you do and do not need for your app, and remove the ones from the codebase that you don't need.

For the features you will use, the next section of the documentation, Guides, will walk you through how to set each one up!

:::note[Open SaaS is built on Wasp]
Remember, this template is built on the Wasp framework. If, at any time, these docs fail to provide enough information about a certain built-in feature, make sure to check out the Wasp docs!
:::

But before you start setting up the main features, let's walk through the customizations you will likely want to make to the template to make it your own.

Customizations Checklist

Wasp Spec (`main.wasp.ts`)

  • Change the app name and title:

    ts title="main.wasp.ts" {2, 4}
    export default app({
      name: "YourAppName",
      wasp: { version: "^0.26.0" },
      title: "Your App Name",
      // ...
    });

    :::caution[Restart Your App]
    Development databases are connceted to your app's name. Upon changing the app name, new, empty development database will be assigned to your app. This means you'll need to rerun wasp db start, wasp db migrate-dev and wasp start.
    :::

  • Update meta tags in the head config (src/client/head.wasp.ts) (even if you don't have a custom domain yet, put one you would like to have, as this won't affect development).

  • Update defaultFrom.name in the emailSender config (src/server/emailSender.wasp.ts) with the name of your app/company/whatever you want your users to see in their inbox, if you're using the emailSender feature and/or email Auth method.

  • Remove any features you might not use or need:

    • Auth methods - methods in src/auth/auth.wasp.ts:
      • If you're not using email Auth method, remove the routes/pages: RequestPasswordReset, PasswordReset, and EmailVerification.
    • Email Sending - src/server/emailSender.wasp.ts.
    • Plausible analytics - head config in src/client/head.wasp.ts.
    • File Uploading - the File model in schema.prisma, the src/file-upload/file-upload.wasp.ts spec, and all operations tied to file uploading.
  • Rename Entites and their properties, Routes/Pages, & Operations, if you wish.

Customizing the Look / Style of the App

  • Update your favicon at public/favicon.ico.
  • Update the banner image used when posting links to your site at public/public-banner.webp.
    • Update the URL for this banner at og:image and twitter:image in the head config of the src/client/head.wasp.ts file.
  • Update the JSON-LD structured data in src/landing-page/components/SchemaMarkup.tsx with your app's name, description, URL, and other metadata. This helps search engines and LLMs understand your app for rich results and AI answers.
  • Make changes to your landing page, LandingPage.tsx.
    • Customize the navBar, features, testimonials, and faqs in the contentSections.ts file.
    • Change/rename the logo.webp and main hero banner (open-saas-banner.webp) in the static folder.
  • If you want to make changes to the global styles of the app, you can do so in Main.css. Be aware that changing the current global styles affects most of the app!

Customizing the Analytics & Admin Dashboard

  • If you're using Plausible, update the head config in src/client/head.wasp.ts with your Plausible domain.
  • Update the calculateDailyStatsJob function in src/analytics/stats.ts to pull the stats from the analytics provider you've chosen (Plausible or Google Analytics).
  • Change the cron schedule of the calculateDailyStatsJob in the src/analytics/analytics.wasp.ts file to match how often you want your stats to be calculated.
  • Update the AdminDashboard components to display the stats you do/don't want to see.

`.env.server` and `.env.client` Files

  • After you've followed the Guides in the next section, you'll need to update the .env.server and .env.client files with your API keys and other environment variables for the services you've decided to use.
  • Delete any redundant environment variables that you're not using, from the .env.* files as well as the .env.*.example files.

Other Customizations

  • Make a new GitHub repository for your app.
  • Deploy your app to a hosting provider.
  • Buy a domain name for your app and get it set up with your hosting provider.
  • Read the e2e-tests README and get your end-to-end tests set up.
    • Change the tests to suit the changes you've made to your app.
  • Get the CI pipeline set up for your app (you can get started by using the Open SaaS development CI example here).

What's next?

In the following Guides sections, we'll walk you through getting those API keys and setting up the finer points of features such as Payments & Webhooks, Auth, Email Sending, Analytics, and more.


Template Blog Src Content Docs Blog 2023 11 21 Coverlettergpt (`template/blog/src/content/docs/blog/2023-11-21-coverlettergpt.md`)


title: How I Built & Grew CoverLetterGPT to 5,000 Users and $200 MRR
date: 2023-11-21
tags: ["indiehacker", "saas", "sideproject"]
subtitle: A guide to building a profitable, open-source side-project
hideBannerImage: false # Banner images stored in public/banner-images/ are automatically used as cover images and social media preview images (og:image) for each blog post.
authors:


Hey, I’m Vince…

I’m a self-taught developer that changed careers during the Covid Pandemic. I did it because I wanted a better career, enjoyed programming, and at the same time, had a keen interest in IndieHacking.

If you’re not aware, IndieHacking is the movement of developers who build potentially profitable side-projects in their spare time. And there are some very successful examples of IndieHackers and “solopreneurs” out there inspiring others, such as levels.io and Marc Lou.

This thought of being able to build my own side-project that could generate profit while I slept was always attractive to me.

So I’m happy to report that I’ve finally done it with my first software-as-a-service (SaaS) app, CoverLetterGPT.xyz, which I launched in March 2023!

I’ll be the first to admit that the results aren’t spectacular, but they’re still something I’m very proud of:

  • over 5,000 registered users
  • $203 monthly recurring revenue (MRR)

Below, I’m going to share with you how I built it (yes, it’s open-source!), how I marketed and monetized it, along with a bunch of helpful resources to help you build your own profitable side-project.

What the heck is CoverLetterGPT?

CoverLetterGPT.xyz was an idea I got after the OpenAI API was released. It’s an app that allows you to upload a PDF of your CV/resumé, along with the job description you’re applying to, and it will generate and edit unique cover letters for you based on this information.

{% embed https://youtu.be/ZhcFRD9cVrI %}

It also lets you save and manage your cover letters per each job, making it easy to make and apply to multiple jobs without having to keep copy and pasting all your important info into ChatGPT!

What’s the Tech Stack?

CoverLetterGPT is entirely open-source, so you can check out the code, fork it, learn from it, make your own, submit a PR (I’d love you forever if you did 🙂)… whatever!

I built it using the Wasp full-stack framework which allowed me to ship it about 10x faster.

Why?

Because Wasp as a framework allows you to describe your app’s core features in a main.wasp config file. Then it continually compiles and “glues” these features into a React-ExpressJS-Prisma full-stack app for you.

All you have to focus on is writing the client and server-side logic, and Wasp will do the boring stuff for you, like authentication & authorization, server config, email sending, and cron jobs.

BTW, Wasp is open-source and free and you can help the project out a ton by starring the repo on GitHub: https://www.github.com/wasp-lang/wasp 🙏

{% cta https://www.github.com/wasp-lang/wasp %} ⭐️ Thanks For Your Support 🙏 {% endcta %}

For the UI, I used Chakra UI, as I always do. I like that it’s a component-based UI library. This helps me build UI’s a lot faster than I would with Tailwind or vanilla CSS.

For payments, I used Stripe, (I’ll go into the details of monetization below).

The Server and Postgres Database are hosted on https://railway.app, with the client on Netlify.com’s free tier.

By the way, If you’re interested in building your own SaaS with almost the same stack as above, I also built a free SaaS template you can use that will save you days of work!

How I Marketed It

My biggest take-away from this whole project was that open-sourcing it was the best way to market it!

This seems counter-intuitive, right? Why would making the code available for anyone to see and copy be good for a business? You’re basically rolling out a red carpet for competitors, aren’t you?

Well, not quite.

First of all, the number of people who will realistically spend the time and energy launching a direct competitor is low. Also, most people interested in your open-source code want to learn some aspect of it and apply it to their own ideas, not just copy yours directly.

Secondly, and most importantly, the fact that it’s open-source makes people a lot more receptive to you talking about it.

When you present something you’ve built and give people the opportunity to learn from it, they’re much more welcoming! As a result, they’re more likely to upvote it, share it, use it, and recommend it to others.

This is exactly what happened with CoverLetterGPT! As a result of me sharing the open-source code, it get featured on the IndieHackers.com newsletter (>100k subscribers), shared on blogs, and talked about on social media platforms.

And even though it’s a small, simple product, I tried launching it on Product Hunt, where it also performed considerably well.

So, all together, these initial efforts combined gave my product a good initial marketing presence. To this day, I haven’t really done much else to market it, except some twitter posts (and this post, if you want to consider it marketing 🤑).

How I Monetized It

When I first launched in March 2023, I didn’t really expect anyone to pay for the product, but I wanted to learn how to use Stripe as a payments processor, thinking that the skills might be useful in the future.

So I started simple, and just put a one-time payment link for tips. No paywall, no subscriptions. It was entirely free to use with any tip amount welcome.

To my surprise, tips started coming in, with some as high as $10 dollars!

This encouraged me to force users to login to use the product, and add a paywall after users used up 3 credits.

My initial payment options were:

  • $4.95 for a 3 months access
  • $2.95 for 10 cover letter generations

That went reasonably well until I implemented the ability for users to use GPT to make finer edits to their generated cover letters. That’s when I changed my pricing and that’s when better profits started to come in:

  • $5.95 / month subscription with GPT-4
  • $2.95 / month subscription with GPT-3.5-turbo

Currently, over 90% of my customers choose the more powerful, more expensive plan with GPT-4, even though the 3 trial credits use the GPT-3.5-turbo model.

(I also integrated Bitcoin Lightning payments — check out the repo if you want to learn how — but haven’t received any yet.)

Now, with an MRR of ~$203, my monthly profit of course depends on my costs, which are:

  • Domain Name: $10/year
  • OpenAI bill: ~ $15/month
  • Hosting bill: ~ $3/month

Which leaves me at about ~ $183/month in profits 😀

Future Plans

One of the most surprising aspects about CoverLetterGPT.xyz’s success is that, on the surface, the product is very simple. Also, I’ve done very little in the way of SEO marketing, and haven’t continued to market it much at all. The current growth is mostly organic at this point thanks to my initial marketing efforts.

But I still have some plans to make it better:

  • buy a better top-level domain (TLD), like CoverLetterGPT.ai
  • add more features, like the ability to generate interview questions based on the cover letters
  • improve the UX and make it look more “professional”

If you have any other ideas how I could improve it, drop me a comment, message me on twitter/x, or submit a PR to the repo.

Final Words + More Resources

My intention with this article was to help others who might be considering launching their own SaaS product. So I hope that’s been the case here. If you still have any questions, don’t hesitate to ask.

Here are also the most important links from this article along with some further resources that will help in building and marketing your own profitable side-project:

Oh, and if you found these resources useful, don't forget to support Wasp by starring the repo on GitHub!

{% cta https://www.github.com/wasp-lang/wasp %} ⭐️ Thanks For Your Support 🙏 {% endcta %}


Template Blog Src Content Docs Blog 2023 11 23 Post (`template/blog/src/content/docs/blog/2023-11-23-post.md`)


title: My first blog post date: 2023-11-20 tags: ["blog", "post", "saas", "rocknroll"] authors: ["Dev"]

Hello

Hello world!


Template Blog Src Content Docs Guides Example (`template/blog/src/content/docs/guides/example.md`)


title: Example Guide

This is an example guide. It's a good idea to have a guide for each major feature of your project. Guides should be short and to the point. They should also be written in a way that is easy to understand for beginners.

Example Heading

...


Opensaas Sh Blog Src Content Docs 0.23 General Admin Dashboard (`opensaas-sh/blog/src/content/docs/0.23/general/admin-dashboard.mdx`)


title: Admin Dashboard
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/general/admin-dashboard

import adminDashboard from '@assets/admin/admin-dashboard.png';
import dbStudio from '@assets/stripe/db-studio.png';
import { Image } from 'astro:assets';

This is a reference on how the Admin dashboard, available at /admin, is set up.

Permissions

The Admin dashboard is only accessible to users with the isAdmin field set to true.

prisma title="schema.prisma" {7}
model User {
  id                        String          @id @default(uuid())
  createdAt                 DateTime        @default(now())

  email                     String?         @unique
  username                  String?         @unique
  isAdmin                   Boolean         @default(false)
  //...

To give yourself administrator priveledges, make sure you add your email addresses to the ADMIN_EMAILS environment variable in .env.server file before registering/logging in with that email address:

sh title=".env.server"
[email protected]

// or add many admins with a comma-separated list

[email protected],[email protected],[email protected]

Or if you've already logged in with an email address that you want to give admin priveledges to, you can run the following command in a separate terminal window to update the user's isAdmin field manually:

sh
wasp db studio

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Admin Dashboard Pages

Analytics Dashboard

The Admin analytics dashboard is a single place for you to view your most important metrics and perform some admin tasks. At the moment, it pulls data from:

  • Payment Processor:
    • Total revenue.
    • Revenue for each day of the past week.
  • Google or Plausible:
    • Total number of page views (non-unique).
    • Percentage change in page views from the previous day.
    • Top sources/referrers with unique visitor count (i.e. how many people came from that source to your app).
  • Database:
    • Total number of registered users.
    • Daily change in number of registered users .
    • Total number of paying users.
    • Daily change in number of paying users.

These metrics are aggregated within the background job dailyStatsJob, which by default is run every hour. You can change the frequency of this job by modifying its cron field:

ts title="main.wasp" {8,7}
job dailyStatsJob {
  executor: PgBoss,
  perform: {
    fn: import { calculateDailyStats } from "@src/analytics/stats"
  },
  schedule: {
    cron: "0 * * * *" // Every hour. useful in production.
    // cron: "* * * * *" // Every minute. useful for debugging.
  },
  entities: [User, DailyStats, Logs, PageViewSource]
}

For more info on Wasp's recurring background jobs, check out the Wasp Jobs docs.

For a guide on how to integrate these services so that you can view your analytics via the dashboard, check out the Payment Integrations and Analytics guide of the docs.

:::note[Help us improve]
We're always looking to improve the Admin dashboard. If you feel something is missing or could be improved, consider opening an issue or submitting a pull request
:::

Users

The Users page is where you can view all your users and their most important details. You can also search and filter users by:

  • Email address.
  • Subscription/payment status.
  • Admin status.

Opensaas Sh Blog Src Content Docs 0.23 General Privacy Policy (`opensaas-sh/blog/src/content/docs/0.23/general/privacy-policy.mdx`)


title: Privacy Policy sidebar: hidden: true slug: 0.23/general/privacy-policy

OpenSaaS.sh

Last updated: 2026-01-13

OpenSaaS.sh ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website (https://opensaas.sh).

Please read this privacy policy carefully. If you do not agree with the terms of this privacy policy, please do not access the site.


What Data Do We Collect?

OpenSaaS.sh collects the following types of personal data:

  • IP address
  • browser type
  • pages visited
  • time spent on pages

How Do We Collect Your Data?

You directly provide OpenSaaS.sh with most of the data we collect. We collect data and process data when you:

  • Use or view our website via your browser's cookies.

How Will We Use Your Data?

OpenSaaS.sh collects your data so that we can:

  • Analyze website traffic
  • Improve our website
  • Understand how visitors use our site

Third-Party Services

We use third-party services to help us operate our website, conduct our business, and provide services to you. These services may collect information about your visit to our website.

We use the following third-party services:

  • Google Analytics
  • REO.dev

These third-party service providers have their own privacy policies addressing how they use your information. We encourage you to review their privacy policies.

We only share information with these parties as necessary to provide our services to you, and they are obligated to keep your information confidential and secure.

How Do We Store Your Data?

OpenSaaS.sh securely stores your data using industry-standard security measures.

Security Measures

We implement the following security measures to protect your data:

  • SSL/TLS encryption
  • Secure cloud infrastructure

Data Retention

OpenSaaS.sh will keep your personal data for as long as necessary to fulfill the purposes outlined in this privacy policy. Once this period has expired, we will delete your data securely.

Marketing

OpenSaaS.sh would like to send you information about products and services of ours that we think you might like.

If you have agreed to receive marketing, you may always opt out at a later date. You have the right at any time to stop OpenSaaS.sh from contacting you for marketing purposes.

If you no longer wish to be contacted for marketing purposes, please contact us at [email protected].

Your Data Protection Rights

OpenSaaS.sh would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:

The right to access – You have the right to request copies of your personal data. We may charge you a small fee for this service.

The right to rectification – You have the right to request that we correct any information you believe is inaccurate. You also have the right to request that we complete information you believe is incomplete.

The right to erasure – You have the right to request that we erase your personal data, under certain conditions.

The right to restrict processing – You have the right to request that we restrict the processing of your personal data, under certain conditions.

The right to object to processing – You have the right to object to our processing of your personal data, under certain conditions.

The right to data portability – You have the right to request that we transfer the data that we have collected to another organization, or directly to you, under certain conditions.

If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us at:

Cookies

Cookies are text files placed on your computer to collect standard Internet log information and visitor behavior information. When you visit our website, we may collect information from you automatically through cookies or similar technology.

For further information, visit allaboutcookies.org.

How Do We Use Cookies?

OpenSaaS.sh uses cookies in a range of ways to improve your experience on our website, including:

  • Functional cookies: Keeping you signed in and remembering your preferences
  • Analytics cookies: Understanding how you use our website to improve our services

Managing Cookies

You can set your browser not to accept cookies, and the website allaboutcookies.org tells you how to remove cookies from your browser. However, in a few cases, some of our website features may not function as a result.

Privacy Policies of Other Websites

Our website may contain links to other websites. Our privacy policy applies only to our website, so if you click on a link to another website, you should read their privacy policy.

Changes to Our Privacy Policy

OpenSaaS.sh keeps its privacy policy under regular review and places any updates on this web page. This privacy policy was last updated on 2026-01-13.

How to Contact Us

If you have any questions about OpenSaaS.sh's privacy policy, the data we hold on you, or you would like to exercise one of your data protection rights, please do not hesitate to contact us at:

How to Contact the Appropriate Authority

Should you wish to report a complaint or if you feel that OpenSaaS.sh has not addressed your concern in a satisfactory manner, you may contact your local data protection supervisory authority.


This privacy policy is based on the GDPR privacy policy template from GDPR.eu.


Opensaas Sh Blog Src Content Docs 0.23 Guides Analytics (`opensaas-sh/blog/src/content/docs/0.23/guides/analytics.md`)


title: Analytics
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/analytics

This guide will show you how to integrate analytics for your app. You can choose between Google Analytics and Plausible.

Google Analytics is free, but uses cookies, so you'll probably want/need to implement the Cookie Consent Modal when using it.

Plausible is an open-source, privacy-friendly alternative to Google Analytics. You DO NOT have to use the cookie consent modal with Plausible, as it does not use cookies. It's also easier to use than Google if you use their hosted service, but be aware it is a paid feature. It is completely free if you want to self-host it, although this comes with some additional setup steps.

If you're looking to add analytics to your blog, you can follow the Adding Analytics to your Blog section at the end of this guide.

Plausible

Hosted Plausible

Sign up for a hosted Plausible account here.

Once you've signed up, you'll be taken to your dashboard. Create your site by adding your domain. Your domain is also your PLAUSIBLE_SITE_ID in your .env.server file. Make sure to add it.

sh
PLAUSIBLE_SITE_ID=<your domain without www>

After adding your domain, you'll be taken to a page with your Plausible script tag. Copy and paste this script tag into the main.wasp file's head section.

js {7}
app OpenSaaS {
  wasp: {
    version: "^0.23.0"
  },
  title: "My SaaS App",
  head: [
    "<your plausible script tag here>",
  ],
  //...

Go back to your Plausible dashboard, click on your username in the top right, and click on the Settings tab. Scroll down, find your API key and paste it into your .env.server file under the PLAUSIBLE_API_KEY variable.

:::note[No Cookies]
Plausible does not use cookies, so you don't need to add it to your Cookie Consent Modal, hence the script can be added directly to app.head in your main.wasp file.
:::

Self-hosted Plausible

Plausible, being an open-source project, allows you to self-host your analytics. This is a great option if you want to keep your data private and not pay for the hosted service.

coming soon...

...until then, check out the official documentation.

:::tip[Contribute!]
If you'd like to help us write this guide, click the "Edit page" button at the bottom of this page

As a completely free, open-source project, we appreciate any help 🙏
:::

Google Analytics

First off, head over to src/analytics/stats.ts and switch out the Plausible Provider for Google Analytics so that your background (cron) job fetches the data from Google Analytics for your Admin Dashboard:

ts ins={3} del={2} title="stats.ts"
//...
import { getDailyPageViews, getSources } from './providers/plausibleAnalyticsUtils';
import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';

export const calculateDailyStats: DailyStatsJob<never, void> = async (_args, context) => { 
  //...
}

Next, make sure you sign up for Google analytics, then go to your Admin panel in the bottom of the left sidebar and then create a "Property" for your app.

Once you've created a new Property, some Installation Instructions will pop up. Select install manually where you should see a string that looks like this:

sh title=""
"">https://www.googletagmanager.com/gtag/js?id=<your-google-analytics-id>

and copy and paste the Google Analytics ID into your .env.client file to get it working with the Cookie Consent Modal provided with this template:

sh title=".env.client"
REACT_APP_GOOGLE_ANALYTICS_ID=<your-google-analytics-id> # e.g. G-1234567890

:::tip[noscript]
In the Installation Instructions, Google Tag Manager might also instruct you to paste the noscript code snippet immediately after the opening `` tag.
You should skip this step because this snippet is activated only if users try to browse your app without JavaScript enabled, which is very rare and React needs JS anyway.
:::

Then, set up the Google Analytics API access by following these steps:

  1. Set up a Google Cloud project: If you haven't already, start by setting up a project in the Google Cloud Console.

  2. Enable the Google Analytics API for your project: Navigate to the "Library" in the Google Cloud Console and search for the "Google Analytics Data API" (for Google Analytics 4 properties) and enable it.

  3. Create credentials: Now go to the "Credentials" tab within your Google Cloud project, click on + credentials, and create a new service account key. First, give it a name. Then, under "Grant this service account access to project", choose viewer.

  4. Create Credentials: When you go back to Credentials page, you should see a new service account listed under "Service Accounts". It will be a long email address to ends with @your-project-id.iam.gserviceaccount.com. Click on the service account name to go to the service account details page.

    • Under "Keys" in the service account details page, click "Add Key" and choose Create new key.

    • Select "JSON", then click "Create" to download your new service account's JSON key file. Keep this file secure and don't add it to your git repo as it grants access to your Google Analytics data.

  5. Update your Google Anayltics Settings: Go back to your Google Analytics dashboard, and click on the Admin section in the left sidebar. Under Property Settings > Property > Property Access Management Add the service account email address (the one that ends with @your-project-id.iam.gserviceaccount.com) and give it Viewer permissions.

  6. Encode and add the Credentials: Add the client_email and the private_key from your JSON Key file into your .env.server file. But be careful! Because Google uses a special PEM private key, you need to first convert the key to base64, otherwise you will run into errors parsing the key. To do this, in a terminal window, run the command below and paste the output into your .env.server file under the GOOGLE_ANALYTICS_PRIVATE_KEY variable:

    sh
    echo -n "-----BEGIN PRIVATE KEY-----\nMI...A++eK\n-----END PRIVATE KEY-----\n" | base64
  7. Add your Google Analytics Property ID: You will find the Property ID in your Google Analytics dashboard in the Admin > Property > Property Settings > Property Details section of your Google Analytics property (not your Google Cloud console). Add this 9-digit number to your .env.server file under the GOOGLE_ANALYTICS_PROPERTY_ID variable.

Adding Analytics to your Blog

To add your analytics script to your Astro Starlight blog, all you need to do is modify the head property in your blog/astro.config.mjs file.

Below is an example of how to add Google Analytics to your blog:

js
export default defineConfig({
  site: 'https://opensaas.sh',
  integrations: [
    starlightBlog({ 
      // ...
    }),
    starlight({
      //...
       head: [
        {
          tag: 'script',
          attrs: {
            src: 'https://www.googletagmanager.com/gtag/js?id=<YOUR-GOOGLE-ANALYTICS-ID>',
          },
        },
        {
          tag: 'script',
          content: `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
        
          gtag('config', '<YOUR-GOOGLE-ANALYTICS-ID>');
          `,
        },
      ],

Opensaas Sh Blog Src Content Docs 0.23 Guides Authentication (`opensaas-sh/blog/src/content/docs/0.23/guides/authentication.md`)


title: Authentication
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/authentication

Setting up your app's authentication is easy with Wasp. In fact, it's already set up for you in the main.wasp file:

tsx title="main.wasp"
auth: {
    userEntity: User,
    methods: {
      email: {}, 
      google: {},
      gitHub: {},
      discord: {}
    },
    onAuthFailedRedirectTo: "/",
  },

The great part is, by defining your auth config in the main.wasp file, Wasp manages most of the Auth process for you, including the auth-related database entities for user credentials and sessions, as well as auto-generated client components for your app on the fly (aka AuthUI -- you can see them in use in the src/auth folder).

Email Verified Auth

email method is the default auth method in Open SaaS.

Since it needs to send emails to verify users and reset passwords, it requires an email sender provider: a service it can use to send emails.
A "email sender" provider is configured via app.emailSender field in the main.wasp file.

:::caution[Dummy Email Provider]
To make it easy for you to get started, Open SaaS initially comes with the Dummy "email sender" provider, which does not send any emails, but instead logs all email verification links/tokens to the server's console!
You can then follow these links to verify the user and continue with the sign-up process.

tsx title="main.wasp"
emailSender: {
    provider: Dummy, // Logs all email verification links/tokens to the server's console.
    defaultFrom: {
      name: "Open SaaS App",
      email: "[email protected]" 
    },
  },

You can not use the Dummy provider in production and your app will not build until you move to a production-ready provider, such as SendGrid. We outline the process of migrating to SendGrid below.
:::

In order to use the email auth method in production, you'll need to switch from the Dummy "email sender" provider to a production-ready provider like SendGrid:

  1. First, set up your app's emailSender in the main.wasp file by following this guide.
  2. Add your SENDGRID_API_KEY to the .env.server file.
  3. Make sure the email address you use in the fromField object is the same email address that you configured your SendGrid account to send out emails with. In the end, your main.wasp file should look something like this:
ts title="main.wasp" {6,7} del={15} ins={16}
auth: {
    methods: {
      email: {
        fromField: {
          name: "Open SaaS App",
          // When using SendGrid, you must use the same email address that you configured your account to send out emails with!
          email: "[email protected]" 
        },
        //...
      }, 
    }
  },
  //...
  emailSender: {
    provider: Dummy,
    provider: SendGrid,
    defaultFrom: {
      name: "Open SaaS App",
      // When using SendGrid, you must use the same email address that you configured your account to send out emails with!
      email: "[email protected]" 
    },
  },

And that's it. Wasp will take care of the rest and update your AuthUI components accordingly.

Check out the Wasp Auth docs for more info.

Google, GitHub, & Discord Auth

We've also customized and pre-built the Google, Discord and GitHub auth flow for you. To start using them, you just need to uncomment out the methods you want in your main.wasp file and obtain the proper API keys to add to your .env.server file.

To create a Google OAuth app and get your Google API keys, follow the instructions in Wasp's Google Auth docs.

To create a GitHub OAuth app and get your GitHub API keys, follow the instructions in Wasp's GitHub Auth docs.

To create a Discord OAuth app and get your Discord API keys, follow the instructions in Wasp's Discord Auth docs

Again, Wasp will take care of the rest and update your AuthUI components accordingly.


Opensaas Sh Blog Src Content Docs 0.23 Guides Authorization (`opensaas-sh/blog/src/content/docs/0.23/guides/authorization.md`)


title: Authorization
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/authorization

This guide will help you get started with authorization in your SaaS app.

Authorization refers to what users can access in your app. This is useful for differentiating between users who have paid for different subscription tiers (e.g. "hobby" vs "pro"), or between users who have admin privileges and those who do not.

Authorization differs from authentication in that authentication refers to the process of verifying that a user is who they say they are (e.g. logging in with a username and password).

To learn more about the different types of user permissions built into this SaaS template, including Stripe subscription tiers and statuses, check out the User Overview Reference.

Also, check out our blog post to learn more about authorization (access control) in web apps.

Client-side Authorization

Open Saas starts with all users having access to the landing page (/), but only authenticated users having access to the rest of the app (e.g. to the /demo-app, or to the /account).

To control which pages require users to be authenticated to access them, you can set the authRequired property of the corresponding page definition in your main.wasp file:

tsx title="main.wasp" {3}
route AccountRoute { path: "/account", to: AccountPage }
page AccountPage {
  authRequired: true,
  component: import Account from "@src/user/AccountPage"
}

This will automatically redirect users to the login page if they are not logged in while trying to access that page.

:::caution[Client-side authorization is just for the looks]
Users can manipulate the client code as they wish, meaning that client-side authorization (access control) serves the purpose of ergonomics/user experience, not the purpose of restricting access to sensitive data.

This means that authorization in the client code is a nice-to-have: it is here to make sure users don't get lost in the part of the app they can't work with because data is missing due to them not having access, not to actually restrict them from doing something.

Actually ensuring they don't have access to the data, that is on the server to ensure, via server-side logic that you will implement for authorization.
:::

If you want more fine-grained control over what users can access, there are two Wasp-specific options:

  1. When you define the authRequired: true property on the page definition, Wasp automatically passes the User object to the page component. Here you can check for certain user properties before authorizing access:
tsx title="examplepage.tsx" "{ user }: { user: user }"
import { type User } from "wasp/entities";

export function ExamplePage({ user }: { user: User }) {
  if (user.subscriptionStatus === 'past_due') {
    return (<span>Your subscription is past due. Please update your payment information.</span>)
  }
  if (user.subscriptionStatus === 'cancel_at_period_end') {
    return (<span>Your susbscription will end on 01.01.2024</span>)
  }
  if (user.subscriptionStatus === 'active') {
    return (<span>Thanks so much for your support!</span>)
  }
};
  1. Or you can take advantage of the useAuth hook and check for certain user properties before authorizing access to certain pages or components:
tsx title="examplecomponent.tsx" {1, 4}
import { useAuth } from "wasp/client/auth";

export function ExampleComponent() {
  const { data: user } = useAuth();

  return (
    <h1> Hi {user.email || 'there'} 👋 </h1>
  )
}

Server-side Authorization

Authorization on the server-side is the core of your access control logic, and determines what users actually can or can't do (unlike client-side authorization logic which is there merely for UX).

You can authorize access to server-side operations by adding a check for a logged-in user on the context.user object which is passed to all operations in Wasp:

tsx title="src/server/actions.ts" "context.user"
export const someServerAction: SomeServerAction<...> = async (args, context) => {
  if (!context.user) {
    throw new HttpError(401); // Throw an error if user is not logged in.
  }

  if (context.user.subscriptionStatus === 'past_due') {
    throw new HttpError(403, 'Your subscription is past due. Please update your payment information.');
  }
  //...
}


title: Cookie Consent Modal
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

import cookieBanner from '@assets/cookie-consent/cookiebanner.png';
import preferences from '@assets/cookie-consent/preferences.png';
import { Image } from 'astro:assets';

Cookie consent banners are annoying, we know. But they are legally required in many countries, so we have to deal with them.

This guide will help you dynamically add or remove cookies from your app via the cookie consent modal that comes with this template.

This is needed for non-essential cookies that are not necessary for the basic functionality of your app, such as analytics cookies or marketing cookies.

The cookie consent modal can be found at app/src/client/components/cookie-consent/ and contains two main files:

  1. Banner.tsx - the component that displays the banner at the bottom of the page.
  2. Config.ts - the configuration file that contains the cookies/scripts that will be dynamically added.

The Banner.tsx component is imported in app/src/client/App.tsx and is rendered at the bottom of the page, while all the changes to the banner itself are done within the Config.ts file, which we explain below.

Configuration

We decided to use the vanilla-cookieconsent library to handle the cookie consent. We've set it up to give you some basic functionality, using mostly the default settings. For a full list of options, you can check the official documentation.

Below, we will guide you through the necessary steps to get the cookie consent modal set up for your app.

Google Analytics

What's impotant to note for this template is that we are simply using the onAccept callbacks to dynamically add or remove our Google Analytics cookies from the page. In order for it to work correctly with your app, you need to add your Google Analytics ID to your .env.client file.

sh title=".env.client"
REACT_APP_GOOGLE_ANALYTICS_ID=G-1234567890

And that's it! The cookie consent modal will now dynamically add or remove the Google Analytics cookies based on the user's choice.

To check if it's working correctly, you can open the browser's developer tools and check the cookies tab. You should see the following cookies being added or removed based on the user's choice:

sh
_ga
_ga... # Google Analytics cookies.
cc_cookie # Cookie Consent cookie. The name of this cookie can be changed in the config file.

Plausible Analytics

If you decide to go with Plausible Analytics, you DO NOT need to ask users for their consent to use cookies because Plausible, as a privacy-first analytics provider, does not use cookies. Instead, It collects website usage data anonymously and in aggregate form only, without any personally identifiable information

By avoiding cookies, Plausible Analytics avoids the need for cookie consent banners.

Your Terms / Privacy Policy

You should also add a link to your terms and privacy policy within consentModal section of config.language:

ts title="config.ts" {10,11}
language: {
      default: 'en',
      translations: {
        en: {
          consentModal: {
            title: 'We use cookies',
            // ...
            // TODO: Add your own privacy policy and terms and conditions links below.
            footer: `
            <a href="<your-url-here>" target="_blank">Privacy Policy</a>
            <a href="<your-url-here>" target="_blank">Terms and Conditions</a>
                    `,
          },
        },
      },
    }

Allowing Users to Control Certain Cookies (OPTIONAL)

If you've added more than just Google Analytics cookies to your app, you can allow users to control which cookies they want to accept or reject. For example, if you've added marketing cookies, you can add a button to the modal that allows users to reject them, while accepting analytics cookies.

To do that, you can change the preferencesModal.sections property in config.language. Any section that you add to preferencesModal.sections must match a linkedCategory in the config.categories property. Make sure you also add a showPreferencesBtn property to consentModal (highlighted below).

Below is an example of what your config might look like if you want to give users the option to control over multiple cookie preferences:

text
/* Detailed source-code truncated for AI context efficiency. */

For more information on how to do that, check the official documentation.


Opensaas Sh Blog Src Content Docs 0.23 Guides Deploying (`opensaas-sh/blog/src/content/docs/0.23/guides/deploying.mdx`)


title: Deploying
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/deploying

import stripeListenEvents from '@assets/stripe/listen-to-stripe-events.png';
import npmVersion from '@assets/stripe/npm-version.png';
import stripeSigningSecret from '@assets/stripe/stripe-webhook-signing-secret.png';
import { Image } from 'astro:assets';

Because this SaaS app is a React/NodeJS/Postgres app built on top of Wasp, Open SaaS can take advantage of Wasp's easy, one-command deploy or manual deploy to any provider of your choice.

The simplest and quickest option is to take advantage of Wasp's one-command deploy to Fly.io or Railway.

Or if you prefer to deploy to a different provider, or your frontend and backend separately, you can follow the Deploying Manually section below.

Steps for Deploying

These are the steps necessary for you to deploy your app. We recommend you follow these steps in order.

Each of these steps is covered in more detail below.

Prerequisites

AWS S3 CORS configuration

If you're storing files in AWS S3, ensure you've listed your production domain
in the bucket's CORS configuration under AllowedOrigins. Check the File
uploading guide
for details.

Env Vars

Make sure you've got all your API keys and environment variables set up before you deploy.

Payment Processor Vars

In the Payments Processor integration guide, you set up your API keys using test keys and test product ids. You'll need to get the live/production versions of those keys. To get these, repeat the instructions in the Integration Guide without being in test mode. Add the new keys to your deployed environment secrets.

Other Vars

Many of your other environment variables will probably be the same as in development, but you should double-check that they are set correctly for production.

Here are a list of all of them (some of which you may not be using, e.g. Analytics, Social Auth) in case you need to check:

General Vars

  • [ ] DATABASE_URL
  • [ ] JWT_SECRET
  • [ ] WASP_WEB_CLIENT_URL
  • [ ] WASP_SERVER_URL

Open AI API Key

  • [ ] OPENAI_API_KEY

Sendgrid API Key

  • [ ] SENDGRID_API_KEY

Social Auth Vars

  • [ ] GOOGLE_CLIENT_ID
  • [ ] GOOGLE_CLIENT_SECRET
  • [ ] GITHUB_CLIENT_ID
  • [ ] GITHUB_CLIENT_SECRET

Analytics Vars

  • [ ] REACT_APP_PLAUSIBLE_ANALYTICS_ID (for client-side)
  • [ ] PLAUSIBLE_API_KEY
  • [ ] PLAUSIBLE_SITE_ID
  • [ ] PLAUSIBLE_BASE_URL
  • [ ] REACT_APP_GOOGLE_ANALYTICS_ID (for client-side)
  • [ ] GOOGLE_ANALYTICS_CLIENT_EMAIL
  • [ ] GOOGLE_ANALYTICS_PROPERTY_ID
  • [ ] GOOGLE_ANALYTICS_PRIVATE_KEY
    (Make sure you convert the private key within the JSON file to base64 first with echo -n "PRIVATE_KEY" | base64. See the Analytics docs for more info)

AWS S3 Vars

  • [ ] AWS_S3_IAM_ACCESS_KEY
  • [ ] AWS_S3_IAM_SECRET_KEY
  • [ ] AWS_S3_FILES_BUCKET
  • [ ] AWS_S3_REGION

:::tip[Deployed? Get some swag! 👕]
Do you have an Open SaaS app running in production? If yes, we'd love to send some swag your way! All you need to do is fill out this form and we'll make it happen.
:::

Adding Server Redirect URL's to Social Auth

After deploying your server, you need to add the correct redirect URIs to the credential settings. For this, refer to the guides from the Wasp Docs.

Payment Providers Production Settings

Stripe

Now you need to set up your stripe webhook for production use. Below are some important steps and considerations you should take as you prepare to deploy your app to production.

Stripe API Versions

When you create your Stripe account, Stripe will automatically assign you to their latest API version at that time. This API version is important because it determines the structure of the responses Stripe sends to your webhook, as well as the structure it expects of the requests you make toward the Stripe API.

Because this template was built with a specific version of the Stripe API in mind, it could be that your Stripe account is set to a different API version.

:::note

ts title="stripeclient.ts"
const STRIPE_API_VERSION = "2025-04-30.basil"; // or e.g. 2023-08-16 if using older format

export const stripeClient = new Stripe(env.STRIPE_API_KEY, {
  apiVersion: STRIPE_API_VERSION,
});

When you specify a specific API version in your Stripe client, the requests you send to Stripe from your server, along with their responses, will match that API version. On the other hand, Stripe will send all other events to your webhook that didn't originate as a request sent from your server, like those made after a user completes a payment on checkout, using the default API version of the API.

This is why it's important to make sure your Stripe client version also matches the API version in your Stripe account, and to thoroughly test any changes you make to your Stripe client before deploying to production.
:::

To make sure your app is consistent with your Stripe account, here are some steps you can follow:

  1. You can find your default API version in the Stripe dashboard under the Developers section.
  2. Check that the API version in your /src/payment/stripe/stripeClient.ts file matches the default API version in your dashboard:
    ts title="stripeclient.ts" {1}
    const STRIPE_API_VERSION = "2025-04-30.basil"; // or e.g. 2023-08-16 if using older format
    
    export const stripeClient = new Stripe(env.STRIPE_API_KEY, {
      apiVersion: STRIPE_API_VERSION,
    });
  3. If they don't match, you can upgrade/downgrade your Stripe NPM package in package.json to match the API version in your dashboard:
  • If your default version on the Stripe dashboard is also the latest version of the API, you can simply upgrade your Stripe NPM package to the latest version.
  • If your default version on the Stripe dashboard is not the latest version, and you don't want to upgrade to the latest version, because e.g. you have other projects that depend on the current version, you can find and install the Stripe NPM package version that matches your default API version by following these steps:
    • Find and note the date of your default API version in the developer dashboard.

    • Go to the Stripe NPM package page and hover over Published date column until you find the package release that matches your version. For example, here we find the NPM version 13.x.x matches the API version of 2023-08-16.

    • Install the correct version of the Stripe NPM package by running:

      sh
      npm install [email protected] # e.g. npm install [email protected]
  1. Test your app thoroughly to make sure that the changes you made to your Stripe client are working as expected before deploying to production.

Creating Your Production Webhook

  1. Go to https://dashboard.stripe.com/webhooks

  2. Click on + add endpoint

  3. Enter your endpoint url, which will be the url of your deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook

  4. Select the events you want to listen to. These should be the same events you're consuming in your webhook which are handled in src/payment/stripe/webhook.ts:

  5. After that, go to the webhook you just created and reveal the new signing secret.

  6. Add this secret to your deployed server's STRIPE_WEBHOOK_SECRET environment variable. If you've deployed to Fly.io, you can do that easily with the following command:

    sh
    wasp deploy fly cmd --context server secrets set STRIPE_WEBHOOK_SECRET=whsec_...

Lemon Squeezy

Creating Your Production Webhook

To set up your Lemon Squeezy webhook, you'll need the URL of you newly deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook.

With the webhook url ready, go to your Lemon Squeezy Webhooks Dashboard:

  1. Click the + button.
  2. Add the webhook forwarding url to the Callback URL section.
  3. Give your webhook a signing secret (a long, random string).
  4. Add this signing secret to your server's production environment variables under LEMONSQUEEZY_WEBHOOK_SECRET.
  5. Make sure to select at least the following updates to be sent:
    • order_created
    • subscription_created
    • subscription_updated
    • subscription_cancelled
  6. Click save.

Polar

Turning off the Sandbox mode

Make sure to turn off the sandbox mode in your server's production environemnt:

ini title=".env.server"
POLAR_SANDBOX_MODE=false

Creating Your Production Webhook

To set up your Polar webhook, you'll need the URL of your newly deployed server + /payments-webhook, e.g. https://open-saas-wasp-sh-server.fly.dev/payments-webhook.

With the webhook URL ready, in your Polar Dashboard:

  1. Navigate to Settings > Webhooks.
  2. Click the Add Endpoint button.
  3. In the URL field, paste your production forwarding address.
  4. Set the Format to "Raw".
  5. Select the following events to listen for:
    • order.paid
    • subscription.updated
  6. Click Save.
  7. Copy the generated webhook secret and set it as the POLAR_WEBHOOK_SECRET environment variable in production.

Customizing SEO and Discoverability

Before deploying, make sure your app's meta tags, JSON-LD schema, and llms.txt reflect your production URL and content so search engines and AI/LLM tools can index and recommend your app correctly. See the SEO guide for details.

Deploying your App

Deploying to Fly.io

Fly.io is a platform for running your apps globally. It's a great choice for deploying your SaaS app because it's free to get started, can host your entire full-stack app in one place, scales well, and has one-command deploy integration with Wasp.

Wasp provides the handy wasp deploy command to deploy your entire full-stack app (DB, server, and client) in one command.

To learn how, please follow the detailed guide for deploying to Fly via the Wasp CLI from the Wasp documentation. We suggest you follow this guide carefully to get your app deployed.

:::caution[Setting Environment Variables]
Remember, because we've set certain client-side env variables, make sure to pass them to the wasp deploy commands so that they can be included in the build:

sh
REACT_APP_CLIENT_ENV_VAR_1=<...> REACT_APP_CLIENT_ENV_VAR_2=<...> wasp deploy

The wasp deploy command will also take care of setting the following server-side environment variables for you so you don't have to:

  • DATABASE_URL
  • PORT
  • JWT_SECRET
  • WASP_WEB_CLIENT_URL
  • WASP_SERVER_URL

For setting the remaining server-side environment variables, please refer to the Deploying with the Wasp CLI Guide.
:::

Deploying Manually

If you prefer to deploy manually, your frontend and backend separately, or just prefer using your favorite provider you can follow Wasp's Manual Deployment Guide.

:::caution[Client-side Environment Variables]
Remember to always set additional client-side environment variables, such as REACT_APP_STRIPE_CUSTOMER_PORTAL by appending them to the build command, e.g.

sh
REACT_APP_CLIENT_ENV_VAR_1=<...> npm run build

:::

Deploying your Blog

Deploying your Astro Starlight blog is a bit different than deploying your SaaS app. As an example, we will show you how to deploy your blog for free to Netlify. You will need a Netlify account and Netlify CLI installed to follow these instructions.

Make sure you are logged in with Netlify CLI:

  • You can check if you are logged in with netlify status.
  • You can log in with netlify login.

Position yourself in the blog directory and run the following command:

sh
npm run build

This will build your blog into the blog/dist directory. Now you can deploy your blog to Netlify with the following command:

sh
netlify deploy

Select the dist directory as the deploy path.

Finally, if the deployment looks good, you can deploy your blog to production with the following command:

sh
netlify deploy --prod

Opensaas Sh Blog Src Content Docs 0.23 Guides Email Sending (`opensaas-sh/blog/src/content/docs/0.23/guides/email-sending.mdx`)


title: Email Sending
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/email-sending

import { TabItem, Tabs } from '@astrojs/starlight/components';

This guide explains how to use the integrated email sender and how you can integrate your own account in this template.

Sending Emails

The `Dummy` Email Provider (for Local Dev Only)

By default we've set up the email sender to use the Dummy provider. This is for local development only and no emails will actually be sent out!
To obtain an email verification token/link, you must check the server logs on initial sign up. You can click this link to verify your email and continue with the sign up process.

tsx title="main.wasp"
app SaaSTemplate {
  // ...
  emailSender: {
    provider: Dummy,
    defaultFrom: {
      name: "Open SaaS App",
      email: "[email protected]" 
    },
  },

Note that your app will not build if using the Dummy provider and you must switch to a production-ready provider in order to do so.

Using a Production-Ready Email Provider (e.g. SendGrid)

To change your email provider to a production-ready one, such as SendGrid, you'll want to configure your emailSender like so:

tsx title="main.wasp"
app SaaSTemplate {
  // ...
  emailSender: {
    provider: SendGrid,
    defaultFrom: {
      name: "Open SaaS App",
      // When using SendGrid, you must use the same email address that you configured your account to send out emails with!
      email: "[email protected]" 
    },
  },

This means that you can send emails from your app using the send function from the email modul provided by Wasp:

tsx title="src/server/webhooks.ts"
import { emailSender } from "wasp/server/email";

//...

  if (subscription.cancel_at_period_end) {
    await emailSender.send({
      to: customer.email,
      subject: 'We hate to see you go :(',
      text: 'We hate to see you go. Here is a sweet offer...',
      html: 'We hate to see you go. Here is a sweet offer...',
    });
  }

In the example above, you can see that we're sending an email to the customer when we receive a cancel subscription event within the Stripe webhook.

This is a powerful feature and super simple to use.

Integrate your email sender

To set up your email sender, you first need an account with one of the supported email providers.

* Register at SendGrid.com and then get your [API KEYS](https://app.sendgrid.com/settings/api_keys). * Copy yours to the `.env.server` file under the `SENDGRID_API_KEY` variable.
text
Make sure to change the `defaultFrom` email address in the `main.wasp` file to use the same email address that you configured your account to send out emails with!

```tsx title="main.wasp" {5}
emailSender: {
  provider: SendGrid,
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]" // Same email address you configured your SendGrid account to send emails with!
  },
```
* Go to [Mailgun](https://mailgun.com) and create an account. * Go to [API Keys](https://app.mailgun.com/settings/api_security/api_keys?onboardingTask=api-key) and create a new API key. * Copy the API key and add it to your .env.server file under the `MAILGUN_API_KEY` variable. * Go to [Domains](https://app.mailgun.com/mg/sending/new-domain?onboardingTask=add-verify-domain) and create a new domain. * Copy the domain and add it to your .env.server file as `MAILGUN_DOMAIN`.
text
Make sure to change the `defaultFrom` email address in the `main.wasp` file to use the same email address that you configured your account to send out emails with!

```tsx title="main.wasp" {5}
emailSender: {
  provider: Mailgun,
  defaultFrom: {
    name: "Open SaaS App",
    email: "[email protected]" // Same email address you configured your Mailgun account to send emails with!
  },
```

If you want more detailed info, or would like to use SMTP, check out the Wasp docs.


Opensaas Sh Blog Src Content Docs 0.23 Guides File Uploading (`opensaas-sh/blog/src/content/docs/0.23/guides/file-uploading.mdx`)


title: File Uploading
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/file-uploading

import cors from '@assets/file-uploads/cors.png';
import createBucket from '@assets/file-uploads/create-bucket.png';
import defaultSettings from '@assets/file-uploads/default-settings.png';
import findS3 from '@assets/file-uploads/find-s3.png';
import keys from '@assets/file-uploads/keys.png';
import newBucket from '@assets/file-uploads/new-bucket.png';
import permissions from '@assets/file-uploads/permissions.png';
import username from '@assets/file-uploads/username.png';
import { Image } from 'astro:assets';

This guide will show you how to set up file uploading in your SaaS app.

There are two options we recommend:

  1. Using AWS S3 with presigned URLS for secure file storage.
  2. Using Multer middleware to upload files to your own server.

We recommend using AWS S3 as it's a scalable, secure option, that can handle a large amount of storage.

If you're just looking to upload small files and don't expect your app to grow to a large scale, you can use Multer to upload files to your app's server.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Using AWS S3

How presigned URLs work

Presigned URLs are URLs that have been signed with your AWS credentials and can be used to upload files to your S3 bucket. They are time-limited and can be generated on the server and sent to the client to upload files directly to S3.

The process of generating a presigned URL is as follows:

  1. The client sends a request to the server to upload a file.
  2. The server generates a presigned URL using its AWS credentials.
  3. The server sends the presigned URL to the client.
  4. The client uses the presigned URL to upload the file directly to S3 before the URL expires.

We use this method to upload files to S3 because it is more secure than uploading files directly from the client to S3. It also allows us to keep our AWS credentials private and not expose them to the client.

To use presigned URLs, we'll need to set up an S3 bucket and get our AWS credentials.

Create an AWS Account

Before you begin, you'll need to create an AWS account. AWS accounts are free to create and are split up into:

  1. Root account.
  2. IAM users.

You'll need to first create a root account, and then an IAM user for your SaaS app before you can start uploading files to S3.

To do so, follow the steps in this external guide: Creating IAM users and S3 buckets in AWS.

Create an AWS S3 Bucket

Once you are logged in with your IAM user, you'll need to create an S3 bucket to store your files:

  1. Navigate to the S3 service in the AWS console.

  2. Click on the Create bucket button.

  3. Fill in the bucket name and region.

  4. Leave all the settings as default and click Create bucket.

Change the CORS settings

Now we need to change some permissions on the bucket to allow for file uploads from your app:

  1. Click on the bucket you just created.

  2. Click on the Permissions tab.

  3. Scroll down to the Cross-origin resource sharing (CORS) section and click Edit.

  4. Insert the correct CORS configuration and click Save changes. You can
    copy-paste most of the config below, but you must edit the
    AllowedOrigins field
    to fit your app. Include http://localhost:3000 for
    local development, and https://<your domain> for production.

    If you don't yet have a domain name, just list http://localhost:3000 for
    now. We'll remind you to add your domain before deploying to production in
    the Deployment docs.

    json {11,12}
    [
        {
            "AllowedHeaders": [
                "*"
            ],
            "AllowedMethods": [
                "POST",
                "GET"
            ],
            "AllowedOrigins": [
                "http://localhost:3000",
                "https://<your-domain>"
            ],
            "ExposeHeaders": []
        }
    ]

Get your AWS S3 credentials

Now that you have your S3 bucket set up, you'll need to get your S3 credentials to use in your app:

  1. Click on your username in the top right corner of the AWS console and select Security Credentials.

  2. Scroll down to the Access keys section.

  3. Click on Create Access Key.

  4. Select the Application running on an AWS service option and create the access key.

  5. Copy the Access key ID and Secret access key and paste them in your src/app/.env.server file:

    sh
    AWS_S3_IAM_ACCESS_KEY=ACK...
      AWS_S3_IAM_SECRET_KEY=t+33a...
      AWS_S3_FILES_BUCKET=your-bucket-name
      AWS_S3_REGION=your-region // (e.g. us-west-2)

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Using and Customizing File Uploads with S3 in your App

With your S3 bucket set up and your AWS credentials in place, you can now start uploading files in your app using presigned URLs by navigating to localhost:3000/file-upload and uploading a file.

To begin customizing file uploads, is important to know where everything lives in your app. Here's a quick overview:

  • main.wasp:
    • The File entity can be found here. Here you can modify the fields to suit your needs.
  • src/file-upload/FileUploadPage.tsx:
    • The FileUploadPage component is where the file upload form lives. It also allows you to download the file from S3 by calling the getDownloadFileSignedURL based on that files key in the app DB.
  • src/file-upload/operations.ts:
    • The createFileUploadUrl action lives here and calls the getUploadFileSignedURLFromS3 within it using your AWS credentials before passing it to the client. This function stores the files in the S3 bucket within folders named after the user's ID, so that each user's files are stored separately.
    • The getAllFilesByUser fetches all File information uploaded by the user. Note that the files do not exist in the app database, but rather the file data, its name and its key, which is used to fetch the file from S3.
    • The getDownloadFileSignedURL query fetches the presigned URL for a file to be downloaded from S3 using the file's key stored in the app's database.

Cleaning up "orphaned" files in S3

In the current logic, files are first deleted from the app's database before attempting to delete them from S3. If, for some reason, the S3 deletion were to fail, the file would remain in S3 and not in the app's database and be orphaned:

ts title="src/file-upload/operations.ts"
export const deleteFile: DeleteFile<DeleteFileInput, File> = async (
  rawArgs,
  context,
) => {
  // ...
  const deletedFile = await context.entities.File.delete({
    where: {
      id: args.id,
      user: {
        id: context.user.id,
      },
    },
  });

  try {
    await deleteFileFromS3({ s3Key: deletedFile.s3Key });
  } catch (error) {
    console.error(
      `S3 deletion failed. Orphaned file s3Key: ${deletedFile.s3Key}`,
      error,
    );
  }
};

To clean up these orphaned files, you can add a cleanup job (that runs at an interval of your choosing) to:

  1. Fetch all file keys from S3.
  2. Fetch all file keys from the app's database.
  3. Compare the two lists and delete any files from S3 that are not in the database.

Here's an example of how you could implement this:

ts title="main.wasp"
job cleanUpOrphanedFilesS3Job {
  executor: PgBoss,
  perform: {
    fn: import { cleanUpOrphanedFilesS3 } from "@src/file-upload/workers"
  },
  schedule: {
    cron: "0 5 * * 0" // Every week on Sunday at 5am.
  },
  entities: [File]
}
text
/* Detailed source-code truncated for AI context efficiency. */

Using Multer to upload files to your server

If you're looking to upload files to the app server, you can use the Multer middleware to handle file uploads. This will allow you to store files on your server and is a good option if you need a quick and dirty, free solution for simple file uploads.

Below are GitHub Gists that show you how to set up file uploads using Multer in your app:


Opensaas Sh Blog Src Content Docs 0.23 Guides Seo Performance (`opensaas-sh/blog/src/content/docs/0.23/guides/seo-performance.mdx`)


title: SEO & Performance
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/seo-performance

import { TabItem, Tabs } from '@astrojs/starlight/components';
import { Image } from 'astro:assets';

This guides explains how to improve the SEO and performance for of your app.

Landing Page Meta Tags

Wasp gives you the ability to add meta tags to your landing page HTML via the main.wasp file's head property:

js {8-11}
app SaaSTemplate {
  wasp: {
    version: "^0.23.0"
  },
  title: "Open SaaS",
  head: [
    "<meta property='og:type' content='website' />",
    "<meta property='og:url' content='https://opensaas.sh' />",
    "<meta property='og:title' content='Open SaaS' />",
    "<meta property='og:description' content='Free, open-source SaaS boilerplate starter for React & NodeJS.' />",
    "<meta property='og:image' content='https://opensaas.sh/public-banner.webp' />",
    //...
  ],
  //...

Change the above highlighted meta tags to match your app. Wasp will inject these tags into the HTML of your index.html file, which is the Landing Page (app/src/client/landing-page/LandingPage.tsx), in this case.

Prerendering Routes

By default, a Wasp app is a Single Page Application (SPA). When someone visits your site, their browser first downloads a mostly empty page and then runs JavaScript to fill it in with the actual content. This works well for pages behind a login, but it has two downsides for pages like your landing page:

  • Visitors wait longer before they see anything.
  • Search engines and AI crawlers sometimes struggle to see your real content, which hurts your ranking.

Prerendering fixes both. You can turn it on for any route by adding prerender: true in your config file:

```ts app.route('LandingPageRoute', { path: '/', to: landingPage, prerender: true }); ``` ```c route LandingPageRoute { path: "/", to: LandingPage, prerender: true } ```

Now, when you run wasp build, Wasp builds a complete HTML version of that page ahead of time. Visitors (and Google) see your real content right away, and React still takes over in the background so buttons, links, and other interactions keep working as normal.

For a content-heavy landing page, this typically makes the page feel several times faster to load.

See the Wasp docs for the full reference and caveats: Prerendering.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Structured Data (JSON-LD)

In addition to meta tags, Open SaaS includes a SchemaMarkup component at src/landing-page/components/SchemaMarkup.tsx that embeds JSON-LD structured data into your landing page. This helps search engines and LLMs understand what your app is, who makes it, and what it offers, improving how it appears in search results, AI answers, and rich snippets.

tsx title="src/landing-page/components/schemamarkup.tsx"
const schema = {
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "SoftwareApplication",
      name: "Your Open SaaS App",
      description: "Your apps main description and features.",
      url: "https://your-saas-app.com",
      // ...
    },
  ],
};

Customize the schema object to match your product:

  • Update name, description, and url
  • Set applicationCategory / applicationSubCategory (see schema.org/SoftwareApplication)
  • Add an Organization entry to associate the app with a company
  • Add a FAQPage entry if your landing page has an FAQ section

The component is rendered inside LandingPage.tsx, and Wasp's prerendering inlines its output into the static HTML so crawlers see the structured data without needing to execute JavaScript.

AI / LLM Discoverability

Beyond traditional search engines, AI assistants and LLM-powered search tools (ChatGPT, Perplexity, Claude, etc.) are an increasingly important source of discovery. The llms.txt convention provides a curated, markdown index of your site that these tools can use to understand and recommend your app.

The template ships a placeholder llms.txt at template/app/public/llms.txt, which gets served at https://your-saas-app.com/llms.txt:

md title="public/llms.txt"
# Your Open SaaS App

> One-line description of what your SaaS does and who it's for.

A longer paragraph giving context: the problem your app solves, the kind of
user it serves, and the key value it delivers.

## Core pages
- [Home](https://your-saas-app.com): Landing page with features, pricing, and FAQ
- [Pricing](https://your-saas-app.com/pricing): Subscription plans and pricing
- [Sign up](https://your-saas-app.com/signup): Create a new account

Customize it before deploying:

  • Replace your-saas-app.com with your production URL
  • Update the name, description, and longer paragraph to match your product
  • Curate the list of links — include only your most important public pages
  • Add or remove sections as needed (e.g. ## API, ## Integrations)

Keep the file short and intentional. It should be a concise index of your site that AI crawlers and LLMs can digest easily.

Docs & Blog Meta Tags

Astro, being a static-site generator, will automatically inject relevant information provided in the blog/astro.config.mjs file, as well as in the frontmatter of .md files into the pages HTML:

yaml
---
title: 'My First Blog Post'
pubDate: 2022-07-01
description: 'This is the first post of my new Astro blog.'
author: 'Astro Learner'
image:
    url: 'https://docs.astro.build/assets/full-logo-light.png'
    alt: 'The full Astro logo.'
tags: ["astro", "blogging", "learning in public"]
---

Improving your SEO is as simple as adding these properties to your docs and blog content!


Opensaas Sh Blog Src Content Docs 0.23 Guides Tests (`opensaas-sh/blog/src/content/docs/0.23/guides/tests.md`)


title: Tests
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/tests

This guide will show you how to use the included end-to-end (e2e) tests for your Open SaaS application.

The Tests Directory

In the root of your project, you'll find an e2e-tests directory which contains the Playwright tests for your Open SaaS application:

text
.
├── e2e-tests/
│   ├── tests/                  # Directory containing the test files
│   ├── README.md               # Instructions on how to run the tests
│   ├── playwright.config.ts    # Playwright configuration
│   ├── package.json
│   ├── ...

To run the tests locally, or in a CI pipeline, follow the instructions in the README.md file in the e2e-tests directory.

Using Tests in CI with GitHub Actions

Although the Open SaaS template does not come with an example GitHub workflow, you can find out how Open SaaS itself set it up in .github/workflows/e2e-tests.yml at Open SaaS repo.

You can copy and paste the .github/ directory containing the e2e-tests.yml workflow into the root of your own repository to run the tests as part of your CI pipeline.

:::caution[WASP_VERSION]
Please make sure to update the WASP_VERSION environment variable in the e2e-tests.yml file to match the version of Wasp you are using in your project.
:::

In order for these tests to run correctly on GitHub, you need to provide the environment variables mentioned in the e2e-tests.yml file within your GitHub repository's "Actions" secrets so that they can be accessed by the tests.


Opensaas Sh Blog Src Content Docs 0.23 Guides Updating Opensaas (`opensaas-sh/blog/src/content/docs/0.23/guides/updating-opensaas.md`)


title: How (Not) to Update Your Open SaaS App
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/updating-opensaas

:::danger[We advise against merging the latest template changes into your app]
If you've already started building your app, we generally advise against merging the latest template changes into your app.

Below we outline our reasoning why, and provide a basic guide to help you update your app if you decide to do so anyway.
:::

Why you probably shouldn't include the latest template changes in your app

We generally advise against updating your Open SaaS-based applications after initial setup.

Why?

Because your codebase will naturally diverge from the template as you build your unique application, and any updates we may make to the template may not be compatible with your modified codebase, or your version of Wasp.

Even if you really want to include a new feature from the template in your app, proceed with caution and thoroughly consider the following:

  • Changes to the template may be tightly coupled. Implementing one change without related ones could cause unexpected issues.
  • Updates might not be compatible with your version of Wasp.
  • The more your codebase has diverged, the more challenging the update will be.

If you still decide to update your app

If you read above, considered the risks, and still need specific improvements, we recommend that you manually merge the changes.

To do this, you should can either 1) merge new Open SaaS template changes into your current project, or 2) merge project changes into a fresh Open SaaS template.

  1. Merge new Open SaaS template changes into your current project by:

    • reviewing the latest commits,
    • understanding what happened,
    • being mindful of the Wasp version you're using,
    • and then fitting those changes into your own codebase.
  2. Merge your project changes into a fresh Open SaaS template by:

    • starting a new, fresh project with the latest Open SaaS template,
    • and then copying over the logic from your existing project that you want to keep.

The method you choose is up to you and will largely depend on the complexity of the changes you need to make.


Opensaas Sh Blog Src Content Docs 0.23 Guides Vibe Coding (`opensaas-sh/blog/src/content/docs/0.23/guides/vibe-coding.mdx`)


title: AI-assisted Coding / Vibe Coding with Open SaaS
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/vibe-coding

import { Image } from 'astro:assets';
import vibeBoi from '@assets/ai/vibe-boi.png';

If you're looking to use AI coding tools to help build your SaaS app, this guide is for you.

:::tip[New to building SaaS? Learn while you build with Open Vibe]
Open Vibe is our free, open-source curriculum that turns Claude Code (or your agent of choice) into a SaaS-building tutor. It pairs interactive diagrams with hands-on lessons — and Phase 2 builds a real SaaS on top of this very Open SaaS template.
:::

Coding with AI, Open SaaS, & Wasp

Wasp is the perfect match for AI-assisted coding due to its central config file which gives LLMs immediate context about the entire full-stack app, as well as its ability to manage boilerplate code so AI doesn't have to.

To make it even easier, here are some resources to help you use Wasp & Open SaaS with AI as effectively as possible.

Agent Skills and Rules

We've put some agent skills and rules into the template to help you get started using Open SaaS with your favorite AI-assisted coding tools (Claude Code, Cursor, VS Code/Copilot, Codex, Gemini CLI, OpenCode, etc.).

Skills are found in the app/.agents (and app/.claude) directory of the Open SaaS template, and follow the Agent Skills standard.
See your AI-assisted coding tool's documentation for more details on how to add Agent Skills if they don't load automatically.

Rules, found within the AGENTS.md (and CLAUDE.md), is another open standard used by many popular AI-assisted coding tools to give instructions to the agents when coding.

:::tip[These are just a starting point!]
Use these skills and rules to get your project up and running.

Run the add-wasp-skills skill to add Wasp framework-specific skills to your project for even more power.
:::

LLM-Friendly Documentation

Note: the AGENTS.md has specific instructions on how to automatically fetch and use the LLM-friendly documentation but you can also paste the following links in chat sessions if needed.

More AI-assisted Coding Learning Resources

Here's a list of articles and tutorials we've made:


Opensaas Sh Blog Src Content Docs 0.23 Start Guided Tour (`opensaas-sh/blog/src/content/docs/0.23/start/guided-tour.md`)


title: Guided Tour
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/start/guided-tour

Awesome, you now have your very own SaaS app up and running! But, first, here are some important things you need to know about your app in its current state:

  1. When signing up with a new user, you will get a message to check your email for a verification link. But, in development, these emails are simply written to your terminal. So, to continue with the registration process, check your server logs after sign up!
sh title="server logs"
[ Server ] ╔═══════════════════════╗
[ Server ] ║ Dummy email sender ✉️  ║
[ Server ] ╚═══════════════════════╝
[ Server ] From:    Open SaaS App <[email protected]>
[ Server ] To:      [email protected]
[ Server ] Subject: Verify your email
[ Server ] ═════════ Text ═════════
[ Server ] Click the link below to verify your email: http://localhost:3000/email-verification?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InZpbm55QHdhc3Auc2giLCJleHAiOjE3MTg5NjUyNTB9.PkRGrmuDPuYFXkTprf7QpAye0e_O9a70xbER6LfxGJw
[ Server ] ═════════ HTML ═════════
[ Server ] <p>Click the link below to verify your email</p>
[ Server ] <a href="http://localhost:3000/email-verification?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InZpbm55QHdhc3Auc2giLCJleHAiOjE3MTg5NjUyNTB9.PkRGrmuDPuYFXkTprf7QpAye0e_O9a70xbER6LfxGJw">Verify email</a> 
[ Server ] ════════════════════════
  1. Your app is still missing some key configurations (e.g. API keys for Payment Processors, OpenAI, AWS S3, Auth, Analytics). These services won't work at the moment, but don't fear, because we've provided detailed guides in these docs to help you set up all the services in this template.
  2. If you want to get a feel for what your SaaS could look like when finished, check out OpenSaaS.sh in your browser. It was built using this template! So make sure to log in, play around with the demo app, make a test payment, and check out the admin dashboard.

In the sections below, we will take a short guide through the codebase and the app's main features. Then at the end of this tour, we also prepared a checklist of likely changes you will want to make to the app to make it your own.

We're looking forward to seeing what you build!

Getting acquainted with the codebase

Now that you've gotten a first look at the app, let's dive into the codebase.

At the root of our project, you will see three folders:

sh
.
├── app
├── blog
└── e2e-tests

app contains the Wasp project files, which is your full-stack (React + NodeJS + Prisma) app along with a Wasp config file, main.wasp, which will be explained in more detail below.

blog contains the Astro Starlight theme for the blog and documentation section.

e2e-tests contains the end-to-end tests using Playwright, which you can run to test your app's functionality.

App File Structure

We've structured this full-stack app template vertically (by feature). That means that most directories within app/src contain both the React client code and NodeJS server code necessary for implementing its logic.

Let's check out what's in the app folder in more detail:

sh
.
├── main.wasp              # Wasp Config file. You define your app structure here.
├── .wasp/                 # Output dir for Wasp. DON'T MODIFY THESE FILES!
├── public/                # Public assets dir, e.g. `www.yourdomain.com/public-banner.webp`.
├── src/                   # Your code goes here.
│   ├── admin/             # Admin dashboard related pages and components.
│   ├── analytics/         # Logic and background jobs for processing analytics.
│   ├── auth/              # All auth-related pages/components and logic.
│   ├── client/            # Shared components, hooks, landing page, and other client code (React).
│   ├── demo-ai-app/       # Logic for the example AI-powered demo app.
│   ├── file-upload/       # Logic for uploading files to S3.
│   ├── landing-page/      # Landing page related code
│   ├── payment/           # Logic for handling payments and webhooks.
│   ├── server/            # Scripts, shared server utils, and other server-specific code (NodeJS).
│   ├── shared/            # Shared constants and util functions.
│   └── user/              # Logic related to users and their accounts.
├── .env.server            # Dev environment variables for your server code.
├── .env.client            # Dev environment variables for your client code.
├── .prettierrc            # Prettier configuration.
├── package.json
├── package-lock.json
└── .wasproot

The Wasp Config file

This template at its core is a Wasp project, where Wasp is a full-stack web app framework that let's you write your app in React, NodeJS, and Prisma and will manage the "boilerplatey" work for you, allowing you to just take care of the fun stuff!

Wasp's secret sauce is its use of a config file (main.wasp) and compiler which takes your code and outputs the client app, server app and deployment code for you.

In this template, we've already defined a number of things in the main.wasp config file, including:

By defining these things in the config file, Wasp continuously handles the boilerplate necessary with putting all these features together. You just need to focus on the business logic of your app.

Wasp abstracts away some things that you would normally be used to doing during development, so don't be surprised if you don't see some of the things you're used to seeing.

:::note
It's possible to learn Wasp's feature set simply through using this template, but if you find yourself unsure how to implement a Wasp-specific feature and/or just want to learn more, a great starting point is the intro tutorial in the Wasp docs which takes ~20 minutes.
:::

Client

The src/client folder contains any additional client-side code that doesn't belong to a feature:

sh
.
└── client
    ├── components         # Your shared React components.
    ├── hooks              # Your shared React hooks.
    ├── static             # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'.
    ├── App.tsx            # Main app component to wrap all child components. Useful for global state, navbars, etc.
    ├── Main.css           # Main CSS file. Includes the Tailwind CSS into the project.
    └── utils.ts

Server

The src/server folder contains any additional server-side code that does not belong to a specific feature:

sh
└── server
    ├── scripts            # Scripts to run via Wasp, e.g. database seeding.
    └── validation.ts

Main Features

Auth

This template comes with a fully functional auth flow out of the box. It takes advantages of Wasp's built-in Auth features, which do the dirty work of rolling your own full-stack auth for you!

js title="main.wasp"
auth: {
    userEntity: User,
    methods: {
      email: { 
        //...
      },
      google: {},
      github: {},
      discord: {}
    },
    onAuthFailedRedirectTo: "/",
  },

By defining the auth structure in your main.wasp file, Wasp manages all the necessary code for you, including:

  • Email verified login with password reset.
  • Social login with Google and/or GitHub.
  • Auth-related database entities for user credentials, sessions, and social logins.
  • Custom-generated AuthUI components for login, signup, and password reset.
  • Auth hooks for fetching user data.

We've set the template up with Wasp's email, google, discord and gitHub methods, which are all battle-tested and suitable for production.

You can get started developing your app with the email method right away!

:::caution[Dummy Email Provider]
Note that the email method relies on an emailSender (configured at app.emailSender in the main.wasp file), a service which sends emails to verify users and reset passwords.

For development purposes, Wasp provides a Dummy email sender which Open SaaS comes with as the default. This provider does not actually send any confirmation emails to the specified email address, but instead logs all email verification links/tokens to the console! You can then follow these links to verify the user and continue with the sign-up process.

tsx title="main.wasp"
emailSender: {
    provider: Dummy, // Logs all email verification links/tokens to the server's console.
    defaultFrom: {
      name: "Open SaaS App",
      email: "[email protected]" 
    },
  },

:::

We will explain more about these auth methods, and how to properly integrate them into your app, in the Authentication Guide.

Subscription Payments with Stripe, Lemon Squeezy or Polar

No SaaS is complete without payments, specifically subscription payments. That's why this template comes with a fully functional Stripe, Lemon Squeezy and Polar integration.

Let's take a quick look at how payments are handled in this template.

  1. A user clicks the BUY button and a checkout session is created on the server.
  2. The user is redirected to the payment processor's checkout page where they enter their payment info.
  3. The checkout session is completed and the user is redirected back to the app.
  4. Payment processor sends a webhook event to the app's server with the payment info.
  5. The app server's webhook handler handles the event and updates the user's subscription status.

The payment processor you choose (Stripe, Lemon Squeezy or Polar) and its related functions can be found at src/payment/paymentProcessor.ts. The PaymentProcessor object holds the logic for creating checkout sessions, webhooks, etc.

The logic for creating the Checkout session is defined in the src/payment/operation.ts file. Actions are a type of Wasp Operation, specifically your server-side functions that are used to write or update data to the database. Once they're defined in the main.wasp file, you can easily call them on the client-side:

a) define the action in the main.wasp file

js title="main.wasp"
action generateCheckoutSession {
  fn: import { generateCheckoutSession } from "@src/payment/operations",
  entities: [User]
}

b) implement the action in the src/payment/operations file

js title="src/server/actions.ts"
export const generateCheckoutSession: GenerateCheckoutSession<
  GenerateCheckoutSessionInput,
  CheckoutSession
> = async (rawPaymentPlanId, context) => {
  // ...
};

c) call the action on the client-side

js title="src/client/app/subscriptionpage.tsx"
import { generateCheckoutSession } from "wasp/client/operations";

async function handleBuyNowClick(paymentPlanId: PaymentPlanId) {
  // ...
  const checkoutSession = await generateCheckoutSession(paymentPlanId);
  // ...
};

The webhook handler is defined in the src/payment/webhook.ts file. Unlike Actions and Queries in Wasp which are only to be used internally, we define the webhook handler in the main.wasp file as an API endpoint in order to expose it externally to the payment processor.

js title="main.wasp"
api paymentsWebhook {
  fn: import { paymentsWebhook } from "@src/payment/webhook",
  httpRoute: (POST, "/payments-webhook"),
  middlewareConfigFn: import { paymentsMiddlewareConfigFn } from "@src/payments/webhook,
  entities: [User],
}

Within the webhook handler, we look for specific events that the payment processor sends us to let us know which payment was completed and for which user. Then we update the user's subscription status in the database.

To learn more about configuring the app to handle your products and payments, check out the Payment Integrations guide.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Analytics and Admin Dashboard

Keeping an eye on your metrics is crucial for any SaaS. That's why we've built an administrator's dashboard where you can view your app's stats, user data, and revenue all in one place.

To do that, we've leveraged Wasp's Jobs feature to run a cron job that calculates your daily stats. The app stats, such as page views and sources, can be pulled from either Plausible or Google Analytics. All you have to do is create a project with the analytics provider of your choice and import the respective pre-built helper functions!

js title="main.wasp"
job dailyStatsJob {
  executor: PgBoss,
  perform: {
    fn: import { calculateDailyStats } from "@src/analytics/stats"
  },
  schedule: {
    cron: "0 * * * *" // Every hour. useful in production.
    // cron: "* * * * *" // Every minute. useful for debugging.
  },
  entities: [User, DailyStats, Logs, PageViewSource]
}

For more info on integrating Plausible or Google Analytics, check out the Analytics guide.

SEO & Discoverability

Open SaaS ships SEO-friendly out of the box. Three pieces work together so your app shows up in search results and AI answers:

  1. Meta tags in main.wasp's head (title, description, Open Graph, Twitter cards) get injected into your landing page HTML.
  2. JSON-LD structured data (Schema Markup) in src/landing-page/components/SchemaMarkup.tsx tells search engines and LLMs what your app is, who makes it, and what it offers — feeding rich snippets and AI-generated answers.
  3. Prerendering turns your React marketing pages (e.g. landing page, pricing, FAQ, etc.) into static HTML at build time, so crawlers see your content (including the schema markup) without needing to execute JavaScript.

Customize the meta tags in main.wasp and the schema object in SchemaMarkup.tsx to match your product. See the SEO guide for details.

App Customization Walkthrough

General Considerations

When you first start your Open SaaS app straight from the template, it will run, but many of the services won't work because they lack your own API keys. Here are list of services that need your API keys to work properly:

  • Auth Methods (Google, Discord, GitHub).
  • Payment processor (Stripe, Lemon Squeezy or Polar).
  • OpenAI (ChatGPT API).
  • Email Sending (Sendgrid) -- you must set this up if you're using the email auth method.
  • Analytics (Plausible or Google Analytics).
  • File Uploading (AWS S3).

Now would be a good time to decide which features you do and do not need for your app, and remove the ones from the codebase that you don't need.

For the features you will use, the next section of the documentation, Guides, will walk you through how to set each one up!

:::note[Open SaaS is built on Wasp]
Remember, this template is built on the Wasp framework. If, at any time, these docs fail to provide enough information about a certain built-in feature, make sure to check out the Wasp docs!
:::

But before you start setting up the main features, let's walk through the customizations you will likely want to make to the template to make it your own.

Customizations Checklist

`main.wasp` Config File

  • [ ] Change the app name and title:

    ts title="main.wasp" {1, 5}
    app YourAppName { 
        wasp: {
          version: "^0.23.0"
        },
        title: "Your App Name",

    :::caution[Restart Your App]
    Development databases are connceted to your app's name. Upon changing the app name, new, empty development database will be assigned to your app. This means you'll need to rerun wasp db start, wasp db migrate-dev and wasp start.
    :::

  • [ ] Update meta tags in app.head (even if you don't have a custom domain yet, put one you would like to have, as this won't affect development).

  • [ ] Update app.emailSender.defaultFrom.name with the name of your app/company/whatever you want your users to see in their inbox, if you're using the emailSender feature and/or email Auth method.

  • [ ] Remove any features you might not use or need:

    • [ ] Auth methods - app.auth.methods:
      • [ ] If you're not using email Auth method, remove the routes/pages: RequestPasswordReset, PasswordReset, and EmailVerification.
    • [ ] Email Sending - app.emailSender.
    • [ ] Plausible analytics - app.head.
    • [ ] File Uploading - entity File, route FileUploadRoute, and all operations tied to file uploading.
  • [ ] Rename Entites and their properties, Routes/Pages, & Operations, if you wish.

Customizing the Look / Style of the App

  • [ ] Update your favicon at public/favicon.ico.
  • [ ] Update the banner image used when posting links to your site at public/public-banner.webp.
    • [ ] Update the URL for this banner at og:image and twitter:image in app.head of the main.wasp file.
  • [ ] Update the JSON-LD structured data in src/landing-page/components/SchemaMarkup.tsx with your app's name, description, URL, and other metadata. This helps search engines and LLMs understand your app for rich results and AI answers.
  • [ ] Make changes to your landing page, LandingPage.tsx.
    • [ ] Customize the navBar, features, testimonials, and faqs in the contentSections.ts file.
    • [ ] Change/rename the logo.webp and main hero banner (open-saas-banner.webp) in the static folder.
  • [ ] If you want to make changes to the global styles of the app, you can do so in Main.css. Be aware that changing the current global styles affects most of the app!

Customizing the Analytics & Admin Dashboard

  • [ ] If you're using Plausible, update the app.head with your Plausible domain.
  • [ ] Update the calculateDailyStats job function in src/analytics/stats.ts to pull the stats from the analytics provider you've chosen (Plausible or Google Analytics).
  • [ ] Change the cron schedule in the calculateDailyStats in the main.wasp file to match how often you want your stats to be calculated.
  • [ ] Update the AdminDashboard components to display the stats you do/don't want to see.

`.env.server` and `.env.client` Files

  • [ ] After you've followed the Guides in the next section, you'll need to update the .env.server and .env.client files with your API keys and other environment variables for the services you've decided to use.
  • [ ] Delete any redundant environment variables that you're not using, from the .env.* files as well as the .env.*.example files.

Other Customizations

  • [ ] Make a new GitHub repository for your app.
  • [ ] Deploy your app to a hosting provider.
  • [ ] Buy a domain name for your app and get it set up with your hosting provider.
  • [ ] Read the e2e-tests README and get your end-to-end tests set up.
    • [ ] Change the tests to suit the changes you've made to your app.
  • [ ] Get the CI pipeline set up for your app (you can get started by using the Open SaaS development CI example here).

What's next?

In the following Guides sections, we'll walk you through getting those API keys and setting up the finer points of features such as Payments & Webhooks, Auth, Email Sending, Analytics, and more.


Opensaas Sh Blog Src Content Docs Guides Payment Integrations Lemon Squeezy (`opensaas-sh/blog/src/content/docs/guides/payment-integrations/lemon-squeezy.mdx`)


title: Lemon Squeezy
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import addProduct from '@assets/lemon-squeezy/add-product.png';
import addVariant from '@assets/lemon-squeezy/add-variant.png';
import storeId from '@assets/lemon-squeezy/store-id.png';
import subscriptionVariantIds from '@assets/lemon-squeezy/subscription-variant-ids.png';
import variantId from '@assets/lemon-squeezy/variant-id.png';
import ngrok from '@assets/ngrok.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts and src/payment/paymentProcessorClient.ts, as described in the important first steps.

Next, you'll need to create a Lemon Squeezy account in test mode. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Get your test Lemon Squeezy API Keys

Once you've created your account, you'll need to get your test API keys. You can do that by navigating to https://app.lemonsqueezy.com/settings/api and creating a new API key:

  1. Click on the + button.
  2. Give your API key a name.
  3. Copy and paste it in your .env.server file under LEMONSQUEEZY_API_KEY.

Get your Lemon Squeezy Store ID

To get your store ID, go to the Lemon Squeezy Dashboard and copy the Store ID from the top right corner.

Copy and paste this number in your .env.server file under LEMONSQUEEZY_STORE_ID.

Creating Products

To create a product, go to the test products url https://app.lemonsqueezy.com/products:

  1. Click on the + New Product button and fill in the relevant information for your product.

  2. Fill in the general information.

  3. For pricing, select the type of product you'd like to create, e.g. Subscription for a recurring monthly payment product or Single Payment for credits-based product.

  4. Make sure you select Software as a service (SaaS) as the Tax category type.

  5. If you want to add different price tiers for Subscription products, click on add variant under the variants tab. Here you can input the name of the variant (e.g. "Hobby", "Pro"), and that variant's price.

  6. For a product with no variants, on the product page, click the ... menu button and select Copy variant ID

  7. For a product with variants, on the product page, click on the product, go to the variants tab and select Copy ID for each variant.

  8. Paste these IDs in the .env.server file:

    • We've set you up with two example subscription product environment variables, PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID and PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID.
    • As well as a one-time payment product/credits-based environment variable, PAYMENTS_CREDITS_10_PLAN_ID.

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Using Lemon Squeezy Webhook in Local Development

Exposing your Webhook Endpoint to the Internet

Lemon Squeezy notifies your Wasp app about customer and payment events through a webhook. However, to make it available to Lemon Squeezy during development, you need to expose your locally running Wasp server (started with wasp start) to the internet.

You can do that by running ngrok on port 3001 (Wasp server runs on port 3001 by default). ngrok will then generate a public URL that we can provide to Lemon Squeezy:

  1. First, make sure you have installed ngrok.

  2. Once ngrok is installed and your Wasp app is running, run:

    sh
    ngrok http 3001
  3. ngrok will display a forwarding address. Copy this address and append /payments-webhook to it. It should look something like this:

    sh title="callback url"
    https://89e5-2003-c7-153c-72a5-f837.ngrok-free.app/payments-webhook

Creating your Lemon Squeezy Webhook

Next, go to your Lemon Squeezy Webhooks Dashboard:

  1. Click the + button.
  2. Add the newly created webhook forwarding url to the Callback URL section.
  3. Give your webhook a signing secret (a long, random string).
  4. Copy and paste this same signing secret into your .env.server file under LEMONSQUEEZY_WEBHOOK_SECRET.
  5. Make sure to select at least the following updates to be sent:
    • order_created
    • subscription_created
    • subscription_updated
    • subscription_cancelled
  6. Click save.

You're now ready to start consuming Lemon Squeezy webhook events in local development.


Opensaas Sh Blog Src Content Docs Guides Payment Integrations Paddle (`opensaas-sh/blog/src/content/docs/guides/payment-integrations/paddle.mdx`)


title: Paddle
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import ngrok from '@assets/ngrok.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts and src/payment/paymentProcessorClient.ts, as described in the important first steps.

Next, you'll need to create a Paddle account in the sandbox mode. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Sandbox and Production Mode

Paddle features two separate environments:

They are fully isolated from each other, which means that you need to create separate accounts for each of them. They also feature independent products, prices, API keys, etc.

For local development and testing, you'll want to use Paddle's sandbox mode.
To enable sandbox mode, make sure that PADDLE_SANDBOX_MODE is set to true in your .env.server file, and REACT_APP_PADDLE_SANDBOX_MODE is set to true in your .env.client file:

ini title=".env.server"
PADDLE_SANDBOX_MODE=true
ini title=".env.client"
REACT_APP_PADDLE_SANDBOX_MODE=true

When you're ready to deploy your app, we'll remind you to set these to false.

Paddle API Key

Once you've created your account, you'll need to get your API key.
You can do that by:

  1. Navigating to My Account > Settings.
  2. Opening the Developer Tools > Authentication page.
  3. Click on the New API key button to create a new key.
  4. Give your key a name (e.g., "Open SaaS Development").
  5. Select the following permissions:
    • Customer portal sessions: write
    • Customers: read and write
    • Metrics: read
    • Reports: read
    • Transactions: read and write
  6. Copy the generated key and add it to your .env.server file:
    ini title=".env.server"
    PADDLE_API_KEY=pdl_sdbx_apikey_...

Paddle Client-side Token

Paddle's checkout runs in the browser with Paddle.js, which needs a client-side token.
You can get one by:

  1. Navigating to My Account > Settings.
  2. Opening the Developer Tools > Authentication page.
  3. Selecting the Client-side tokens tab.
  4. Click on the New client-side token button to create a new token.
  5. Give your token a name (e.g., "Open SaaS Development Client").
  6. Copy the generated token and add it to your .env.client file:
    ini title=".env.client"
    REACT_APP_PADDLE_CLIENT_TOKEN=test_...

Paddle requires a default payment link before it can open a checkout. Paddle doesn't accept localhost URLs, but in sandbox mode the link isn't actually visited, so you can use https://example.com.

To set it:

  1. Navigate to Checkout > Checkout configuration.
  2. Set the Default payment link to https://example.com.
  3. Click Save.

In production, you'll need to set it to your deployed app's URL. We'll remind you to do that when you deploy your app.

Creating Products

To create Paddle products, in your Paddle dashboard:

  1. Navigate to the Catalog > Products page.
  2. Click on the + New product button to create a new product.
  3. Fill in the product details and click Save.
  4. Click on your newly created product in the Catalog > Products list to open its product page.
  5. On the product page, click on the New Price + button.
    • Fill in the Internal description (e.g. "Monthly"). It's required, but isn't shown to your customers.
    • Set the Base price (e.g. 9.99).
    • For subscription products, select a Recurring billing period (e.g. monthly).
    • For one-time payment products, select One-time.
    • Set the quantity to a minimum of 1 and a maximum of 1. Open SaaS grants one plan (or one pack of credits) per purchase, so customers shouldn't be able to buy more than one at a time.
  6. Click Save.
  7. Copy the Price ID (it starts with pri_). Note that Open SaaS uses the price ID, not the product ID.
  8. Add the Price ID to your .env.server file.
    Open SaaS by default expects three products, two subscriptions and one one-time purchase plan:
    ini title=".env.server"
    PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID=<your-hobby-price-id>
    PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID=<your-pro-price-id>
    PAYMENTS_CREDITS_10_PLAN_ID=<your-credits-price-id>

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Using Paddle Webhook in Local Development

Exposing your Webhook Endpoint to the Internet

Paddle notifies your Wasp app about customer and payment events through a webhook. However, to make it available to Paddle during development, you need to expose your locally running Wasp server (started with wasp start) to the internet.

You can do that by running ngrok on port 3001 (Wasp server runs on port 3001 by default). ngrok will then generate a public URL that we can provide to Paddle:

  1. First, make sure you have installed ngrok.

  2. Once ngrok is installed and your Wasp app is running, run:

    sh
    ngrok http 3001
  3. ngrok will display a forwarding address. Copy this address and append /payments-webhook to it. It should look something like this:

    sh title="callback url"
    https://89e5-2003-c7-153c-72a5-f837.ngrok-free.app/payments-webhook

Creating your Paddle Webhook

Next, configure the webhook in your Paddle dashboard:

  1. Navigate to the Events > Notifications page.
  2. Click the + New destination button.
  3. Fill in the Description (e.g. "Open SaaS Development").
  4. Set the Notification Type to Webhook.
  5. In the URL field, paste your ngrok forwarding address with /payments-webhook appended (for example: https://abc123.ngrok-free.app/payments-webhook).
  6. Set the Usage type to Both.
  7. Select the following events to listen for:
    • transaction.completed
    • subscription.updated
    • subscription.canceled
  8. Click Save.
  9. Click on your newly created destination in the Events > Notifications list to open it.
  10. Click copy on the Secret key and add it to your .env.server file:
    ini title=".env.server"
    PADDLE_WEBHOOK_SECRET=pdl_ntfset_...

Testing Payments in Local Development

Before testing payments, make sure that you created and set all the required env variables in .env.server (PADDLE_API_KEY, <NAME>_PLAN_ID and PADDLE_WEBHOOK_SECRET) and .env.client (REACT_APP_PADDLE_CLIENT_TOKEN), that you set a default payment link, and that your ngrok tunnel is running.

You can then test the payment flow:

  1. Click a Buy button for any product on the homepage.
  2. Paddle's checkout should open as an overlay on the page.
  3. Fill in the checkout form with test payment information.
  4. Complete the payment.
  5. You should be redirected to the checkout success page.

To verify everything executed correctly you can:

  • Check Paddle webhook event logs:
    1. Navigate to the Events > Notifications page.
    2. Open your previously created destination.
    3. Confirm all of the events have been delivered successfully.
  • Inspect your database's User table:
    1. Run Wasp DB studio:
      sh
      wasp db studio
    2. Navigate to localhost:5555 and check the User table.
    3. Confirm the subscriptionStatus is active for the user who made the purchase.

Opensaas Sh Blog Src Content Docs Guides Payment Integrations Polar (`opensaas-sh/blog/src/content/docs/guides/payment-integrations/polar.mdx`)


title: Polar
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import ngrok from '@assets/ngrok.png';
import polarUserTable from '@assets/polar/user-table.png';
import polarWebhookLogs from '@assets/polar/webhook-log.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts and src/payment/paymentProcessorClient.ts, as described in the important first steps.

Next, you'll need to create a Polar account in the sandbox mode. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Sandbox and Production Mode

Polar features two separate environments:

They are fully isolated from each other, which means that you need to create seperate organizations for each of them. They also feature independent products, sales, access tokens, etc.

For local development and testing, you'll want to use Polar's sandbox mode.
To enable sandbox mode, make sure that POLAR_SANDBOX_MODE is set to true in your .env.server file:

ini title=".env.server"
POLAR_SANDBOX_MODE=true

When you're ready to deploy your app, we'll remind you to set this to false.

Polar Organization Access Token

Once you've created your account, you'll need to get your organization access token.
You can do that by:

  1. Navigating to the Developers section under the Settings > General page.
  2. Click on the New Token button to create a new token.
  3. Give your token a name (e.g., "Open SaaS Development").
  4. Select an expiration date. No expiration is fine for sandbox mode, but discouraged for production mode.
  5. Select the following scopes:
    • checkouts:write
    • customer_sessions:write
    • customers:read
    • customers:write
    • orders:read
  6. Copy the generated token and add it to your .env.server file:
    ini title=".env.server"
    POLAR_ORGANIZATION_ACCESS_TOKEN=polar_oat_...

Creating Products

To create Polar products, in your Polar dashboard:

  1. Navigate to Prodcuts > Catalogue page.
  2. Click on the + New Product button to create a new product.
  3. Fill in the product details.
    • For subscription products, select Recurring subscription pricing.
    • For one-time payment products, select One-time purchase pricing.
  4. Click Create Product.
  5. Tap the ⠇ icon next to your product and select Copy Product ID.
  6. Add the Product ID to your .env.server file.
    Open SaaS by default expects three products, two subscriptions and one one-time purchase plan:
    ini title=".env.server"
    PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID=<your-hobby-product-id>
    PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID=<your-pro-product-id>
    PAYMENTS_CREDITS_10_PLAN_ID=<your-credits-product-id>

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Using Polar Webhook in Local Development

Exposing your Webhook Endpoint to the Internet

Polar notifies your Wasp app about customer and payment events through a webhook. However, to make it available to Polar during development, you need to expose your locally running Wasp server (started with wasp start) to the internet.

You can do that by running ngrok on port 3001 (Wasp server runs on port 3001 by default). ngrok will then generate a public URL that we can provide to Polar:

  1. First, make sure you have installed ngrok.

  2. Once ngrok is installed and your Wasp app is running, run:

    sh
    ngrok http 3001
  3. ngrok will display a forwarding address. Copy this address and append /payments-webhook to it. It should look something like this:

    sh title="callback url"
    https://89e5-2003-c7-153c-72a5-f837.ngrok-free.app/payments-webhook

Creating your Polar Webhook

Next, configure the webhook in your Polar dashboard:

  1. Navigate to Settings > Webhooks page.
  2. Click Add Endpoint button.
  3. In the URL field, paste your ngrok forwarding address with /payments-webhook appended (for example: https://abc123.ngrok-free.app/payments-webhook).
  4. Set the Format to "Raw".
  5. Select the following events to listen for:
    • order.paid
    • subscription.updated
  6. Click Save.
  7. Copy the generated webhook secret and add it to your .env.server file:
    ini title=".env.server"
    POLAR_WEBHOOK_SECRET=polar_whs_...

Testing Payments in Local Development

Before testing payments, make sure that you created and set all the required env variables in .env.server (POLAR_ORGANIZATION_ACCESS_TOKEN, <NAME>_PLAN_ID and POLAR_WEBHOOK_SECRET ) and that your ngrok tunnel is running.

You can then test the payment flow:

  1. Click a Buy button for any product on the homepage.
  2. You should be redirected to Polar's checkout page.
  3. Fill in the checkout form with test payment information.
  4. Complete the payment.
  5. You should be redirected back to the checkout success page.

To verify everything executed correctly you can:

  • Check Polar webhook event logs:

    1. Navigate to Settings > Webhooks page
    2. Click the Details button of your previously created webhook.
    3. Confirm all of the events have been successful:
  • Inspect your database's User table:

    1. Run Wasp DB studio:
      sh
      wasp db studio
    2. Navigate to localhost:5555 and check the User table.
    3. Confirm the subscriptionStatus is active for the user who made the purchase.

Opensaas Sh Blog Src Content Docs Guides Payment Integrations Stripe (`opensaas-sh/blog/src/content/docs/guides/payment-integrations/stripe.mdx`)


title: Stripe
banner:
content: |
Have an Open SaaS app in production? We'll send you some swag! 👕


import ngrok from '@assets/ngrok.png';
import testApiKeys from '@assets/stripe/api-keys.png';
import dbStudio from '@assets/stripe/db-studio.png';
import priceIds from '@assets/stripe/price-ids.png';
import switchPlans from '@assets/stripe/switch-plans.png';
import testProduct from '@assets/stripe/test-product.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts and src/payment/paymentProcessorClient.ts, as described in the important first steps.

Next, you'll need to create a Stripe account. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Get your test Stripe API Keys

Once you've created your account, you'll need to get your test API keys. You can do that by navigating to https://dashboard.stripe.com/test/apikeys or by going to the Stripe Dashboard and clicking on the Developers:

  1. Click on the Reveal test key token button and copy the Secret key.
  2. Paste it in your .env.server file under STRIPE_API_KEY.

Create Test Products

To create a test product, go to the test products url https://dashboard.stripe.com/test/products, or after navigating to your dashboard, click the test mode toggle:

  1. Click on the Add a product button and fill in the relevant information for your product.

    • Select Software as a service (SaaS) as the product type.
    • For Subscription products, select Recurring as the billing type.
    • For One-time payment products, select One-time as the billing type.
  2. If you want to add different price tiers for the same product (e.g. monthly and yearly), click the Add another price button at the buttom.

  3. After you save the product, you'll be directed to the product page.

  4. Copy the price IDs and paste them in the .env.server file.

    • We've set you up with two example subscription product environment variables, PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID and PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID.
    • As well as a one-time payment product/credits-based environment variable, PAYMENTS_CREDITS_10_PLAN_ID.

If you intend to let your users switch between two subscription plans, e.g. upgrade from hobby to pro, you'll need to create two separate products with their own price IDs. The ability for users to swich plans can then be configured later in the Customer Portal.

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Create a Test Customer

You can create a test customer in the Stripe Dashboard.

Click on the Add a customer button and fill in the relevant information for your test customer.

Alternatively, Open SasS automatically creates a test customer the first time a user starts a checkout session.
This customer is linked to the email address associated with the user in your app.

Set up the Customer Portal

You can set up your customer portal in your Stripe Dashboard.

By default, OpenSaas generates a unique customer portal link for each user on the back end.
If you'd rather provide a permanent link to the customer portal, activate it and copy the Portal link.

If you'd like to give users the ability to switch between different plans, e.g., upgrade from a "Hobby" to a "Pro" subscription, go down to the Subscriptions dropdown and select customers can switch plans.

Then select the products you'd like them to be able to switch between.

Now, after your users have paid, they can click on Manage Subscription in the client and will be taken to the customer portal where they can update their current plan.

Stripe CLI

To install the Stripe CLI follow the instructions here.

Make sure to login after you install the Stripe CLI:

sh
stripe login

:::caution[Errors running the Stripe CLI]
If you're seeing errors, consider appending sudo to the stripe commands.
See this GitHuh issue for more details.
:::

Testing Webhooks via the Stripe CLI

Start the Stripe CLI webhook forwarding on port 3001 where your Node server is running (if port 3001 was taken, wasp start picks another one, so use the server port it prints):

sh
stripe listen --events invoice.paid,customer.subscription.updated,customer.subscription.deleted --forward-to localhost:3001/payments-webhook

Since Stripe CLI 1.51, stripe listen requires you to list the events to forward. The command above lists the events the template's webhook handles. If you handle more events in src/payment/stripe/webhook.ts, add them to the command too.

Remember to copy and paste the outputted webhook signing secret (whsec_...) into your .env.server file under STRIPE_WEBHOOK_SECRET.

In another terminal window, trigger a test event:

sh
stripe trigger payment_intent.succeeded

The results of the event firing will be visible in the initial terminal window. You should see messages like this:

sh
...
2023-11-21 09:31:09   --> invoice.paid [evt_1OEpMPILOQf67J5TjrUgRpk4]
2023-11-21 09:31:09  <--  [204] POST http://localhost:3001/payments-webhook [evt_1OEpMPILOQf67J5TjrUgRpk4]

For more info on testing webhooks, check out https://stripe.com/docs/webhooks#test-webhook.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Testing Checkout and Payments via the Client

Make sure the Stripe CLI is running by following the steps above.
You can then test the payment flow via the client by doing the following:

  1. Click on a Buy button on the for any of the products on the homepage. You should be redirected to the checkout page.
  2. Fill in the form with the following test credit card number 4242 4242 4242 4242 and any future date for the expiration date and any 3 digits for the CVC.
  3. Click on the "Pay" button. You should be redirected to the success page.
  4. Check your terminal window for status messages and logs.
  5. You can also check your Database via the DB Studio to see if the user entity has been updated by running:
    sh
    wasp db studio
  6. Navigate to localhost:5555 and click on the users table. You should see the subscriptionStatus is active for the user that just made the purchase.

:::note
If you want to learn more about how a user's payment status, subscription status, and subscription tier affect a user's priveledges within the app, check out the User Overview reference.
:::


Opensaas Sh Blog Src Content Docs 0.23 Guides Payment Integrations Lemon Squeezy (`opensaas-sh/blog/src/content/docs/0.23/guides/payment-integrations/lemon-squeezy.mdx`)


title: Lemon Squeezy
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/payment-integrations/lemon-squeezy

import addProduct from '@assets/lemon-squeezy/add-product.png';
import addVariant from '@assets/lemon-squeezy/add-variant.png';
import storeId from '@assets/lemon-squeezy/store-id.png';
import subscriptionVariantIds from '@assets/lemon-squeezy/subscription-variant-ids.png';
import variantId from '@assets/lemon-squeezy/variant-id.png';
import ngrok from '@assets/ngrok.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts, as described in the important first steps.

Next, you'll need to create a Lemon Squeezy account in test mode. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Get your test Lemon Squeezy API Keys

Once you've created your account, you'll need to get your test API keys. You can do that by navigating to https://app.lemonsqueezy.com/settings/api and creating a new API key:

  1. Click on the + button.
  2. Give your API key a name.
  3. Copy and paste it in your .env.server file under LEMONSQUEEZY_API_KEY.

Get your Lemon Squeezy Store ID

To get your store ID, go to the Lemon Squeezy Dashboard and copy the Store ID from the top right corner.

Copy and paste this number in your .env.server file under LEMONSQUEEZY_STORE_ID.

Creating Products

To create a product, go to the test products url https://app.lemonsqueezy.com/products:

  1. Click on the + New Product button and fill in the relevant information for your product.

  2. Fill in the general information.

  3. For pricing, select the type of product you'd like to create, e.g. Subscription for a recurring monthly payment product or Single Payment for credits-based product.

  4. Make sure you select Software as a service (SaaS) as the Tax category type.

  5. If you want to add different price tiers for Subscription products, click on add variant under the variants tab. Here you can input the name of the variant (e.g. "Hobby", "Pro"), and that variant's price.

  6. For a product with no variants, on the product page, click the ... menu button and select Copy variant ID

  7. For a product with variants, on the product page, click on the product, go to the variants tab and select Copy ID for each variant.

  8. Paste these IDs in the .env.server file:

    • We've set you up with two example subscription product environment variables, PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID and PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID.
    • As well as a one-time payment product/credits-based environment variable, PAYMENTS_CREDITS_10_PLAN_ID.

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Using Lemon Squeezy Webhook in Local Development

Exposing your Webhook Endpoint to the Internet

Lemon Squeezy notifies your Wasp app about customer and payment events through a webhook. However, to make it available to Lemon Squeezy during development, you need to expose your locally running Wasp server (started with wasp start) to the internet.

You can do that by running ngrok on port 3001 (Wasp server runs on port 3001 by default). ngrok will then generate a public URL that we can provide to Lemon Squeezy:

  1. First, make sure you have installed ngrok.

  2. Once ngrok is installed and your Wasp app is running, run:

    sh
    ngrok http 3001
  3. ngrok will display a forwarding address. Copy this address and append /payments-webhook to it. It should look something like this:

    sh title="callback url"
    https://89e5-2003-c7-153c-72a5-f837.ngrok-free.app/payments-webhook

Creating your Lemon Squeezy Webhook

Next, go to your Lemon Squeezy Webhooks Dashboard:

  1. Click the + button.
  2. Add the newly created webhook forwarding url to the Callback URL section.
  3. Give your webhook a signing secret (a long, random string).
  4. Copy and paste this same signing secret into your .env.server file under LEMONSQUEEZY_WEBHOOK_SECRET.
  5. Make sure to select at least the following updates to be sent:
    • order_created
    • subscription_created
    • subscription_updated
    • subscription_cancelled
  6. Click save.

You're now ready to start consuming Lemon Squeezy webhook events in local development.


Opensaas Sh Blog Src Content Docs 0.23 Guides Payment Integrations Polar (`opensaas-sh/blog/src/content/docs/0.23/guides/payment-integrations/polar.mdx`)


title: Polar
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/payment-integrations/polar

import ngrok from '@assets/ngrok.png';
import polarUserTable from '@assets/polar/user-table.png';
import polarWebhookLogs from '@assets/polar/webhook-log.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts, as described in the important first steps.

Next, you'll need to create a Polar account in the sandbox mode. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Sandbox and Production Mode

Polar features two separate environments:

They are fully isolated from each other, which means that you need to create seperate organizations for each of them. They also feature independent products, sales, access tokens, etc.

For local development and testing, you'll want to use Polar's sandbox mode.
To enable sandbox mode, make sure that POLAR_SANDBOX_MODE is set to true in your .env.server file:

ini title=".env.server"
POLAR_SANDBOX_MODE=true

When you're ready to deploy your app, we'll remind you to set this to false.

Polar Organization Access Token

Once you've created your account, you'll need to get your organization access token.
You can do that by:

  1. Navigating to the Developers section under the Settings > General page.
  2. Click on the New Token button to create a new token.
  3. Give your token a name (e.g., "Open SaaS Development").
  4. Select an expiration date. No expiration is fine for sandbox mode, but discouraged for production mode.
  5. Select the following scopes:
    • checkouts:write
    • customer_sessions:write
    • customers:read
    • customers:write
    • orders:read
  6. Copy the generated token and add it to your .env.server file:
    ini title=".env.server"
    POLAR_ORGANIZATION_ACCESS_TOKEN=polar_oat_...

Creating Products

To create Polar products, in your Polar dashboard:

  1. Navigate to Prodcuts > Catalogue page.
  2. Click on the + New Product button to create a new product.
  3. Fill in the product details.
    • For subscription products, select Recurring subscription pricing.
    • For one-time payment products, select One-time purchase pricing.
  4. Click Create Product.
  5. Tap the ⠇ icon next to your product and select Copy Product ID.
  6. Add the Product ID to your .env.server file.
    Open SaaS by default expects three products, two subscriptions and one one-time purchase plan:
    ini title=".env.server"
    PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID=<your-hobby-product-id>
    PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID=<your-pro-product-id>
    PAYMENTS_CREDITS_10_PLAN_ID=<your-credits-product-id>

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Using Polar Webhook in Local Development

Exposing your Webhook Endpoint to the Internet

Polar notifies your Wasp app about customer and payment events through a webhook. However, to make it available to Polar during development, you need to expose your locally running Wasp server (started with wasp start) to the internet.

You can do that by running ngrok on port 3001 (Wasp server runs on port 3001 by default). ngrok will then generate a public URL that we can provide to Polar:

  1. First, make sure you have installed ngrok.

  2. Once ngrok is installed and your Wasp app is running, run:

    sh
    ngrok http 3001
  3. ngrok will display a forwarding address. Copy this address and append /payments-webhook to it. It should look something like this:

    sh title="callback url"
    https://89e5-2003-c7-153c-72a5-f837.ngrok-free.app/payments-webhook

Creating your Polar Webhook

Next, configure the webhook in your Polar dashboard:

  1. Navigate to Settings > Webhooks page.
  2. Click Add Endpoint button.
  3. In the URL field, paste your ngrok forwarding address with /payments-webhook appended (for example: https://abc123.ngrok-free.app/payments-webhook).
  4. Set the Format to "Raw".
  5. Select the following events to listen for:
    • order.paid
    • subscription.updated
  6. Click Save.
  7. Copy the generated webhook secret and add it to your .env.server file:
    ini title=".env.server"
    POLAR_WEBHOOK_SECRET=polar_whs_...

Testing Payments in Local Development

Before testing payments, make sure that you created and set all the required env variables in .env.server (POLAR_ORGANIZATION_ACCESS_TOKEN, <NAME>_PLAN_ID and POLAR_WEBHOOK_SECRET ) and that your ngrok tunnel is running.

You can then test the payment flow:

  1. Click a Buy button for any product on the homepage.
  2. You should be redirected to Polar's checkout page.
  3. Fill in the checkout form with test payment information.
  4. Complete the payment.
  5. You should be redirected back to the checkout success page.

To verify everything executed correctly you can:

  • Check Polar webhook event logs:

    1. Navigate to Settings > Webhooks page
    2. Click the Details button of your previously created webhook.
    3. Confirm all of the events have been successful:
  • Inspect your database's User table:

    1. Run Wasp DB studio:
      sh
      wasp db studio
    2. Navigate to localhost:5555 and check the User table.
    3. Confirm the subscriptionStatus is active for the user who made the purchase.

Opensaas Sh Blog Src Content Docs 0.23 Guides Payment Integrations Stripe (`opensaas-sh/blog/src/content/docs/0.23/guides/payment-integrations/stripe.mdx`)


title: Stripe
banner:
content: >
Have an Open SaaS app in production? We'll send you some
swag! 👕

slug: 0.23/guides/payment-integrations/stripe

import ngrok from '@assets/ngrok.png';
import testApiKeys from '@assets/stripe/api-keys.png';
import dbStudio from '@assets/stripe/db-studio.png';
import priceIds from '@assets/stripe/price-ids.png';
import switchPlans from '@assets/stripe/switch-plans.png';
import testProduct from '@assets/stripe/test-product.png';
import { Image } from 'astro:assets';

First, make sure you've defined your payment processor in src/payment/paymentProcessor.ts, as described in the important first steps.

Next, you'll need to create a Stripe account. You can do that here.

:::tip[Star our Repo on GitHub! 🌟]
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!

If you're finding this template and its guides useful, consider giving us a star on GitHub
:::

Get your test Stripe API Keys

Once you've created your account, you'll need to get your test API keys. You can do that by navigating to https://dashboard.stripe.com/test/apikeys or by going to the Stripe Dashboard and clicking on the Developers:

  1. Click on the Reveal test key token button and copy the Secret key.
  2. Paste it in your .env.server file under STRIPE_API_KEY.

Create Test Products

To create a test product, go to the test products url https://dashboard.stripe.com/test/products, or after navigating to your dashboard, click the test mode toggle:

  1. Click on the Add a product button and fill in the relevant information for your product.

    • Select Software as a service (SaaS) as the product type.
    • For Subscription products, select Recurring as the billing type.
    • For One-time payment products, select One-time as the billing type.
  2. If you want to add different price tiers for the same product (e.g. monthly and yearly), click the Add another price button at the buttom.

  3. After you save the product, you'll be directed to the product page.

  4. Copy the price IDs and paste them in the .env.server file.

    • We've set you up with two example subscription product environment variables, PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_ID and PAYMENTS_PRO_SUBSCRIPTION_PLAN_ID.
    • As well as a one-time payment product/credits-based environment variable, PAYMENTS_CREDITS_10_PLAN_ID.

If you intend to let your users switch between two subscription plans, e.g. upgrade from hobby to pro, you'll need to create two separate products with their own price IDs. The ability for users to swich plans can then be configured later in the Customer Portal.

:::note[Naming products differently]
Note that if you change names of your products, you'll need to update your app code in src/payment/plans.ts and src/payment/paymentProcessorPlans.ts to match these names as well.
:::

Create a Test Customer

You can create a test customer in the Stripe Dashboard.

Click on the Add a customer button and fill in the relevant information for your test customer.

Alternatively, Open SasS automatically creates a test customer the first time a user starts a checkout session.
This customer is linked to the email address associated with the user in your app.

Set up the Customer Portal

You can set up your customer portal in your Stripe Dashboard.

By default, OpenSaas generates a unique customer portal link for each user on the back end.
If you'd rather provide a permanent link to the customer portal, activate it and copy the Portal link.

If you'd like to give users the ability to switch between different plans, e.g., upgrade from a "Hobby" to a "Pro" subscription, go down to the Subscriptions dropdown and select customers can switch plans.

Then select the products you'd like them to be able to switch between.

Now, after your users have paid, they can click on Manage Subscription in the client and will be taken to the customer portal where they can update their current plan.

Stripe CLI

To install the Stripe CLI follow the instructions here.

Make sure to login after you install the Stripe CLI:

sh
stripe login

:::caution[Errors running the Stripe CLI]
If you're seeing errors, consider appending sudo to the stripe commands.
See this GitHuh issue for more details.
:::

Testing Webhooks via the Stripe CLI

Start the Stripe CLI webhook forwarding on port 3001 where your Node server is running:

sh
stripe listen --forward-to localhost:3001/payments-webhook

:::caution[Webhook URL]
In older versions of this template, the webhook URL was http://localhost:3001/stripe-webhook.
If you're using an older version, make sure to use the url that matches the webhook url in your main.wasp file payemnts API definition.
:::

Remember to copy and paste the outputted webhook signing secret (whsec_...) into your .env.server file under STRIPE_WEBHOOK_SECRET.

In another terminal window, trigger a test event:

sh
stripe trigger payment_intent.succeeded

The results of the event