Introduction#
At Jitsu, we are big fans of Next.js. This website (jitsu.com) is built with it, and recently used Next.js to re-build Jitsu.Cloud billing server.
Few, we started another internal project and decided to experiment with Supabase and Next.js middleware. Here’s what we learned in the process
TL;DR - skip to What we’re going to build if you're familiar with Supabase and Next.js
What is Next.js#
Next.js is an open-source a web-development framework built on React. Unlike with React, you can write server-side code with Next.js too. In fact, every Next.js application is mix of backend and frontend. Next.js is maintained by Vercel. They make money out of paid Next.js hosting services.
What is Next.js middleware?#
Vercel team introduced middleware in recent v12 release. Middleware is just a file
(_middleware.js
or middleware.ts
) that you can place anywhere in pages/
folder of your Next.js project.
All requests to pages in this folder (and subfolders) will go through middleware.ts
). Middleware file can decided
if request should go through, alter request and etc/ Think of middleware as filter for particular sub-tree of your pages.
pages/
folder/
middleware.ts
page.tsx
One of great use-cases for middleware is authorization. Example (middleware.ts
):
export async function middleware(req: NextRequest, ev: NextFetchEvent) {
if (!isValidUser(req)) {
return NextResponse.redirect(`/login`)
} else {
//allow request to go through
return NextResponse.next()
}
}
What is Supabase?#
Supabase is an open-source Firebase alternative. It allows web developers to "outsource" backend logic - such as authorization, structured data storage (SQL) and file storage.
What we’re going to build#
The complete example can be found at @jitsucom/supabase-nextjs-middleware GitHub repo
We’re going to build a simple Next.js app which consist of index page (index.ts
) and few pages in app/
folder.
Index page will implement login/logout form and will be accessible for all users. We're going use
Google as login provider, but you can configure any supported provider
app/
pages will be hidden from unauthorized users. All unauthorized users will be redirected to login (index page)
Login page#
This page is fairly simple and standard. The authorization happens on a client side.
The only thing that is not standard is snippet
await fetch("/api/auth/set", {
method: "POST",
headers: new Headers({ "Content-Type": "application/json" }),
credentials: "same-origin",
body: JSON.stringify({ event, session }),
})
After successful login, we're calling /api/auth/set
API that sets a server cookie sb:token
. The cookie can be read
by middleware.ts
later.
pages/api/auth/set.ts
implements that API:
export default async function handler(req, res) {
await supabase.auth.api.setAuthCookie(req, res)
}
And we need to remove cookie when user logs out. We need to call API too:
await fetch("/api/auth/remove", {
method: "GET",
credentials: "same-origin"
})
See implementation in pages/api/auth/remove.ts
.
Supabase haven’t implemented removeAuthCookie()
yet, so we have to do it manually
export default async function handler(req, res) {
res.setHeader('Set-Cookie', 'sb:token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT');
res.send({});
}
Middleware#
Here’s middleware.ts
:
export async function middleware(req: NextRequest, ev: NextFetchEvent) {
let authResult = await getUser(req)
if (authResult.error) {
console.log("Authorization error, redirecting to login page", authResult.error)
return NextResponse.redirect(`/?ret=${encodeURIComponent(req.nextUrl.pathname)}`)
} else if (!authResult.user) {
console.log("No auth user, redirecting")
return NextResponse.redirect(`/?ret=${encodeURIComponent(req.nextUrl.pathname)}`)
} else {
console.log("User is found", authResult.user)
return NextResponse.next()
}
}
All interesting staff happens in getUser
method. Supabase has an API - supabase.auth.api.getUserByCookie
, but unfortunately
it doesn't work for Next.js. Good thing is that we can implement verify the auth manually by calling
/auth/v1/user
Supabase end-point
let token = req.cookies["sb:token"]
if (!token) {
return {
user: null,
data: null,
error: "There is no supabase token in request cookies",
}
}
let authRequestResult = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/user`, {
headers: {
Authorization: `Bearer ${token}`,
APIKey: process.env.NEXT_PUBLIC_SUPABASE_KEY || "",
},
})
let result = await authRequestResult.json()
console.log("Supabase auth result", result)
if (authRequestResult.status != 200) {
return {
user: null,
data: null,
error: `Supabase auth returned ${authRequestResult.status}. See logs for details`,
}
} else if (result.aud === "authenticated") {
return {
user: result,
data: result,
error: null,
}
}
Accessing user from SSR environment ( getServerSideProps
)#
Now, all authorized users will be able to access all pages inside app/
. In the browser, Supabase user can be obtained through supabase.auth.user()
call
(don’t forget to wrap it in useEffect()
, otherwise the code will be executed as SSR).
But what if we want to access current user user information during SSR — inside getServerSideProps
? Here’s what we can do:
Supabase has an API for that — supabase.auth.api.getUser()
. But this function queries Supabase HTTP API to validate the token.
But it's redundant since the token has been already validated in middleware.ts
call.
Luckily, Supabase token is a JWT token, meaning that all user information is encoded inside it.
The token also contains the signature that should be verified. But verification has been done in middleware.ts
so we can
just decode the token and skip verification.
const Hidden: NextPage<{user: any}> = (props) => {
return <h1>Do not tell anybody about this page, {props.user.email}</h1>
}
export async function getServerSideProps(context) {
let supabaseToken = context.req.cookies['sb:token']
if (!supabaseToken) {
throw new Error("It should not happen! Since this page is guarded by _middlware.ts the presense of supabase token cookie (sb:token) should be already checked")
}
return {
props: {
//we do not need to verify JWT signature since it has been already done in _middlware.ts
user: jwt.decode(supabaseToken),
},
}
}
(Don’t forget to yarn add jsonwebtoken
or npm install jsonwebtoken
!)
The complete example#
The complete application example with all deployment and configuration example can be found [on our GitHub] (https://github.com/jitsucom/supabase-nextjs-middleware/blob/main/pages/app/hidden-page.tsx).