RBAC & Private Routes in Next.js with Supabase using NextShield.
NextShield + Supabase Example.

I'm an engineer that loves JavaScript technologies and Linux, currently learning Go.
Search for a command to run...
NextShield + Supabase Example.

I'm an engineer that loves JavaScript technologies and Linux, currently learning Go.
Thank you for the documentation. I had one issue, I had Supabase Auth working with Email/Password signup and third party auth. Once I tried implementing Next Shield, the signin/signup functionality fails. I get 403 on signin and 500 on signup.
Any insights on how to fix this?
NextShield + Clerk Example

Get RBAC & Private Routes in your Next.js app with any auth provider using NextShield.

Learn how to create email templates with react and send them with SMTP.js

As you maybe already know, the flutter's 2.0 stable channel already integrates the feature of building apps for desktop, but it stills in beta, you have to consider that. In this post, I'm going to show you how to generate the build for Windows and c...

Hello everyone! Hope you liked the previous post. Today we are going to build a Supabase example with NextShield, so let's get started.

Go to supabase website and click on "Start your project":

Sign in and click on "New Project":

Fill the form and create the project:

Wait until the project is created:

We need to configure supabase to satisfy our needs with RBAC, so go to the SQL menu and click on new query:

Create the profiles table:
create table public.profiles (
id uuid references auth.users not null,
role text,
primary key (id)
);
And enable row level security to restrict the access:
alter table public.profiles enable row level security;
Then add the following policies to grant user access to their data:
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
Finally, create a trigger to insert a new record when a new user is created:
-- inserts a row into public.profiles
create function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public.profiles (id, role)
values (new.id, "EMPLOYEE");
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
For a real project, you want this trigger to be well-tested because if it fails it could block the user sign ups.
To avoid writing boilerplate code you can use the project that we wrote in the previous post, but if you didn't follow that one, you can just clone the repo (clone the main branch).
npm i next-shield @supabase/supabase-js valtio
Replace the code under styles/globals.css with:
* {
box-sizing: border-box;
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-color: black;
color: white;
}
nav {
display: flex;
flex-direction: column;
}
@media (min-width: 768px) {
nav {
flex-direction: row;
justify-content: space-evenly;
}
}
a {
display: block;
text-align: center;
text-decoration: none;
color: white;
padding: 1rem;
transition: all ease-in-out 0.3s;
}
a:hover {
color: black;
background-color: #3cce8d;
}
button {
padding: 0.5rem 1rem;
margin: 0.5rem;
cursor: pointer;
background-color: white;
color: black;
border: none;
font-weight: 700;
font-size: 1.2rem;
transition: all ease-in-out 0.3s;
}
button:hover {
background-color: #3cce8d;
}
.center {
height: 40vh;
display: grid;
place-items: center;
text-align: center;
}
@media (min-width: 768px) {
.center {
height: 90vh;
}
}
.loading {
margin: 40vh auto;
}
Create a file in the root of the project called .env.local with the following values:
NEXT_PUBLIC_SUPABASE_URL=""
NEXT_PUBLIC_SUPABASE_KEY=""
Then go to your supabase project, scroll down and search for those values:


Create a folder called db with the file client.ts:

And Initialize the client
import { createClient } from '@supabase/supabase-js'
// Create a single supabase client for interacting with your database
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL as string,
process.env.NEXT_PUBLIC_SUPABASE_KEY as string
)
Supabase gives you a way of having OAuth2 by providing the secret keys of the auth provider that you're using (Google, Twitter, GitHub, etc.), you can use whatever you want in this example even passwordless auth, in my case I used the google provider, to get the keys you can see the following tutorial:
Go to the file types/User.ts and replace its content with:
export interface UserProfile {
id: string
role: string
}
export interface Auth {
isAuth: boolean
isLoading: boolean
user: UserProfile | null | undefined
}
export type AuthStore = {
updateAuth: (params: Auth) => void
} & Auth
Create a store folder with an index.ts file:

Then we are going to use the valtio state manager for our store, with the following code:
import { proxy } from 'valtio'
import { supabase } from '@/db/client'
import type { AuthStore } from '@/types/User'
export const authStore = proxy<AuthStore>({
user: undefined,
isAuth: !!supabase.auth.user(),
isLoading: false,
updateAuth({ isAuth, isLoading, user }) {
authStore.isAuth = isAuth
authStore.isLoading = isLoading
authStore.user = user
},
})
That's it, the main reason for using valtio is the simplicity of usage and the improvements that it has to avoid unnecessary rerenders.
useAuth Hook.Create a folder called hooks with an auth.ts file:

Create a function called useAuth:
export function useAuth() {}
Inside of it use the useSnapshot to access to our store:
const { updateAuth, ...state } = useSnapshot(authStore)
Then define the sign in & out functions:
const signIn = useCallback(async () => {
await supabase.auth.signIn({ provider: 'google' })
}, [])
const signOut = useCallback(async () => {
await supabase.auth.signOut()
}, [])
After that add an useEffect and pass the updateAuth method as the only dependency:
useEffect(() => {
}, [updateAuth])
Then create a function called getUserProfile to query the user profile:
const getUserProfile = async () => {
const { data: profile } = await supabase
.from<UserProfile>('profiles')
.select('id, role')
.single()
if (profile) {
updateAuth({
user: profile,
isAuth: true,
isLoading: false,
})
return
}
updateAuth({
user: null,
isAuth: false,
isLoading: false,
})
}
The reason why we are not using a filter to get a specific user is because we already defined in our policies that each user owns their data and nobody else.
After defining this function, execute it to make a request when the component mounts and also execute it inside the onAuthStateChange to request the user's data whenever the auth state changes:
getUserProfile()
const { data } = supabase.auth.onAuthStateChange(() => {
getUserProfile()
})
And also create a subscription to get realtime data:
const profile = supabase
.from<UserProfile>('profiles')
.on('UPDATE', payload => {
updateAuth({
user: payload.new,
isAuth: true,
isLoading: false,
})
})
.subscribe()
And finally unsubscribe when the component unmounts:
return () => {
data?.unsubscribe()
supabase.removeSubscription(profile)
}
Then just return the functions that we created alongside the state:
return {
signIn,
signOut,
...state,
}
In dashboard.tsx, users/index.tsx and users/[id].tsx add the sign out:
import { Layout } from '@/components/routes/Layout'
import { useAuth } from '@/hooks/auth'
export default function Dashboard() {
const { signOut } = useAuth()
return (
<Layout title="Dashboard">
<h1>Dashboard</h1>
<button onClick={signOut}>Sign Out</button>
</Layout>
)
}
Add the sign in to the login.tsx page:
import { Layout } from '@/components/routes/Layout'
import { useAuth } from '@/hooks/auth'
export default function Login() {
const { signIn } = useAuth()
return (
<Layout title="Login">
<h1>Login</h1>
<button onClick={signIn}>Sign In</button>
</Layout>
)
}
Add the changeRole functionality in the profile.tsx page:
import { useCallback } from 'react'
import { Layout } from '@/components/routes/Layout'
import { supabase } from '@/db/client'
import { useAuth } from '@/hooks/auth'
export default function Profile() {
const { signOut, user } = useAuth()
const role = user?.role === 'EMPLOYEE' ? 'ADMIN' : 'EMPLOYEE'
const changeRole = useCallback(async () => {
await supabase.from('profiles').update({ role })
}, [role])
return (
<Layout title="Profile">
<h1>Profile</h1>
<button onClick={signOut}>Sign Out</button>
<button onClick={changeRole}>Change my user role to {role}</button>
</Layout>
)
}
Add ComponentShield in the pricing page:
import { ComponentShield } from 'next-shield'
import { Layout } from '@/components/routes/Layout'
import { useAuth } from '@/hooks/auth'
export default function Pricing() {
const { user, isAuth, isLoading } = useAuth()
return (
<Layout title="Pricing">
<h1>Pricing</h1>
<ComponentShield showIf={isAuth && !isLoading}>
<p>You are authenticated</p>
</ComponentShield>
<ComponentShield RBAC showForRole="ADMIN" userRole={user?.role}>
<p>You are an ADMIN</p>
</ComponentShield>
<ComponentShield RBAC showForRole="EMPLOYEE" userRole={user?.role}>
<p>You are an EMPLOYEE</p>
</ComponentShield>
</Layout>
)
}
Go to the Shield.tsx component, import the useAuth hook, and replace the values.
const { user, isAuth, isLoading } = useAuth()
const shieldProps: NextShieldProps<
['/profile', '/dashboard', '/users', '/users/[id]'],
['/', '/login']
> = {
router,
isAuth,
isLoading,
privateRoutes: ['/profile', '/dashboard', '/users', '/users/[id]'],
publicRoutes: ['/', '/login'],
hybridRoutes: ['/pricing'],
loginRoute: '/login',
LoadingComponent: <Loading />,
RBAC: {
ADMIN: {
grantedRoutes: ['/dashboard', '/profile', '/users', '/users/[id]'],
accessRoute: '/dashboard',
},
EMPLOYEE: {
grantedRoutes: ['/profile', '/dashboard'],
accessRoute: '/profile',
},
},
userRole: user?.role, // Must be undefined when isAuth is false & defined when is true
}
As an unauthenticated user, you only can access public and hybrid routes:

And when you're authenticated, you only can access hybrid and private routes where you have granted access:

That's it, really simple, you can check the github repo to download this example, and also don't forget to read the docs.

Hope you like it, I'm going to publish an example per week with the most popular auth providers (Clerk, Auth0, etc.), so if you don't wanna miss any article please follow me ;D.
