As you may have seen, we are working on a new SaaS kit based on Supabase and Next.js 13 using React Server Components and the new app directory. We are excited to share a behind-the-scenes look at how we built the kit.
A quick recap: what is a SaaS kit?
Makerkit's kits are a collection of templates that you can use to build your own SaaS products.
The goal of the kits is to provide the building blocks that you need to build your own SaaS product and to help you get started quickly, such as a landing page, authentication, Stripe payments, emails, multi-tenancy architecture based on organizations, user invites, and more.
The kits are built using the latest technologies and are designed to be easy to customize and extend to fit your needs.
The Supabase and Next.js RSC SaaS kit
The Supabase and Next.js RSC SaaS kit is a new kit that we are working on. It is based on Supabase, Next.js 13 React Server Components and the new app directory.
A rewrite of the Supabase and Remix SaaS kit
It's mostly a rewrite of the Supabase and Remix SaaS kit so the two kits are very similar. While competing, the two frameworks are extremely similar, and the migration was mostly a matter of changing the framework-specific code.
In short, both frameworks are great and you can't go wrong with either one. It's important to remember that while Remix is production-ready, (at the time of writing) the Next.js app directory is still in beta.
The Next.js app directory is in beta
Therefore, this kit is going to be a beta kit as well until the Next.js app directory is out of beta.
Enter React Server Components
The most striking difference that you will find in the new kit is the use of React Server Components.
The new Next.js app directory leverages React Server Components by default: Server components are rendered on the server and then sent to the client as a serialized stream.
What the above means is that the client can render the component without having to download the JavaScript bundle: as you can imagine, this allows Next.js to deliver a much faster initial page load.
Did you notice performance gains?
Generally speaking, yes.
But the performance gains don't come for free: your components need to be written in a way that allows them to be rendered on the server, which means they will mostly be data-fetching and rendering components, and no interactivity.
Interactive components (client components) will be rendered using SSR and hydrate on the client just like you're used to the traditional Next.js /pages
directory.
When you're able to write your components in a way that allows them to be rendered on the server, combined with carefully written laoding.tsx
loading handlers, you can take advantage of the performance gains that server components unlock and provide a much smoother user experience than possible today.
The new Next.js app directory
The new Makerkit app directory will look like the following tree:
- app - (app) - components - dashboard - page.tsx - settings - organization - page.tsx - profile - page.tsx - subscription - page.tsx - layout.tsx - loading.tsx - (site) - about - page.tsx - faq - page.tsx - pricing - page.tsx - layout.tsx - loading.tsx - page.tsx - api - organizations - route.ts - stripe - checkout - route.ts - auth - components - password-reset - page.tsx - sign-in - page.tsx - sign-up - page.tsx - layout.tsx - loading.tsx - invite - [code] - components - layout.tsx - onboarding - page.tsx - components.css - globals.css - layout.tsx - loading.tsx
If you're not familiar with the Next.js app directory, here is a quick overview:
- pages are defined using the special filename
page.tsx
- layouts are defined using the special filename
layout.tsx
- loading handlers are defined using the special filename
loading.tsx
- directories will create a new route, for example,
app/dashboard
will create the route/app/dashboard
- directories using parenthesis are "pathless", .e.g.
(app)
will not create the route/app
, but will start from the root of the app directory, for example,(app)/dashboard
will create the route/dashboard
. The same goes for(site)
Pathless directories
Pathless directories are useful for defining routes that are not part of your app: for example, the (site)
directory is used to define the routes for your landing page, pricing page, and more. Instead, the app
directory is used to define the routes for your app, i.e. the ones behind authentication.
Data-Fetching Layouts
What makes these particularly useful is the fact we can define data-fetching layouts that can fetch the data that is common to the pages in the directory: for example, it may not be useful to fetch the current organization in the site
pages, but it is useful to fetch the current organization in the app
pages.
Or, for example, we want to ensure that logged-in users can't access the auth
pages, but we don't want to do the same for the app
pages. By using layouts in such a way that we can control both data-fetching and authentication, we can write our pages in a way that is more declarative and easier to reason about.
Co-locating components
Another interesting feature of the Next.js app directory is the ability to co-locate components with your pages. As you can see, we define the directory components
close to where they're used, such as near the auth
pages.
In this way, we can easily see which components are used by which pages, and we can easily move them around if we need to.
Given we have shared
components, we can also define a components
directory at the root of the app directory, which will be shared across all pages.
The good parts of the Next.js app directory
The Next.js app directory is a great way to organize your Next.js app. In fact, I quite enjoyed the experience of using it.
Layouts
Layouts allow us to define components that wrap your pages and provide a consistent look and feel across your app.
Additionally, layouts also help you to fetch data that is common to all pages in your app, such as the current user, the current organization, and more.
Data loading
We can use Layouts for fetching data that is common to all pages in its directory. For example, we can fetch the current user in the auth
directory, and we can fetch the current organization in the app
directory, and so on.
In Makerkit, we define "data loaders" such as loadAppData
and loadAuthData
that are used to fetch the data that is common to all pages in the app
and auth
directories, respectively.
These also perform some validation such as ensuring that the user is authenticated and that the user is part of the organization, and so on. In such cases, we define a redirect
property that tells the layouts not to proceed and redirect somewhere instead.
Let's see the loader for the authentication pages:
import getSupabaseServerClient from '~/core/supabase/server-client';import configuration from '~/configuration';const loadAuthPageData = async () => { try { const client = getSupabaseServerClient(); const { data: { session }, } = await client.auth.getSession(); if (session) { return { redirect: true, destination: configuration.paths.appHome, }; } return {}; } catch (e) { return {}; }};export default loadAuthPageData;
Now, let's use the loader above in the auth
layout:
import { use } from 'react';import { redirect } from 'next/navigation';import loadAuthPageData from '~/lib/server/loaders/load-auth-page-data';import Logo from '~/core/ui/Logo';import I18nProvider from '~/i18n/I18nProvider';function AuthLayout({ children }: React.PropsWithChildren) { const data = use(loadAuthPageData()); if ('redirect' in data && data.destination) { return redirect(data.destination); } return ( <I18nProvider> <div className={ 'flex h-screen flex-col items-center justify-center space-y-4 md:space-y-8 lg:bg-gray-50 dark:lg:bg-black-700' } > <div> <Logo /> {children} </div> </div> </I18nProvider> );}export default AuthLayout;
As you can see, we:
- fetch the data
- if the data contains a
redirect
property, we redirect to the destination (for example, when the user is already logged in) - otherwise, we render the children (such as the sign-in page, etc.)
The other layouts will work in a very similar way.
The new Next.js Route handlers
Next.js has also released an alternative way to define API routes, using the new Route handlers. This new API is still experimental and will replace the "old" pages/api
API handlers.
Let's take a quick look at the new API handlers. We have added a new directory called api
that contains the API handlers. To define a route, we create a directory within api
and create a file called route.ts
.
For example, we create a route handler for the /api/stripe/checkout
route:
export async function POST( req: Request) { const payload = await req.text(); /// handle webhook here return NextResponse.json({ success: true, });}
As you can see we can define the method handlers by simply exporting the method name using its uppercase form. For example, we can define a GET
handler by exporting a GET
function.
export async function GET( req: Request) { const data = await getData(); return NextResponse.json(data);}
While I have created a folder named api
which looks a lot like the pages/api
directory, it is important to notice that you can co-locate API handlers with your pages, for example, you can create a checkout
directory in the app
directory and create a route.ts
file there and execute a request against the /app/checkout
route.
In my opinion, it can still be useful to define API handlers in a separate directory, such as the api
directory, as it allows you to define API handlers that are not part of your app, such as the stripe
API handlers. This is just my preference at this time, and it can likely change in the future.
Difficulties with the Next.js app directory
While everything has been generally smooth, there have been a few difficulties with the Next.js app directory.
i18n
At this time, the biggest difficulty with the Next.js app directory has been i18n using i18next
. I have been able to get it working, but it has been a bit of a struggle.
The examples online are either lacking or simply don't work, and I have had to spend a lot of time trying to figure out how to get them working.
Mutations
Next.js is still working on mutations, which will likely allow us to define mutations in the same way we define API handlers, and automatically refresh the loaders when the mutations are executed, similar to how Remix works.
To work it around for the time being, mutations are executed using fetch requests wrapped with useMutation
from swr
, and manually calling router.refresh
when the mutation succeeds. Ideally, we will be able to remove the router.refresh
call once mutations are supported.
Conclusion
The Next.js Supabase SaaS kit is slated to be released in March, and I am very excited about it. Do you have any questions about the kit? Let me know!
If you want, sign up for the newsletter to be notified when the kit is released.
Ciao!