Bisali Bisali
Mongolo-Mongolo
Supabase na lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa lisa
Supabase na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na nafine-grained permissionsmisi na mtumiaji na data
You may want to restrict actions like editing or deleting data to resource owners, prevent users from voting on their own content, or enforce different permissions for different user roles.
This tutorial walks through how to implement Supabase authentication and authorization in a Next.js application.
Supabase authentication and authorizationNext.jsWe'll start with Supabase Auth for login and session management, then add authorization rules using Relationship-Based Access Control (ReBAC), enforced through Supabase Edge Functions and a local Policy Decision Point (PDP).
Supabase AuthmisoRelationship-Based Access Control (ReBAC)Supabase Edge Functionslocal Policy Decision Point (PDP)
By the end, you’ll have a real-time collaborative polling app that supports both public and protected actions—and a flexible authorization system you can evolve as your app grows.
What We’re Building
Nde ya motuna, s'azinga app ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna yaSupabaseNext.js
Let's app na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na naSupabase Authmiso
Metaboliki ya Supabase->Auth, Postgres, RLS, Realtime, na Edge Functions-yaki na Relationship-Based Access Control (ReBAC) modela ekomisa miso ya miso ya miso ya miso ya miso ya miso ya miso.
TangoPostgresRLSNtanetiEdge FunctionsRelationship-Based Access Control (ReBAC)Tech Stack
- Supabase – Backend-as-a-service for database, authentication, realtime, and edge functions
- Next.js – Frontend framework for building the app UI and API routes
- Permit.io – (for ReBAC) to define and evaluate authorization logic via PDP
- Supabase CLI – To manage and deploy Edge Functions locally and in production
Bisáleli
- Node.js installed
- Supabase account
- Permit.io account
- Familiarity with React/Next.js
- Starter project repo
What Can This App Do?
The demo application is a real-time polling platform built with Next.js and Supabase, where users can create polls and vote on others.
- Any user (authenticated or not) can view the list of public polls
- Only authenticated users can create polls and vote
- A user cannot vote on a poll they created
- Only the creator of a poll can edit or delete it
Tutorial Tango
We’ll follow these general steps:
- Set up Supabase project, schema, auth, and RLS
- Build core app features like poll creation and voting
- Model authorization rules define roles and rules in Permit.io
- Create Supabase Edge Functions for syncing users, assigning roles, and checking permissions
- Enforce policies in the app frontend using those edge functions
Let's get started -
Setting up Supabase in the Project
Setting up Supabase in the ProjectOptional: Clone the Starter Template
I've already created a starter template on GitHub with all the code you need to start so we can focus on implementing Supabase and Permit.io.
starter templateKitHub
You can clone the project by running the following command:
git clone <https://github.com/permitio/supabase-fine-grained-authorization>
Komi ya boteyi, boteyi na boteyi na boteyi na boteyi na boteyi na boteyi:
cd realtime-polling-app-nextjs-supabase-permitio
npm install
cd realtime-polling-app-nextjs-supabase-permitio
npm install
Creating a new Project in Supabase
To get started:
-
Go to https://supabase.com and sign in or create an account.
-
Click "New Project" and fill in your project name, password, and region.
-
Once created, go to Project Settings → API and note your Project URL and Anon Key — you’ll need them later.
Go to https://supabase.com and sign in or create an account.
Go to https://supabase.com and sign in or create an account.
https://supabase.comClick "New Project" and fill in your project name, password, and region.
Click "New Project" and fill in your project name, password, and region.
"Motobaz"Once created, go to Project Settings → API and note your Project URL and Anon Key — you’ll need them later.
Once created, go to Project Settings → API and note your Project URL and Anon Key — you’ll need them later.
Project Settings → APIProject URLAnon Key
Nkinga Authentication na Database na Supabase
We’ll use Supabase’s built-in email/password auth:
-
In the sidebar, go to Authentication → Providers
-
Enable the Email provider
-
(Optional) Disable email confirmation for testing, but keep it enabled for production
Na boti na boti na boti na boti na boti na boti na boti na boti
Ndi na boti na boti na boti na boti na boti na boti na boti na boti na botiAuthentication → Providers
Ndimi na Email na yo
Enable the Email provider
Email(Optional) Kofutela e-mail ya konfirmation ya motuna, mpe o komona ya motuna ya motuna
(Optional) Kofutela e-mail ya konfirmation ya motuna, mpe o komona ya motuna ya motuna ya motuna
Creating the Database Schema
This app uses three main tables: polls
, options
, and votes
. Use the SQL Editor in the Supabase dashboard and run the following:
polls
options
votes
SQL Editor
-- Create a polls table
CREATE TABLE polls (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
question TEXT NOT NULL,
created_by UUID REFERENCES auth.users(id) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
creator_name TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
);
-- Create an options table
CREATE TABLE options (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
poll_id UUID REFERENCES polls(id) ON DELETE CASCADE,
text TEXT NOT NULL,
);
-- Create a votes table
CREATE TABLE votes (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
poll_id UUID REFERENCES polls(id) ON DELETE CASCADE,
option_id UUID REFERENCES options(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
UNIQUE(poll_id, user_id)
);
-- Create a polls table
CREATE TABLE polls (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
question TEXT NOT NULL,
created_by UUID REFERENCES auth.users(id) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
creator_name TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
);
-- Create an options table
CREATE TABLE options (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
poll_id UUID REFERENCES polls(id) ON DELETE CASCADE,
text TEXT NOT NULL,
);
-- Create a votes table
CREATE TABLE votes (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
poll_id UUID REFERENCES polls(id) ON DELETE CASCADE,
option_id UUID REFERENCES options(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()),
UNIQUE(poll_id, user_id)
);
Enabling Row Level Security (RLS)
Enable RLS for each table and define policies:
RLS-- Polls politics ALTER TABLE polls ENABLE ROW LEVEL SECURITY; CREATE POLICY "Anyone can view polls" ON polls FOR SELECT USING (true); CREATE POLICY "Anyone can view options" ON options FOR SELECT USING (true); CREATE POLICY "Poll creators can add options" ON OPTIONS FOR INSERT TO AUTHENTIZED WITH CHECK (auth.uid() = created_by); -- Options POLICY ALTER TABLE options ENABLE ROW LEVEL SECURITY (EXISTS (SELECT 1 FROM polls WHERE votes idERE = options.pollid_ECECT AND created auth_by auth.-- Polls policies
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view polls" ON polls
FOR SELECT USING (true);
CREATE POLICY "Authenticated users can create polls" ON polls
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = created_by);
-- Options policies
ALTER TABLE options ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view options" ON options
FOR SELECT USING (true);
CREATE POLICY "Poll creators can add options" ON options
FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM polls
WHERE id = options.poll_id
AND created_by = auth.uid()
)
);
-- Votes policies
ALTER TABLE votes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view votes" ON votes
FOR SELECT USING (true);
CREATE POLICY "Authenticated users can vote once" ON votes
FOR INSERT TO authenticated
WITH CHECK (
auth.uid() = user_id AND
NOT EXISTS (
SELECT 1 FROM polls
WHERE id = votes.poll_id
AND created_by = auth.uid()
)
);
To use Supabase’s real-time features:
-
In the sidebar, go to Table Editor
-
For each of the three tables (polls
, options
, votes
):
-
Click the three dots → Edit Table
-
Toggle "Enable Realtime"
-
Save changes
In the sidebar, go to Table Editor
In the sidebar, go to Table Editor
Table Editor
For each of the three tables (polls
, options
, votes
):
-
Click the three dots → Edit Table
-
Toggle "Enable Realtime"
-
Save changes
For each of the three tables (polls
, options
, votes
):
polls
options
votes
-
Click the three dots → Edit Table
-
Toggle "Enable Realtime"
-
Save changes
Kisa ba pound → Edit Table
Click the three dots → Edit Table
Edit Table
Toggle "Enable Realtime"
Toggle "Ndimi"
"Enable Realtime"
Save changes
Save changes
Implementing Supabase Email Authentication in the App
Nde ya demo app, na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na.env.local
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
Ndimi ya nongo ya nongo ya nongo ya nongo ya nongo na nongo ya nongo ya nongo ya nongo ya nongo ya nongo ya nongo ya nongo ya nongo :
import { useState } daga "react"; import { createClient } daga "@/utils/supabase/komponent"; const LogInButton = () => { const supabase = createClient(); async function logIn() { const { error } = await supabase.auth.signUp email({ data: user_name: userName, }, }); if (error) { setError(error.message); } else {ShowModal(false); } } async function signUp() { const error } { const { } = await supabase.auth.signUp email({ error.auth.sign}, default email, { data: user_name, password, }; } (error)import { useState } from "react";
import { createClient } from "@/utils/supabase/component";
const LogInButton = () => {
const supabase = createClient();
async function logIn() {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
} else {
setShowModal(false);
}
}
async function signUp() {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
user_name: userName,
},
},
});
if (error) {
setError(error.message);
} else {
setShowModal(false);
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (isLogin) {
await logIn();
} else {
await signUp();
}
};
return (
<>
<button
onClick={() => setShowModal(true)}
className="flex items-center gap-2 p-2 bg-gray-800 text-white rounded-md">
Log In
</button>
...
</>
);
};
export default LogInButton;
Here, we are using Supabase’s signInWithPassword
method to log in a user and the signUp
method to sign up a new user with their email and password. We are also storing the user's name in the user_name
field in the user's metadata.
signInWithPassword
signUp
user_name
Bambisani mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbongo mbsupabase.auth.signOut()
imporite { createClient } daga "@/utils/supabase/component"; import {useRouter } daga "next/router"; const LogOutButton = ({ closeDropdown }: { closeDropdown: () => void }) => { const router = useRouter(); const supabase = createClient(); const handleLogOut = async () => { await supabase.auth.signOut(); closeDropdown(); router.push("/"}); back ( ... ); } export default LogOutButton;
import { createClient } from "@/utils/supabase/component";
import { useRouter } from "next/router";
const LogOutButton = ({ closeDropdown }: { closeDropdown: () => void }) => {
const router = useRouter();
const supabase = createClient();
const handleLogOut = async () => {
await supabase.auth.signOut();
closeDropdown();
router.push("/");
};
return (
...
);
};
export default LogOutButton;
Here, we are using the signOut
method from Supabase to log out the user and redirect them to the home page.
signOut
Misa ya motuna ya motuna ya motuna ya motuna
Nkinga miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso.
- Show/hide UI elements like login/logout buttons
- Conditionally restrict access to protected pages (like voting or managing polls)
- Ensure only authenticated users can perform restricted actions
Show/hide elementi UI kama login/logout buttons Komi ya ba motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna ya motuna Ensure only authenticated users can perform restricted actions
Na na na na na na na na na na na na na na na na na na na na na na na na na na na na na na nasupabase.auth.onAuthStateChange()
In the Layout.tsx
file: Track Global Auth State
In theLayout.tsx
file: Track Global Auth State
import React, { usEffect, useState } daga "react"; import { createClient } daga "@/utils/supabase/component"; import { User } daga "@supabase/supabase-js"; const Layout = ({ watoto }: { watoto: React.ReactNode( )); { data = supabase.authon.AuthonStateChange((event, session) => usEffect(() => { const fetchUser = async () => { const supabase = createClient(); { data = supabase.authon.AuthonStateChange((event, session))) => { set(Useression?.user.Pitch null); }import React, { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const Layout = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchUser = async () => {
const supabase = createClient();
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
);
};
export default Layout;
Restrict Access on Protected Pages
Kitamba mbongo mbongo na mbongo mbongo, pe-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-popoll detailspoll management
Here’s how it looks in pages/polls/[id].tsx
:
pages/polls/[id].tsx
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const Page = () => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchUser = async () => {
const supabase = createClient();
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
setLoading(false);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
);
export default Page;
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const Page = () => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchUser = async () => {
const supabase = createClient();
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
setLoading(false);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
);
export default Page;
And a similar pattern applies in pages/polls/manage.tsx
, where users should only see their own polls if logged in:
pages/polls/manage.tsx
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const Page = () => {
const [user, setUser] = useState<User | null>(null);
const supabase = createClient();
useEffect(() => {
const fetchUser = async () => {
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
if (!session?.user) {
setLoading(false);
}
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
);
};
export default Page;
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const Page = () => {
const [user, setUser] = useState<User | null>(null);
const supabase = createClient();
useEffect(() => {
const fetchUser = async () => {
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
if (!session?.user) {
setLoading(false);
}
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
);
};
export default Page;
Ndiyo miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso. Miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso.
user
checkPermission
Ntlalo ya Polling App
With Supabase configured and authentication working, we can now build the core functionality of the polling app. In this section, we’ll cover:
- Creating new polls
- Fetching and displaying polls in real-time
- Implementing a voting system
Creating new polls Fetching and displaying polls in real-time Moke ya motuna
Eto na na na na na na na na na na na na na na na na na na na na na na na na na
Creating New Polls
Users must be logged in to create polls. Each poll includes a question, an expiration date, and a set of options. We also record who created the poll so we can later use that relationship for access control.
Inside NewPoll.tsx
, fetch the authenticated user, and use Supabase to insert the poll and its options:
NewPoll.tsx
import React, { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const NewPoll = () => {
const [user, setUser] = useState<User | null>(null);
const supabase = createClient();
useEffect(() => {
const fetchUser = async () => {
const supabase = createClient();
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
setErrorMessage("Please provide a question and at least two options.");
return;
}
// Create the poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({
question,
expires_at: new Date(expiryDate).toISOString(),
created_by: user?.id,
creator_name: user?.user_metadata?.user_name,
})
.select()
.single();
if (pollError) {
console.error("Error creating poll:", pollError);
setErrorMessage(pollError.message);
return;
}
// Create the options
const { error: optionsError } = await supabase.from("options").insert(
options
.filter(opt => opt.trim())
.map(text => ({
poll_id: poll.id,
text,
}))
);
if (!optionsError) {
setSuccessMessage("Poll created successfully!");
handleCancel();
} else {
console.error("Error creating options:", optionsError);
setErrorMessage(optionsError.message);
}
};
return (
...
);
};
export default NewPoll;
import React, { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const NewPoll = () => {
const [user, setUser] = useState<User | null>(null);
const supabase = createClient();
useEffect(() => {
const fetchUser = async () => {
const supabase = createClient();
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
setErrorMessage("Please provide a question and at least two options.");
return;
}
// Create the poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({
question,
expires_at: new Date(expiryDate).toISOString(),
created_by: user?.id,
creator_name: user?.user_metadata?.user_name,
})
.select()
.single();
if (pollError) {
console.error("Error creating poll:", pollError);
setErrorMessage(pollError.message);
return;
}
// Create the options
const { error: optionsError } = await supabase.from("options").insert(
options
.filter(opt => opt.trim())
.map(text => ({
poll_id: poll.id,
text,
}))
);
if (!optionsError) {
setSuccessMessage("Poll created successfully!");
handleCancel();
} else {
console.error("Error creating options:", optionsError);
setErrorMessage(optionsError.message);
}
};
return (
...
);
};
export default NewPoll;
We’ll later call an Edge Function here to assign the “creator” role in Permit.io.
Fetching and Displaying Polls
Polls are divided into active (not yet expired) and past (expired). You can fetch them using Supabase queries filtered by the current timestamp, and set up real-time subscriptions to reflect changes instantly.
activemoko
Bomba ya pages/index.tsx
:
pages/index.tsx
ETENI YA 10 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YA 12 - ETENI YAimport { PollProps } from "@/helpers";
import { createClient } from "@/utils/supabase/component";
export default function Home() {
const supabase = createClient();
useEffect(() => {
const fetchPolls = async () => {
setLoading(true);
const now = new Date().toISOString();
try {
// Fetch active polls
const { data: activePolls, error: activeError } = await supabase
.from("polls")
.select(
`
id,
question,
expires_at,
creator_name,
created_by,
votes (count)
`
)
.gte("expires_at", now)
.order("created_at", { ascending: false });
if (activeError) {
console.error("Error fetching active polls:", activeError);
return;
}
// Fetch past polls
const { data: expiredPolls, error: pastError } = await supabase
.from("polls")
.select(
`
id,
question,
expires_at,
creator_name,
created_by,
votes (count)
`
)
.lt("expires_at", now)
.order("created_at", { ascending: false });
if (pastError) {
console.error("Error fetching past polls:", pastError);
return;
}
setCurrentPolls(activePolls);
setPastPolls(expiredPolls);
} catch (error) {
console.error("Unexpected error fetching polls:", error);
} finally {
setLoading(false);
}
};
fetchPolls();
// Set up real-time subscription on the polls table:
const channel = supabase
.channel("polls")
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "polls",
},
fetchPolls
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
return (
...
);
}
Viewing and Managing User Polls
Here, we are fetching active and past polls from the polls
table in Supabase. We are also setting up a real-time subscription to listen for changes in the polls
table so that we can update the UI with the latest poll data. To differentiate between active and past polls, we are comparing the expiry date of each poll with the current date.
polls
polls
Update the pages/manage.tsx
page to fetch and display only polls created by the user:
pages/manage.tsx
imporite { PollProps } daga "@/polls"; const Page = () => { useEffect(() => { if (!user?.id) return; const fetchPolls = async () => { try { const_at", { ascending: error } = await supabase .from("polls") .select( `id, question, expires_at, creator_name, created_by, votes (count) `) .eq("created_by", user.id) .order("created_at", { ascending: error }); if (error) default.default.default("Error fetchannel:", error); return; } setPolls filter(dataimport { PollProps } from "@/helpers";
const Page = () => {
useEffect(() => {
if (!user?.id) return;
const fetchPolls = async () => {
try {
const { data, error } = await supabase
.from("polls")
.select(
`
id,
question,
expires_at,
creator_name,
created_by,
votes (count)
`
)
.eq("created_by", user.id)
.order("created_at", { ascending: false });
if (error) {
console.error("Error fetching polls:", error);
return;
}
setPolls(data || []);
} catch (error) {
console.error("Unexpected error fetching polls:", error);
} finally {
setLoading(false);
}
};
fetchPolls();
// Set up real-time subscription
const channel = supabase
.channel(`polls_${user.id}`)
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "polls",
filter: `created_by=eq.${user.id}`,
},
fetchPolls
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user]);
return (
...
);
};
export default Page;
Here, we only fetch polls created by the user and listen for real-time updates in the polls
table so that the UI is updated with the latest poll data.
polls
Bambis, miso PollCard
component ya te, ba na pe-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-po-poPollCard
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const PollCard = ({ poll }: { poll: PollProps }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const supabase = createClient();
const fetchUser = async () => {
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
setLoading(false);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
)}
</Link>
);
};
export default PollCard;
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const PollCard = ({ poll }: { poll: PollProps }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const supabase = createClient();
const fetchUser = async () => {
const { data } = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
setLoading(false);
});
return () => {
data.subscription.unsubscribe();
};
};
fetchUser();
}, []);
return (
...
)}
</Link>
);
};
export default PollCard;
Kindi, na katakolon ya katakolon, mpe na katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya katakolon ya kat
Implementing the Poll Voting System
The voting logic enforces:
- Only one vote per user per poll
- Creators cannot vote on their own polls
- Votes are stored in the
votes
table
- Results are displayed and updated in real time
Komi ya libosó Creators cannot vote on their own polls Votes are stored in the votes
table votes
Misato ya misato ya misato ya misato ya misato ya misato
Let's bounce miso ya miso ya miso na ViewPoll.tsx
miso:
ViewPoll.tsx
Fetch the Logged-In UserWe need the current user’s ID to determine voting eligibility and record their vote.
Fetch the Logged-In User
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const ViewPoll = () => {
const [user, setUser] = useState<User | null>(null);
const supabase = createClient();
useEffect(()
const fetchUser = async () => {
const {
data: { user },
} = await supabase.auth.getUser();
setUser(user);
};
fetchUser();
}, []);
import { createClient } from "@/utils/supabase/component";
import { User } from "@supabase/supabase-js";
const ViewPoll = () => {
const [user, setUser] = useState<User | null>(null);
const supabase = createClient();
useEffect(()
const fetchUser = async () => {
const {
data: { user },
} = await supabase.auth.getUser();
setUser(user);
};
fetchUser();
}, []);
Load Poll Details and Check Voting StatusOnce we have the user, we fetch:
Layimeti ya Poll na Tango ya Poto
- The poll itself (including options and vote counts)
- Whether this user has already voted
The poll itself (including options and vote counts) Whether this user has already voted
We also call these again later in real-time updates.
useEffect(() => {
if (!user) {
return;
}
const checkUserVote = async () => {
const { data: votes } = await supabase
.from("votes")
.select("id")
.eq("poll_id", query.id)
.eq("user_id", user.id)
.single();
setHasVoted(!!votes);
setVoteLoading(false);
};
const fetchPoll = async () => {
const { data } = await supabase
.from("polls")
.select(
`
*,
options (
id,
text,
votes (count)
)
`
)
.eq("id", query.id)
.single();
setPoll(data);
setPollLoading(false);
checkUserVote();
};
fetchPoll();
useEffect(() => {
if (!user) {
return;
}
const checkUserVote = async () => {
const { data: votes } = await supabase
.from("votes")
.select("id")
.eq("poll_id", query.id)
.eq("user_id", user.id)
.single();
setHasVoted(!!votes);
setVoteLoading(false);
};
const fetchPoll = async () => {
const { data } = await supabase
.from("polls")
.select(
`
*,
options (
id,
text,
votes (count)
)
`
)
.eq("id", query.id)
.single();
setPoll(data);
setPollLoading(false);
checkUserVote();
};
fetchPoll();
Listen for Real-Time Updates
Listen for Real-Time UpdatesWe subscribe to changes in the votes
table, scoped to this poll. When a new vote is cast, we fetch updated poll data and voting status.
votes
const channel = supabase
.channel(`poll-${query.id}`)
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "votes",
filter: `poll_id=eq.${query.id}`,
},
() => {
fetchPoll();
checkUserVote();
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [query.id, user]);
const channel = supabase
.channel(`poll-${query.id}`)
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "votes",
filter: `poll_id=eq.${query.id}`,
},
() => {
fetchPoll();
checkUserVote();
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [query.id, user]);
Ntloko ya libosó
Handle the Vote SubmissionIf the user hasn’t voted and is allowed to vote (we’ll add a permission check later), we insert their vote.
const handleVote = async (optionId: string) => { kama (!user) reverse; try { const { error } = await supabase.from("votes").insert({ poll_id: query.id, option_id: optionId, user_id: user.id, }); na (!error) { setHasVoted(true); } } catch (error) { console.error("Error voting:", error); };
const handleVote = async (optionId: string) => {
if (!user) return;
try {
const { error } = await supabase.from("votes").insert({
poll_id: query.id,
option_id: optionId,
user_id: user.id,
});
if (!error) {
setHasVoted(true);
}
} catch (error) {
console.error("Error voting:", error);
}
};
Ntlalo ya Poll RezultatNtlalo ya kopona ya mikolo ya mikolo ya mikolo ya mikolo ya mikolo ya mikolo ya mikolo ya mikolo ya mikolo.Display the Poll Results
if (!poll || pollLoading || voteLoading) return <div>Loading...</div>;
// 6. calculate total votes
const totalVotes = calculateTotalVotes(poll.options);
const countdown = getCountdown(poll.expires_at);
return (
...
);
};
export default ViewPoll;
if (!poll || pollLoading || voteLoading) return <div>Loading...</div>;
// 6. calculate total votes
const totalVotes = calculateTotalVotes(poll.options);
const countdown = getCountdown(poll.expires_at);
return (
...
);
};
export default ViewPoll;
With this setup in place, your voting system is fully functional. But right now, anyone logged in could technically try to vote—even on their own poll. Next, we’ll add authorization checks using Permit.io and Supabase Edge Functions to enforce those rules.
kontrola ya libosóPermit.ioSupabase Edge Functions
Before we do that, let’s first look at the type of authorization layer we are going to implement.
Understanding ReBAC (Relationship-Based Access Control)
Supabase handles authentication and basic row-level permissions well, but it doesn’t support complex rules like:
- Preventing users from voting on their own polls
- Assigning per-resource roles (like “creator” for a specific poll)
- Managing access via external policies
Preventing users from voting on their own polls Assigning per-resource roles (like “creator” for a specific poll) Managing access via external policies
To support these kinds of relationship-based permissions, we’ll implement ReBAC with Permit.io.
Relationship-Based Access Control (ReBAC) is a model for managing permissions based on the relationships between users and resources. Instead of relying solely on roles or attributes (as in RBAC or ABAC), ReBAC determines access by evaluating how a user is connected to the resource they’re trying to access.
Relationship-Based Access Control (ReBAC)Relationship-Based Access Control (ReBAC)
In this tutorial, we apply ReBAC to a polling app:
- A user who created a poll is the only one who can manage (edit/delete) it
- A user cannot vote on their own poll
- Other authenticated users can vote once per poll
A user who created a poll is the only one who can manage (edit/delete) it misoA user cannot vote on their own poll cannotownOther authenticated users can vote once per poll
By modeling these relationships in Permit.io, we can define fine-grained access rules that go beyond Supabase’s built-in Row Level Security (RLS). We’ll enforce them at runtime using Supabase Edge Functions and Permit’s policy engine.
For more on ReBAC, check out Permit.io’s ReBAC docs.
Permit.io’s ReBAC docsAccess Control Design
Na na na na na na na na na na na na na
- One resource with resource-specific actions:
- polls:
create
, read
, delete
, update
.
- Two roles for granting permission levels based on a user’s relationship with the resources:
- authenticated: Can perform
create
and read
actions in polls. Can not delete
, or update
actions in polls.
- creator: Can
create
, read
, delete
, and update
actions in polls. Can perform read
and create
actions in votes. Cannot use create
on their own polls.
One resource with resource-specific actions:
- polls:
create
, read
, delete
, update
.
- polls:
create
, read
, delete
, update
.
polls: create
, read
, delete
, update
. pollscreate
read
delete
update
Two roles for granting permission levels based on a user’s relationship with the resources:
- authenticated: Can perform
create
and read
actions in polls. Can not delete
, or update
actions in polls.
- creator: Can
create
, read
, delete
, and update
actions in polls. Can perform read
and create
actions in votes. Cannot use create
on their own polls.
- authenticated: Can perform
create
and read
actions in polls. Can not delete
, or update
actions in polls.
- creator: Can
create
, read
, delete
, and update
actions in polls. Can perform read
and create
actions in votes. Cannot use create
on their own polls.
authenticated: Can perform create
and read
actions in polls. Can not delete
, or update
actions in polls. authenticated:create
read
delete
update
creator: Can create
, read
, delete
, and update
actions in polls. Can perform read
and create
actions in votes. Cannot use create
on their own polls. creator:create
read
delete
update
read
create
create
Setting up Permit.io
Let's go through setting up na model na autorisation na Permit.
- Create a new project in Permit.io
- Name it something like
supabase-polling
- Define the
polls
resource
- Go to the Policy → Resources tab
- Click “Create Resource”
- Name it
polls
, and add the actions: read
, create
, update
, delete
- Enable ReBAC for the resource
-
Under “ReBAC Options,” define the following roles:
authenticated
creator
-
Click Save
Create a new project in Permit.io
- Name it something like
supabase-polling
Create a new projectCreate a new project in Permit.io
- Name it something like
supabase-polling
Name it something like supabase-polling
supabase-polling
Ntlali polls
rezource - Ntlali Policy → Resources tab
- Kliki "Create Resource"
- Ntlali
polls
, yéti ya miso: read
, create
, update
, delete
Ndisi yapolls
resource
- Go to the Policy → Resources tab
- Click “Create Resource”
- Name it
polls
, and add the actions: read
, create
, update
, delete
Na na Policy → Resources tab Policy → ResourcesClick “Create Resource” “Create Resource”Name it polls
, and add the actions: read
, create
, update
, delete
polls
read
create
update
delete
Enable ReBAC for the resource
-
Under “ReBAC Options,” define the following roles:
authenticated
creator
-
Click Save
Enable ReBAC for the resource
-
Under “ReBAC Options,” define the following roles:
authenticated
creator
-
Click Save
Under “ReBAC Options,” define the following roles:
authenticated
creator
Under “ReBAC Options,” define the following roles:
authenticated
creator
moko
authenticated
creator
creator
Click Save
Click Save
Save
Navigate to the "Roles" tab to view the roles from the resources we just created. Note that Permit created the default roles (admin
, editor
, user
) that are unnecessary for this tutorial.
admin
editor
user
-
Define access policies
- Go to the Policy → Policies tab
- Use the visual matrix to define:
-
authenticated
can read
and create
polls
-
creator
can read
, update
, and delete
polls
-
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
-
Add resource instances
-
Go to Directory → Instances
-
Add individual poll IDs as resource instances (you’ll automate this later when new polls are created)
-
Assign roles to users per poll (e.g. user123
is creator
of poll456
)
Define access policies
- Go to the Policy → Policies tab
- Use the visual matrix to define:
-
authenticated
can read
and create
polls
-
creator
can read
, update
, and delete
polls
-
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
Ntloko ya makolinhot
Define access policies
- Go to the Policy → Policies tab
- Use the visual matrix to define:
-
authenticated
can read
and create
polls
-
creator
can read
, update
, and delete
polls
-
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
Go to the Policy → Policies tab Policy → PoliciesUse the visual matrix to define:
-
authenticated
can read
and create
polls
-
creator
can read
, update
, and delete
polls
-
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
-
authenticated
can read
and create
polls
-
creator
can read
, update
, and delete
polls
-
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
authenticated
can read
and create
polls
authenticated
can read
and create
polls
authenticated
read
create
creator
can read
, update
, and delete
polls
creator
can read
, update
, and delete
polls
creator
read
update
delete
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
(Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
Add resource instances
-
Go to Directory → Instances
-
Add individual poll IDs as resource instances (you’ll automate this later when new polls are created)
-
Assign roles to users per poll (e.g. user123
is creator
of poll456
)
Add resource instances
Add resource instances -
Ntlali na Directory → Instances
-
Ntlali na miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso
-
Go to Directory → Instances
Ntlali na Ntlali → Instances
Directory → Instances-
Add individual poll IDs as resource instances (you’ll automate this later when new polls are created)
Add individual poll IDs as resource instances (you’ll automate this later when new polls are created)
-
Assign roles to users per poll (e.g. user123
is creator
of poll456
)
Assign roles to users per poll (e.g. user123
is creator
of poll456
)
user123
creator
poll456
This structure gives us the power to write flexible access rules and enforce them per user, per poll.
Now that we have completed the initial setup on the Permit dashboard, let's use it in our application. Next, we’ll connect Permit.io to our Supabase project via Edge Functions that:
Permit.io - Sync na na na na na na na na na na na na na na
- Sync new users
- Assign creator roles
- Ntloko ya libosó
Setting up Permit in the Polling Application
Setting up Permit in the Polling ApplicationPermit offers multiple ways to integrate with your application, but we'll use the Container PDP for this tutorial. You have to host the container online to access it in Supabase Edge functions. You can use services like railway.com. Once you have hosted it, save the url for your container.
railway.com
Obtain your Permit API key by clicking "Projects" in the Permit dashboard sidebar, navigating to the project you are working on, clicking the three dots, and selecting "Copy API Key".
Creating Supabase Edge Function APIs for Authorization
Supabase Edge Functions are perfect for integrating third-party services like Permit.io. We’ll use them to enforce our ReBAC rules at runtime by checking whether users are allowed to perform specific actions on polls.
Supabase Edge FunctionsMisa ya miso na Supabase
Create Functions in SupabaseInitialise Supabase in your project and create three different functions using the supabase functions new
command. These will be the starting point for your functions:
supabase functions new
npx supabase init
npx supabase functions new syncUser
npx supabase functions new updateCreatorRole
npx supabase functions new checkPermission
npx supabase init
npx supabase functions new syncUser
npx supabase functions new updateCreatorRole
npx supabase functions new checkPermission
This will create a functions
folder in the supabase
folder along with the endpoints. Now, let’s write the codes for each endpoint.
functions
supabase
Syncing Users to Permit.io on Signup (syncUser.ts
)
syncUser.ts
This function listens for Supabase’s SIGNED_UP
auth event. When a new user signs up, we sync their identity to Permit.io and assign them the default authenticated
role.
SIGNED_UP
authenticated
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Headers': 'Authorization, x-client-info, apikey, Content-Type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
// Supabase Edge Function to sync new users with Permit.io
Deno.serve(async (req) => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { event, user } = await req.json();
// Only proceed if the event type is "SIGNED_UP"
if (event === "SIGNED_UP" && user) {
const newUser = {
key: user.id,
email: user.email,
name: user.user_metadata?.name || "Someone",
};
// Sync the user to Permit.io
await permit.api.createUser(newUser);
await permit.api.assignRole({
role: "authenticated",
tenant: "default",
user: user.id,
});
console.log(`User ${user.email} synced to Permit.io successfully.`);
}
// Return success response
return new Response(
JSON.stringify({ message: "User synced successfully!" }),
{ status: 200, headers: corsHeaders },
);
} catch (error) {
console.error("Error syncing user to Permit: ", error);
return new Response(
JSON.stringify({
message: "Error syncing user to Permit.",
"error": error
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
});
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Headers': 'Authorization, x-client-info, apikey, Content-Type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
// Supabase Edge Function to sync new users with Permit.io
Deno.serve(async (req) => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { event, user } = await req.json();
// Only proceed if the event type is "SIGNED_UP"
if (event === "SIGNED_UP" && user) {
const newUser = {
key: user.id,
email: user.email,
name: user.user_metadata?.name || "Someone",
};
// Sync the user to Permit.io
await permit.api.createUser(newUser);
await permit.api.assignRole({
role: "authenticated",
tenant: "default",
user: user.id,
});
console.log(`User ${user.email} synced to Permit.io successfully.`);
}
// Return success response
return new Response(
JSON.stringify({ message: "User synced successfully!" }),
{ status: 200, headers: corsHeaders },
);
} catch (error) {
console.error("Error syncing user to Permit: ", error);
return new Response(
JSON.stringify({
message: "Error syncing user to Permit.",
"error": error
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
});
Assigning the Creator Role (updateCreatorRole.ts
)
updateCreatorRole.ts
Komi ya mtumiaji ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi ya boteyi :
-
Sync the poll as a new Permit.io resource instance
-
Assign the user the creator
role for that poll
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Headers': 'Authorization, x-client-info, apikey, Content-Type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
Deno.serve(async (req) => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { userId, pollId } = await req.json();
// Validate input parameters
if (!userId || !pollId) {
return new Response(
JSON.stringify({ error: "Missing required parameters." }),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
// Sync the resource (poll) to Permit.io
await permit.api.syncResource({
type: "polls",
key: pollId,
tenant: "default",
attributes: {
createdBy: userId
}
});
// Assign the creator role to the user for this specific poll
await permit.api.assignRole({
role: "creator",
tenant: "default",
user: userId,
resource: {
type: "polls",
key: pollId,
}
});
return new Response(
JSON.stringify({
message: "Creator role assigned successfully",
success: true
}),
{ status: 200, headers: corsHeaders },
);
} catch (error) {
console.error("Error assigning creator role: ", error);
return new Response(
JSON.stringify({
message: "Error occurred while assigning creator role.",
error: error
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
});
Sync the poll as a new Permit.io resource instance
Sync the poll as a new Permit.io resource instance
Assign the user the creator
role for that poll
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Headers': 'Authorization, x-client-info, apikey, Content-Type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
Deno.serve(async (req) => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { userId, pollId } = await req.json();
// Validate input parameters
if (!userId || !pollId) {
return new Response(
JSON.stringify({ error: "Missing required parameters." }),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
// Sync the resource (poll) to Permit.io
await permit.api.syncResource({
type: "polls",
key: pollId,
tenant: "default",
attributes: {
createdBy: userId
}
});
// Assign the creator role to the user for this specific poll
await permit.api.assignRole({
role: "creator",
tenant: "default",
user: userId,
resource: {
type: "polls",
key: pollId,
}
});
return new Response(
JSON.stringify({
message: "Creator role assigned successfully",
success: true
}),
{ status: 200, headers: corsHeaders },
);
} catch (error) {
console.error("Error assigning creator role: ", error);
return new Response(
JSON.stringify({
message: "Error occurred while assigning creator role.",
error: error
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
});
Assign the user the creator
role for that poll
creator
Eko ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye ye yeimport "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
'Access-Control-Allow-Origin': "*",
'Access-Control-Allow-Headers': 'Authorization, x-client-info, apikey, Content-Type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE',
}
Deno.serve(async (req) => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { userId, pollId } = await req.json();
// Validate input parameters
if (!userId || !pollId) {
return new Response(
JSON.stringify({ error: "Missing required parameters." }),
{ status: 400, headers: { "Content-Type": "application/json" } },
);
}
// Sync the resource (poll) to Permit.io
await permit.api.syncResource({
type: "polls",
key: pollId,
tenant: "default",
attributes: {
createdBy: userId
}
});
// Assign the creator role to the user for this specific poll
await permit.api.assignRole({
role: "creator",
tenant: "default",
user: userId,
resource: {
type: "polls",
key: pollId,
}
});
return new Response(
JSON.stringify({
message: "Creator role assigned successfully",
success: true
}),
{ status: 200, headers: corsHeaders },
);
} catch (error) {
console.error("Error assigning creator role: ", error);
return new Response(
JSON.stringify({
message: "Error occurred while assigning creator role.",
error: error
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
});
Checking Permissions (checkPermission.ts
)
checkPermission.ts
This function acts as the gatekeeper—it checks whether a user is allowed to perform a given action (create
, read
, update
, delete
) on a specific poll.
create
read
update
delete
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"Authorization, x-client-info, apikey, Content-Type",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE",
};
Deno.serve(async req => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { userId, operation, key } = await req.json();
// Validate input parameters
if (!userId || !operation || !key) {
return new Response(
JSON.stringify({ error: "Missing required parameters." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Check permissions using Permit's ReBAC
const permitted = await permit.check(userId, operation, {
type: "polls",
key,
tenant: "default",
// Include any additional attributes that Permit needs for relationship checking
attributes: {
createdBy: userId, // This will be used in Permit's policy rules
},
});
return new Response(JSON.stringify({ permitted }), {
status: 200,
headers: corsHeaders,
});
} catch (error) {
console.error("Error checking user permission: ", error);
return new Response(
JSON.stringify({
message: "Error occurred while checking user permission.",
error: error,
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
});
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { Permit } from "npm:permitio";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"Authorization, x-client-info, apikey, Content-Type",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE",
};
Deno.serve(async req => {
const permit = new Permit({
token: Deno.env.get("PERMIT_API_KEY"),
pdp: "<https://real-time-polling-app-production.up.railway.app>",
});
try {
const { userId, operation, key } = await req.json();
// Validate input parameters
if (!userId || !operation || !key) {
return new Response(
JSON.stringify({ error: "Missing required parameters." }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Check permissions using Permit's ReBAC
const permitted = await permit.check(userId, operation, {
type: "polls",
key,
tenant: "default",
// Include any additional attributes that Permit needs for relationship checking
attributes: {
createdBy: userId, // This will be used in Permit's policy rules
},
});
return new Response(JSON.stringify({ permitted }), {
status: 200,
headers: corsHeaders,
});
} catch (error) {
console.error("Error checking user permission: ", error);
return new Response(
JSON.stringify({
message: "Error occurred while checking user permission.",
error: error,
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
});
Local Testing
Start your Supabase dev server to test the functions locally:
npx supabase start
npx supabase functions serve
npx supabase start
npx supabase functions serve
You can then hit your functions at:
<http://localhost:54321/functions/v1/><function-name>
<http://localhost:54321/functions/v1/><function-name>
Tango:
<http://localhost:54321/functions/v1/checkPermission>
<http://localhost:54321/functions/v1/checkPermission>
Integrating Authorization Checks in the UI
Now that we’ve created our authorization logic with Permit.io and exposed it via Supabase Edge Functions, it’s time to enforce those checks inside the app’s components.
In this section, we’ll update key UI components to call those functions and conditionally allow or block user actions like voting or managing polls based on permission checks.
NewPoll.tsx
: Assign Creator Role After Poll Creation
NewPoll.tsx
After creating a poll and saving it to Supabase, we call the updateCreatorRole
function to:
updateCreatorRole
-
Sync the new poll as a resource in Permit.io
-
Assign the current user the creator
role for that specific poll
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
setErrorMessage("Please provide a question and at least two options.");
return;
}
try {
// Create the poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({
question,
expires_at: new Date(expiryDate).toISOString(),
created_by: user?.id,
creator_name: user?.user_metadata?.user_name,
})
.select()
.single();
if (pollError) {
console.error("Error creating poll:", pollError);
setErrorMessage(pollError.message);
return;
}
// Create the options
const { error: optionsError } = await supabase.from("options").insert(
options
.filter(opt => opt.trim())
.map(text => ({
poll_id: poll.id,
text,
}))
);
if (optionsError) {
console.error("Error creating options:", optionsError);
setErrorMessage(optionsError.message);
return;
}
// Update the creator role in Permit.io
const response = await fetch(
"<http://127.0.0.1:54321/functions/v1//updateCreatorRole>",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user?.id,
pollId: poll.id,
}),
}
);
const { success, error } = await response.json();
if (!success) {
console.error("Error updating creator role:", error);
// Note: We don't set an error message here as the poll was still created successfully
}
setSuccessMessage("Poll created successfully!");
handleCancel();
} catch (error) {
console.error("Error in poll creation process:", error);
setErrorMessage("An unexpected error occurred while creating the poll.");
}
};
Sync the new poll as a resource in Permit.io
Sync the new poll as a resource in Permit.io
Assign the current user the creator
role for that specific poll
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
setErrorMessage("Please provide a question and at least two options.");
return;
}
try {
// Create the poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({
question,
expires_at: new Date(expiryDate).toISOString(),
created_by: user?.id,
creator_name: user?.user_metadata?.user_name,
})
.select()
.single();
if (pollError) {
console.error("Error creating poll:", pollError);
setErrorMessage(pollError.message);
return;
}
// Create the options
const { error: optionsError } = await supabase.from("options").insert(
options
.filter(opt => opt.trim())
.map(text => ({
poll_id: poll.id,
text,
}))
);
if (optionsError) {
console.error("Error creating options:", optionsError);
setErrorMessage(optionsError.message);
return;
}
// Update the creator role in Permit.io
const response = await fetch(
"<http://127.0.0.1:54321/functions/v1//updateCreatorRole>",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user?.id,
pollId: poll.id,
}),
}
);
const { success, error } = await response.json();
if (!success) {
console.error("Error updating creator role:", error);
// Note: We don't set an error message here as the poll was still created successfully
}
setSuccessMessage("Poll created successfully!");
handleCancel();
} catch (error) {
console.error("Error in poll creation process:", error);
setErrorMessage("An unexpected error occurred while creating the poll.");
}
};
Ndima ya mtumiaji na creator
miso ya miso ya miso
creator
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
setErrorMessage("Please provide a question and at least two options.");
return;
}
try {
// Create the poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({
question,
expires_at: new Date(expiryDate).toISOString(),
created_by: user?.id,
creator_name: user?.user_metadata?.user_name,
})
.select()
.single();
if (pollError) {
console.error("Error creating poll:", pollError);
setErrorMessage(pollError.message);
return;
}
// Create the options
const { error: optionsError } = await supabase.from("options").insert(
options
.filter(opt => opt.trim())
.map(text => ({
poll_id: poll.id,
text,
}))
);
if (optionsError) {
console.error("Error creating options:", optionsError);
setErrorMessage(optionsError.message);
return;
}
// Update the creator role in Permit.io
const response = await fetch(
"<http://127.0.0.1:54321/functions/v1//updateCreatorRole>",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user?.id,
pollId: poll.id,
}),
}
);
const { success, error } = await response.json();
if (!success) {
console.error("Error updating creator role:", error);
// Note: We don't set an error message here as the poll was still created successfully
}
setSuccessMessage("Poll created successfully!");
handleCancel();
} catch (error) {
console.error("Error in poll creation process:", error);
setErrorMessage("An unexpected error occurred while creating the poll.");
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (question.trim() && options.filter(opt => opt.trim()).length < 2) {
setErrorMessage("Please provide a question and at least two options.");
return;
}
try {
// Create the poll
const { data: poll, error: pollError } = await supabase
.from("polls")
.insert({
question,
expires_at: new Date(expiryDate).toISOString(),
created_by: user?.id,
creator_name: user?.user_metadata?.user_name,
})
.select()
.single();
if (pollError) {
console.error("Error creating poll:", pollError);
setErrorMessage(pollError.message);
return;
}
// Create the options
const { error: optionsError } = await supabase.from("options").insert(
options
.filter(opt => opt.trim())
.map(text => ({
poll_id: poll.id,
text,
}))
);
if (optionsError) {
console.error("Error creating options:", optionsError);
setErrorMessage(optionsError.message);
return;
}
// Update the creator role in Permit.io
const response = await fetch(
"<http://127.0.0.1:54321/functions/v1//updateCreatorRole>",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user?.id,
pollId: poll.id,
}),
}
);
const { success, error } = await response.json();
if (!success) {
console.error("Error updating creator role:", error);
// Note: We don't set an error message here as the poll was still created successfully
}
setSuccessMessage("Poll created successfully!");
handleCancel();
} catch (error) {
console.error("Error in poll creation process:", error);
setErrorMessage("An unexpected error occurred while creating the poll.");
}
};
ViewPoll.tsx
: Restrict Voting Based on Permissions
ViewPoll.tsx
Before allowing a user to vote on a poll, we call the checkPermission
function to verify they have the create
permission on the votes
resource. This is how we enforce the rule: “A creator cannot vote on their own poll.”
checkPermission
create
votes
“A creator cannot vote on their own poll.”
Check voting permission:
Check voting permission:
const [canVote, setCanVote] = useState(false);
useEffect(() => {
const checkPermission = async () => {
if (!user || !query.id) return;
try {
const response = await fetch("<http://127.0.0.1:54321/functions/v1/checkPermission>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
operation: "create",
key: query.id,
}),
});
const { permitted } = await response.json();
setCanVote(permitted);
} catch (error) {
console.error("Error checking permission:", error);
setCanVote(false);
}
};
checkPermission();
}, [user, query.id]);
const [canVote, setCanVote] = useState(false);
useEffect(() => {
const checkPermission = async () => {
if (!user || !query.id) return;
try {
const response = await fetch("<http://127.0.0.1:54321/functions/v1/checkPermission>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
operation: "create",
key: query.id,
}),
});
const { permitted } = await response.json();
setCanVote(permitted);
} catch (error) {
console.error("Error checking permission:", error);
setCanVote(false);
}
};
checkPermission();
}, [user, query.id]);
Disable vote buttons if user isn’t allowed:
Bomba ya ba mbisi ya ba mbisi ya ba mbisi ya ba mbisi ya ba mbisi ya ba mbisi ya ba mbisi ya ba mbisi:
<button
onClick={() => handleVote(option.id)}
disabled={!user || !canVote}}
className="w-full text-left p-4 rounded-md hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{option.text}
</button>
<button
onClick={() => handleVote(option.id)}
disabled={!user || !canVote}}
className="w-full text-left p-4 rounded-md hover:bg-slate-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{option.text}
</button>
Tétiki ya ba mbisi ya ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi ba mbisi baNtlalo miso ya miso ya miso ya miso:
{user && !canVote && ( <p className="mt-4 text-gray-600">Bomba ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya moto ya{user && !canVote && (
<p className="mt-4 text-gray-600">You cannot vote on your own poll</p>
)}
PollCard.tsx
: Control Access to Edit/Delete
PollCard.tsx
We also restrict poll management actions (edit and delete) by checking if the user has the update
or delete
permission on that poll.
update
delete
Nkinga misato ya misato:
Check management permissions:
const [canManagePoll, setCanManagePoll] = useState(false);
useEffect(() => {
const checkPollPermissions = async () => {
if (!user || !poll.id) return;
try {
// Check for both edit and delete permissions
const [editResponse, deleteResponse] = await Promise.all([
fetch("<http://127.0.0.1:54321/functions/v1/checkPermission>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
operation: "update",
key: poll.id,
}),
}),
fetch("/api/checkPermission", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
operation: "delete",
key: poll.id,
}),
}),
]);
const [{ permitted: canEdit }, { permitted: canDelete }] =
await Promise.all([editResponse.json(), deleteResponse.json()]);
// User can manage poll if they have either edit or delete permission
setCanManagePoll(canEdit || canDelete);
} catch (error) {
console.error("Error checking permissions:", error);
setCanManagePoll(false);
}
};
checkPollPermissions();
}, [user, poll.id]);
const [canManagePoll, setCanManagePoll] = useState(false);
useEffect(() => {
const checkPollPermissions = async () => {
if (!user || !poll.id) return;
try {
// Check for both edit and delete permissions
const [editResponse, deleteResponse] = await Promise.all([
fetch("<http://127.0.0.1:54321/functions/v1/checkPermission>", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
operation: "update",
key: poll.id,
}),
}),
fetch("/api/checkPermission", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
operation: "delete",
key: poll.id,
}),
}),
]);
const [{ permitted: canEdit }, { permitted: canDelete }] =
await Promise.all([editResponse.json(), deleteResponse.json()]);
// User can manage poll if they have either edit or delete permission
setCanManagePoll(canEdit || canDelete);
} catch (error) {
console.error("Error checking permissions:", error);
setCanManagePoll(false);
}
};
checkPollPermissions();
}, [user, poll.id]);
Conditionally show management buttons:
Conditionally show management buttons:
Replace:
{user?.id === poll?.created_by && (
{user?.id === poll?.created_by && (
Na:
{canManagePoll && (
<div className="flex justify-start gap-4 mt-4">
<button type="button" onClick={handleEdit}>
</button>
<button type="button" onClick={handleDelete}>
</button>
</div>
)}
{canManagePoll && (
<div className="flex justify-start gap-4 mt-4">
<button type="button" onClick={handleEdit}>
</button>
<button type="button" onClick={handleDelete}>
</button>
</div>
)}
Testing the Integration
Once integrated, you should see the following behaviors in the app:
-
Logged-out users can view polls but not interact
-
Authenticated users can vote on polls they didn’t create
-
Creators cannot vote on their own polls
-
Only creators see edit/delete options on their polls
Misato ya misato ya misato ya misato ya misato ya misato
Logged-out users can view polls but not interact
Authenticated users can vote on polls they didn’t create
Authenticated users can vote on polls they didn’t create
didn’t
Creators cannot vote on their own polls
Creators cannot vote on their own polls
cannot vote
Only creators see edit/delete options on their polls
Only creators see edit/delete options on their polls
Edit/delete
You should be able to see the application's changes by going to the browser. On the home screen, users can view the list of active and past polls, whether they are logged in or not. However, when they click on a poll, they will not be able to view the poll details or vote on it. Instead, they will be prompted to log in.
Once logged in, the user can view the details of the poll and vote on it. However, if the user is the creator of the poll, they will not be able to vote on it. They will see a message indicating that they cannot vote on their own poll. They will also be allowed to manage any poll that they create.
Conclusion
In this tutorial, we explored how to implement Supabase authentication and authorization in a real-world Next.js application.
Supabase authentication and authorizationNext.jsWe started by setting up Supabase Auth for login and signup, created a relational schema with Row Level Security, and added dynamic authorization logic using ReBAC. With the help of Supabase Edge Functions and a Policy Decision Point (PDP), we enforced permission checks directly from the frontend.
Supabase AuthReBACSupabase Edge FunctionsPolicy Decision Point (PDP)
Kandisa Supabase Auth na miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso ya miso:
Supabase Auth - Ndimi na email na password
- Ndimi na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo na mpilo
- Ndimi na email na password
- Ndimi na mpilo na mpilo na mpilo na mpilo
- Ndimi na mpilo na mpilo na mpilo na mpilo
- Assign and evaluate user roles based on relationships to data
Let's make it easier to build apps to make it easier to build apps to make it easier to build apps to make it easier to build apps to build apps to build apps that require both authentication and fine-grained authorization.
Further Reading
-
-
-
-
-
-
Permit.io ReBAC Guide-
Permit + Authentication Providers
Permit + Authentication Providers-
Permit Elements: Embedded UI for Role Management
Permit Elements: Embedded UI for Role Management-
Data Filtering with Permit-
Audit Logs
Got questions? Join our Slack community, where hundreds of developers are building and discussing authorization.
Slack communitySlack community