Full-stack SaaS Demo App
This demo app is a full-stack recipe SaaS app. It is not meant to be an actual SaaS app, but rather a demo of how one could be built. The ‘product’ here is the ability to search for recipes. The “free” version allows a limited number of recipe searches, and “paid” versions allow a higher number of recipe searches.
However, this app could easily be modified so that the “subscription” is for something else, like the ability to create, save, or share recipes with friends.
Why such a long and detailed README.MD?
- Because when I was learning how to do this just recently, I felt like I had to stitch together a bunch of different tutorials, docs and repos to get a working app. What I mean is the kind of working app whose setup, deployment, patterns and logic can be easily understood, and one can get to building/modifying things further without too much friction. While it is my intention to make it easier for others in this regard, I do not claim to be an expert in any of the technologies used here. I’m sure there are better ways to do things, and I’m open to suggestions. I’m also open to contributions, so feel free to open a PR if you think something can be improved.
Here are some features:
- User management and authentication with Supabase
- Data access & management tooling on top of PostgreSQL with Supabase
- Integration with Stripe Checkout and the Stripe customer portal
- Automatic syncing of pricing plans and subscription statuses via Stripe webhooks
- Fetching recipes from an external API and displaying it in the app
- Saving any of the recipes retrieved from the search
- Deleting any of the already saved recipes
- Using server actions for everything except Stripe and fetching recipes
- shadcn ui (yes, that is a feature)
Clone the repo and install dependencies:
Supabase env variables
.env.example file and rename it to
.env. For the first step, fill in the Supabase environment variables. You can find these in your Supabase project settings.
Supabase database setup
Copy the contents of
schema.sql. Go to your project -> SQL editor -> paste and run. This should create the following tables in your Supabase database:
All tables except
user_recipes are those you’d get if you ran the
Stripe Subscriptions starter template in Supabase. The
user_api_limit table is used to keep track of how many API calls to an external API a user has made, and the
user_recipes table is used to keep track of which recipes a user has saved. These two tables were created specifically for this demo app.
Stipre environment variables
Once you’ve set up your Supabase database, you’ll need to set up your Stripe environment variables. You can find
Publishable key and
Secret key in your Stripe dashboard. To retrieve the
- Navigate to your Stripe dashboard. In the search bar, type
webhooksand go to
Developers > Webhooks->
Test in a local environment
- Open two concurrent terminal windows.
- In the first terminal window, run
npm run dev.
- In the second terminal window, run
stripe loginand follow the auth flow. Then return to your terminal.
- In the window where you ran
stripe login, run
stripe listen --forward-to localhost:3000/api/webhooks(replace the port with whatever you’re using). If you followed the steps above, you should see a webhook secret (
whsecprefix) printed in your terminal. Copy and paste it in the place of
- To check the configuration, run
stripe trigger payment_intent.succeeded. You should see a bunch of responses in your terminal with
Get your API key from Low Carb Recipes and paste it in the place of
RAPID_API_KEY in your
Creating a Stripe product
Important: If you’re developing locally, make sure the development server (
npm run dev) and Stripe webhook forwarding (
stripe listen --forward-to localhost:3000/api/webhooks) are running concurrently in two separate terminals like in the section above. Otherwise, interactions with Stripe (creating a product, purchasing a subscription, etc.) will work in Stripe, but they won’t be reflected in your Supabase database.
Navigate to your Stripe dashboard -> Products -> Add product. For this demo, I recommend creating a simple product, meaning one with a standard pricing model, one currency, one price, and without metered usage. If everything is set up correclty, you should see the product you created in your Supabase database.
Deployment (with Stripe test mode)
- Deploy the app to Vercel like you normally would with all your .env variables
- Copy the domain that Vercel assigned to you (e.g. https://yourwebsite.vercel.app/)
- Go to your Stripe dashboard ->
add an endpoint
Select events to listen to->
select all events(or just those you need)
After this, you should be redirected to a page where you’ll find ‘Signing secret
. Copy and paste it in the place of STRIPE_WEBHOOK_SECRET
in your.env` file. Then, redeploy the app to Vercel.
URL Configuration -> put your domain assigned by Vercel in
Deployment (with Stripe live mode)
Archive testing products
Archive all test mode Stripe products before going live. Before creating your live mode products, make sure to follow the steps below to set up your live mode env vars and webhooks.
Configure production environment variables
To run the project in live mode and process payments with Stripe, switch Stripe from “test mode” to “production mode.” Your Stripe API keys will be different in production mode, and you will have to create a separate production mode webhook. Copy these values and paste them into Vercel, replacing the test mode values.
Afterward, you will need to rebuild your production deployment for the changes to take effect. Within your project Dashboard, navigate to the “Deployments” tab, select the most recent deployment, click the overflow menu button (next to the “Visit” button) and select “Redeploy” (do NOT enable the “Use existing Build Cache” option).
To verify you are running in production mode, test checking out with the Stripe test card. The test card should not work.
The following section servers to explain the structure and/or logic of the app so that you can spend less time understanding/bug-fixing and more time modifying it to your needs.
- I’m keeping the Supabase database types in
types_db.ts. If you make any changes to the database, make sure to update the types by following this guide.
- The types in
types.tsstore the types for recipe data as well as some
helpertypes that relate to the Supabase database (e.g.
The structure of
middleware.ts is slightly different from the default ones you see (like in Supabase docs). The few changes I made are:
- A check to see if a user is not logged in and the currentl URL is not root (
/). If both are true, users are redirected to the
sign-inpage. This is because most SaaS app have a homepage that is accessible to everyone, even if they’re not logged in.
- A check to see if a user is logged in and the current URL is root (
/). If both are true, users are redirected to the
recipespage. You probably won’t need this in your app, but I put it there because I wanted to make sure that users who are logged in don’t see the homepage (since it barely has any content).
There’s a bunch of ways to define a matcher (see here). In this case, it’s a regex that matches all routes except for those defined there. If you want to modify this, make sure it does not match the Stripe routes (defined in
app/api). If it does, you’ll be getting
307 status codes when testing the Stripe integration locally.
Working with Supabase in server components
app/supabase-server.ts defines a bunch of utility functions. The two you’ll probably be using most often in server components/actions are
createServerSupabaseClientto create a Supabase client that you can use to perform database operations.
getSessionto get the current session (and retrieve user id if needed).
app/actions/delete-recipe-supabase.ts for an example of how to use these functions.
Working with Supabase in client components
There’s multiple ways to do this, but I chose to create a
supabase-provider.tsx in the
providers directory. This creates a session content that wraps the entire app in
app/layout.tsx. This means that in client components, we can import
useSessionContent and get
app/(site)/plans/components/products.tsx for an example of this.
SSG vs. SSR
- Since all pages (except for homepage) utilize cookies, they need to be server-side rendered. This is why you’ll see
export const revalidate = 0in a bunch of places. Excluding this will throw
dynamicservererror: dynamic server usage: cookieson deployment in Vercel.
Working with Stripe data about products, prices and more
See helper functions in
app/supabase-server.ts. Examples of how to use these to retrieve and display relevant Stripe data can be found in
Known issues/areas for improvement
- Subscribing to a plan signs you out. I asusme this has to do with the fact that in
/utils/supabase-admin.ts, I’m setting
persistSession: false. However, if I set it to
true, I get a
no storage option exists to persist the sessionwarning, and the problem persists. In that case, I think it gets overridden by
export const revalidate = 0throughout the project (which I can’t really remove because otherwise deployment to Vercel fails). I’m not sure how to fix this, but if you do, please let me know or submit a PR.
- The ‘form’ on the account page is lacking proper configuration and error handling, but works (meaning you can update your name and email and it will sync with Supabase)