Getting started
User authentication
Pre-requisites needed
You will need a database for this section, please make sure that you've set up a database before you start this guide.
If you want to implement Magic Links, you will need to have configured emails in order for it to work.
SupaSaaS uses the tried and tested community favourite, NextAuth, to handle user authentication.
NextAuth allows us to create two types of authentication workflows:
- Social Login (which includes GitHub, Google, Facebook & 60+ more providers)
- Magic Links (passwordless login).
It's completely up to you which type you use, or what providers you want to offer. In this guide, I'll show you how to implement both.
NextAuth
Configuration
The authentication logic for NextAuth is contained in the file @/config/auth.ts
. In this file, you can configure what auth providers you want to use.
NextAuth requires 2 environment variables to work, make sure to fill these in like below.
.env NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="4348yhu34h3ui4ofjndfsdfeirh4b637u5sfd3"
The NEXTAUTH_SECRET
is a random string of at least 10 characters which is used to encrypt authentication data.
You can generate a NextAuth secret key using this free tool.
Environment variables
Make sure that you don't prefix your environment variables with NEXT_PUBLIC_
as this will expose them to the client side and be vulnerable to security risks.
Magic Links
Magic Links are a great way to allow users to log in without needing to remember a password.
They work by sending an email to the user with a link that they can click to log in.
Loops setup
Add an EmailProvider
to the authOptions
configuration object. This will allow NextAuth to send emails using the Loops API.
@/src/auth.ts import { sendTransactionalEmailWithLoops } from "@/lib/email/loops"; providers: [ EmailProvider({ async sendVerificationRequest({ identifier: email, url }) { // Send transactional with Loops await sendTransactionalEmailWithLoops({ transactionalId: process.env.NEXT_PUBLIC_LOOPS_TRANSACTIONAL_ID || "", // the transactional id you created on Loops email, dataVariables: { url, // the dynamic url we setup in the "Setup emails" section }, }); }, }), ],
Mailchimp setup
Once you've got the API key, we need to add it to your environment variables.
Head over to your .env file and paste it into the LOOPS_API_KEY
variable. It should look like this:
@/src/auth.ts import { emailFrom } from "@/config/config.ts"; import { sendTransactionalEmailWithMailchimp } from "@/lib/email/mailchimp"; providers: [ EmailProvider({ await sendTransactionalEmailWithMailchimp({ to: [email], from: emailFrom, subject: "Sign In to MyApp", text: `Please click here to authenticate - ${url}`, html: `<p>Please click <a href="${url}">here</a> to authenticate</p>`, }); }), ],
Social Login
To enable Google login, you need to create a new project on the Google Developer Console.
Once you've created a new project, head over to the OAuth consent screen
and fill in the necessary details.
After that, go to the Credentials
tab and create a new OAuth 2.0 Client ID
. Make sure to add http://localhost:3000/api/auth/callback/google
as an authorized redirect URI.
Once you've created the client ID, copy the Client ID
and Client Secret
and add them to your environment variables.
.env GOOGLE_CLIENT_ID="your google client id" GOOGLE_CLIENT_SECRET="your google client secret"
GitHub
To enable GitHub login, you need to create a new OAuth app on the GitHub Developer Settings page.
Fill in the necessary details and add http://localhost:3000/api/auth/callback/github
as the callback URL.
Once you've created the OAuth app, copy the Client ID
and Client Secret
and add them to your environment variables.
.env GITHUB_ID="your github client id" GITHUB_SECRET="your github client secret"
To enable Facebook login, you need to create a new app on the Facebook Developer Console.
Fill in the necessary details and add http://localhost:3000/api/auth/callback/facebook
as the callback URL.
Once you've created the app, copy the App ID
and App Secret
and add them to your environment variables.
.env FACEBOOK_ID="your facebook app id" FACEBOOK_SECRET="your facebook app secret"
To enable Twitter login, you need to create a new app on the Twitter Developer Console.
Fill in the necessary details and add http://localhost:3000/api/auth/callback/twitter
as the callback URL.
Once you've created the app, copy the API Key
and API Secret Key
and add them to your environment variables.
.env TWITTER_ID="your twitter api key" TWITTER_SECRET="your twitter api secret key"
Enabling providers
NextAuth supports over 60+ providers, you can find the full list here.
You can have as many or as little providers enabled at any time as you like. Simply add them to the providers
array in the authOptions
object in the @/config/auth.ts
file like below.
@/config/auth.ts import GitHubProvider from "next-auth/providers/github"; import GoogleProvider from "next-auth/providers/google"; import FacebookProvider from "next-auth/providers/facebook"; import TwitterProvider from "next-auth/providers/twitter"; providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", }), GitHubProvider({ clientId: process.env.GITHUB_ID || "", clientSecret: process.env.GITHUB_SECRET || "", }), FacebookProvider({ clientId: process.env.FACEBOOK_ID || "", clientSecret: process.env.FACEBOOK_SECRET || "", }), TwitterProvider({ clientId: process.env.TWITTER_ID || "", clientSecret: process.env.TWITTER_SECRET || "", }), ],
You can also have Magic Links authentication enabled alongside social login providers. Simply add the EmailProvider
to the providers
array like below.
@/config/auth.ts import EmailProvider from "next-auth/providers/email"; import GitHubProvider from "next-auth/providers/github"; import GoogleProvider from "next-auth/providers/google"; import FacebookProvider from "next-auth/providers/facebook"; import TwitterProvider from "next-auth/providers/twitter"; providers: [ EmailProvider({ async sendVerificationRequest({ identifier: email, url }) { // Send transactional with Loops await sendTransactionalEmailWithLoops({ transactionalId: process.env.NEXT_PUBLIC_LOOPS_TRANSACTIONAL_ID || "", // the transactional id you created on Loops email, dataVariables: { url, // the dynamic url we setup in the "Setup emails" section }, }); }, }), GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID || "", clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", }), GitHubProvider({ clientId: process.env.GITHUB_ID || "", clientSecret: process.env.GITHUB_SECRET || "", }), FacebookProvider({ clientId: process.env.FACEBOOK_ID || "", clientSecret: process.env.FACEBOOK_SECRET || "", }), TwitterProvider({ clientId: process.env.TWITTER_ID || "", clientSecret: process.env.TWITTER_SECRET || "", }), ],
SupaSaas comes with a set of pre-built components that you can use to allow users to sign in and sign up.
You can find these components in the @/components/Auth
directory.
Sign In & Sign Out Pages
Pre-built and configured sign in @/app/(frontend)/signin
and sign up @/app/(frontend)/signup
pages are available for you to customize.
Social Login Buttons
I also provide a SocialAuthSignInButton
component that you can use to allow users to sign in with their social accounts from anywhere in your app.
To use it, just import it and render with the provider as the prop.
example.tsx import SocialAuthSignInButton from "@/components/Auth/Buttons/SocialAuthSignInButton"; <div className="flex flex-col gap-2"> <SocialAuthSignInButton provider="google" /> <SocialAuthSignInButton provider="github" /> </div>
Auth State Management
SupaSaaS uses Zustand for state management. It's a simple and fast state management solution that allows you to manage global state in your application.
I've implemented an authStore
that you can use to house all logic related to user authentication, which can then be accessed by any component from anywhere in your app with just 2 lines of code. It's pretty awesome!
You can find the authStore
in the @/store/auth-store.ts
file.
In this file, you'll find the getSubscriptionInfo
function which handles setting the users subscription status.
Set your payment provider
You need to comment/uncomment the payment provider that you're using in the getSubscriptionInfo
function.
@/store/auth-store.ts getSubscriptionInfo: async () => { // const data = await getLemonSqueezyPlan(); const data = await getStripePlan(); // ... rest of function }
Plan properties
It's in this file that the logic is stored which is responsible for checking the users active plan, and setting what plan properties they have access to.
For example, in a "Basic Plan" a user might have access to upload 10 images, whereas in a "Pro Plan" they might have access to upload 100 images.
Please refer to the create a database guide and configure payments guide for more information on how the model relationships are set up to handle this.
Inside the getSubscriptionInfo
function, scroll down until you see this code:
@/store/auth-store.ts // Loop through the plan properties and set the values for the user // Do this for each property your app needs to set, or implement your own logic if it's alot. data.planProperties.forEach((property: ProductPlanProperty) => { if (property.propertyName === "MAX_IMAGES") { set({ planMaxImages: property.value }); } });
This is where you can set the plan properties for the user. Make sure that each property name matches the name of a ProductPlanPropertyName in your database.
In our example, MAX_IMAGES is a property that we set for the user, which is the maximum number of images they can upload.
Setting plan properties
First, you need to update the AuthStore
type to include the new property. In this example, let's add a property which limits the number of domains our user can add to some feature within our app.
In your database, you would have a ProductPlanPropertyName
called MAX_DOMAINS
which you can set for each plan. Please refer to the configure payments guide for more information on how to set this up.
@/store/auth-store.ts type AuthStore { planMaxImages: number; planMaxDomains: number; // Add property name here }
Then, you need to update the initialState
object to include the new property.
@/store/auth-store.ts const useAuthStore = create<AuthStore>((set) => ({ planName: "", planStatus: "", planProperties: [], planCanRemoveBranding: false, planMaxImages: 0, planMaxDomains: 0, // Add the initial state value here billingPortal: "", // ... rest of function }));
Finally, you need to update the getSubscriptionInfo
function to set the new property for the user.
@/store/auth-store.ts data.planProperties.forEach((property: ProductPlanProperty) => { if (property.propertyName === "MAX_IMAGES") { set({ planMaxImages: property.value }); } // Add the new property here and set the planMaxDomains value if (property.propertyName === "MAX_DOMAINS") { set({ planMaxDomains: property.value }); } });
Accessing auth state
You can access the auth state from any component in your app with just 2 lines of code by importing the useAuthStore
hook and calling it like below.
example.tsx import { useAuthStore } from "@/store/auth-store"; const { planStatus, planMaxImages, planMaxDomains } = useAuthStore();
You can then use these values in your component like below.
example.tsx <div> <p>Plan status: {planStatus}</p> <p>Max images: {planMaxImages}</p> <p>Max domains: {planMaxDomains}</p> </div>
useAppAuth hook
I've also created a custom hook called useAppAuth
that you can use.
This will return you ALL the necessary authentication properties that you could possibly need for managing your app.
The hook will return you the following properties:
requiredPlanName userPlanName userPlanProperties userPlanMaxImages userPlanCanRemoveBranding userPlanBillingPortal isLoggingIn isLoggedIn isLoggedOut isPlanActive loggedInEmail loggedInName loggedInImage loggedInId getSubscriptionInfo
You can find this hook in the @/hooks/useAppAuth.ts
file.
You can use this hook in any component where you need to check auth status like below.
example.tsx import { useAppAuth } from "@/hooks/useAppAuth"; const { isLoggedIn, isPlanActive, userPlanMaxImages } = useAppAuth(); if (!isLoggedIn) { return <p>You need to be logged in to access this page</p>; } if (!isPlanActive) { return <p>You need to upgrade your plan to access this feature</p>; } return <p>You can upload {userPlanMaxImages} images</p>;