by ガブリエル・L・マナー
ガブリエル・L・マナー
Supabase makes it easy to add authentication to your app with built-in support for email, OAuth, and magic links. But while Supabase Auth handles who your users are, you often need an authorization layer as well.
Supabase は、組み込まれた auth および Row Level Security (RLS) で素晴らしいバックエンドを提供しており、特にユーザーとデータの間の 関係に基づく の許可を管理することは簡単ではありません。fine-grained permissions タイトルユーザーとデータの関係 データの編集や削除などのアクションをリソース所有者に制限したり、ユーザーが独自のコンテンツに投票するのを妨げたり、異なるユーザーの役割に対して異なる許可を強制したりする場合があります。 This tutorial walks through how to implement Supabase authentication and authorization in a Next.js application. We'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). 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. このガイドでは、実時投票アプリケーションを Supabase と Next.js を使用して構築し、アクションで認証と認証の両方を示します。SupabaseNext.js アプリは、ユーザーがアンケートを作成し、他者に投票し、独自のコンテンツのみを管理することを可能にします。 Supabase Auth をログイン/サインアップのために実装する方法と、投票、編集、削除を制御する 認証ポリシーの実施方法を示します。Supabase Authライセンスポリシー Supabaseのコア機能Auth、Postgres、 RLS、 Realtime、および Edge Functionsを、Relationship-Based Access Control (ReBAC) The demo application is a real-time polling platform built with Next.js and Supabase, where users can create polls and vote on others. We’ll follow these general steps: Let's get started - 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. You can clone the project by running the following command: Once you have cloned the project, navigate to the project directory and install the dependencies: > > 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. Click "New Project" and enter your project name, password, and region. 「新プロジェクト」をクリックし、プロジェクト名、パスワード、地域を記入します。"New Project" Once created, go to Project Settings → API and note your Project URL and Anon Key — you’ll need them later. Created once, go to Project Settings → API and note your Project URL and Anon Key — you will need them later. We will 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 In the sidebar, go to Authentication → Providers In the sidebar, go to Authentication → Providers Enable the Email provider Enable the Email provider (オプション) テスト用電子メール確認を無効にしますが、生産用に有効にします (Optional) Disable email confirmation for testing, but keep it enabled for production This app uses three main tables: Enable RLS for each table and define policies: Supabaseのリアルタイム機能を使用するには: In the sidebar, go to Table Editor For each of the three tables ( Click the three dots → Edit Table Toggle "Enable Realtime" Save changes In the sidebar, go to Table Editor サイドバーで Table Editor For each of the three tables ( Click the three dots → Edit Table Toggle "Enable Realtime" Save changes For each of the three tables ( Click the three dots → Edit Table Toggle "Enable Realtime" Save changes Click the three dots → Edit Table Click the three dots → Edit Table Toggle "Enable Realtime" Toggle "Enable Realtime" Save changes Save Changes In this demo app, anyone can view the list of polls available on the app, both active and expired. To view the details of a poll, manage, or vote on any poll, the user must be logged in. We will be using email and password as means of authentication for this project. In your Next.js project, store your Supabase credentials in ログインコンポーネントを更新して、メール/パスワードでログインとログインの両方を処理する: Here, we are using Supabase’s You can also use Here, we are using the ユーザーの認証状態の変更に耳を傾けることで、ユーザーの認証状態に基づいてUIを更新できます。 We’ll use In the On pages like poll details or poll management, you should also listen for authentication state changes to prevent unauthenticated users from accessing them. This is how it looks in And a similar pattern applies in これらのパターンは、ユーザーの現在の認証状態を UI が反映し、後で追加する認証チェックの基盤を形成することを保証します. たとえば、後で With Supabase configured and authentication working, we can now build the core functionality of the polling app. In this section, we’ll cover: This gives us the basic app behavior that we will soon protect with fine-grained permissions. 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 We’ll later call an Edge Function here to assign the “creator” role in Permit.io. 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. Example from Here, we are fetching active and past polls from the ここでは、ユーザーによって作成されたアンケートのみを取得し、最新のアンケートデータでUIを更新するために また、 So now, on a poll card, if the logged-in user is the poll creator, icons for editing and deleting the poll will be displayed to them. This allows the user to manage only their polls. The voting logic enforces: Let's break down how this works in the Fetch the Logged-In UserWe need the current user’s ID to determine voting eligibility and record their vote. Load Poll Details and Check Voting Status Once we have the user, we pick: We also call these again later in real-time updates. Listen for Real-Time Updates We subscribe to changes in the 投票の提出を処理する ユーザーが投票しなかった場合、投票が許可されている場合(後で許可チェックを追加します)、彼らの投票を挿入します。 Display the Poll ResultsWe calculate the total number of votes and a countdown to the expiration time. You can then use this to display progress bars or stats. 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. それを行う前に、最初に実装する権限レイヤーの種類を見てみましょう。 Supabase handles authentication and basic row-level permissions well, but it doesn’t support complex rules like: これらの種類の関係ベースの許可をサポートするために、Permit.io で ReBAC を実装します。 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. In this tutorial, we apply ReBAC to a polling app: 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. For our Polling app, we will define: Let’s walk through setting up the authorization model in Permit. Under “ReBAC Options,” define the following roles: Click Save Under “ReBAC Options,” define the following roles: Click Save Under “ReBAC Options,” define the following roles: Click Save Under “ReBAC Options,” define the following roles: Under “ReBAC Options,” define the following roles: Click Save Navigate to the "Roles" tab to view the roles from the resources we just created. Note that Permit created the default roles ( Define access policies (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. Define access policies (Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
アクセスポリシーの定義 (オプション) 投票許可をこの部分として、または2番目のリソース経由で設定できます。 (Optional) You can configure vote permissions as part of this or via a second resource if you model votes separately
What We’re Building
Tech Stack
Prerequisites
What Can This App Do?
Tutorial Overview
プロジェクトにおける Supabase の設定
Supabase in the Project の設定Optional: Clone the Starter Template
git clone <https://github.com/permitio/supabase-fine-grained-authorization>
git clone <https://github.com/permitio/supabase-fine-grained-authorization>
cd realtime-polling-app-nextjs-supabase-permitio
npm install
cd realtime-polling-app-nextjs-supabase-permitio
npm install
Creating a new Project in Supabase
Supabase の認証とデータベースの設定
Database Schemaの作成
polls
, options
, and votes
. Use the SQL Editor in the Supabase dashboard and run the following:polls
options
votes
SQLエディター
-- テーブルを作成するアンケートテーブルを作成する ( 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, ); -- オプションテーブルを作成する ( UUID idFAULT uuid_generate_v4() PRIMARY KEY, pollid UUID REFERENCES polls(id) ON DELETE CASCADE, text NOT NU
-- 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)
-- 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()
)
);
-- 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()
)
);
polls
, options
, votes
):
polls
, options
, votes
):
polls
, options
, votes
):polls
options
votes
Implementing Supabase Email Authentication in the App
.env.local
:.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
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;
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;
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
supabase.auth.signOut()
to log users out and redirect them:supabase.auth.signOut()
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;
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;
signOut
method from Supabase to log out the user and redirect them to the home page.signOut
ユーザーの認証状態の変更について聴く
supabase.auth.onAuthStateChange()
to listen to these events and update the app accordingly.supabase.auth.onAuthStateChange()
Layout.tsx
file: Track Global Auth StateLayout.tsx
file: Track Global Auth State
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;
保護されたページへのアクセス制限
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;
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;
user
オブジェクトを使用して checkPermission
Edge 関数を呼び出して、ユーザーが投票または特定のアンケートを管理できるかどうかを決定します。user
checkPermission
Building the Polling App Functionality
New Pollsを作成する
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;
Fetching and Displaying Polls
pages/index.tsx
:pages/index.tsx
import { 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 (
...
);
}
import { 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 (
...
);
}
ユーザーアンケートの表示と管理
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
pages/manage.tsx
ページを更新して、ユーザーが作成したアンケートのみを取得して表示する:pages/manage.tsx
import { 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;
polls
テーブルのリアルタイムのアップデートを聴きます。polls
PollCard
コンポーネントを更新して、ログインしたユーザーがアンケート作成者である場合、アンケートの編集と削除のアイコンがアンケートに表示されます。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;
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;
Implementing the Poll Voting System
votes
tablevotes
tablevotes
ViewPoll.tsx
コンポーネント:ViewPoll.tsx
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();
}, []);
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();
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]);
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);
}
};
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);
}
};
<
<<
<<
<<<
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
actions in polls. Can 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;
Understanding ReBAC (Relationship-Based Access Control)
Access Control Design
create
, read
, delete
, update
. create
and read
actions in polls. Can not delete
in polls. Can cre
create
, read
, delete
, update
.
トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ >pollscreate
, read
, delete
, update
.create
read
delete
update
create
and read
actions in polls. Can not delete
, or update
actions in polls.create
, read
, delete
, and update
actions in polls. Can perform read
and create
actions in votes. Cannot use create
on their own polls.create
and read
actions in polls. Can not delete
, or update
actions in polls.create
read
delete
update
トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ >creator: create
read
delete
update
read
create
create
Setting up Permit.io
supabase-polling
polls
resource
polls
, and add the actions: read
, create
, update
, delete
authenticated
creator
supabase-polling
supabase-polling
supabase-polling
supabase-polling
polls
resource
polls
resource
polls
, and add the actions: read
, create
, update
, delete
polls
, and add the actions: read
, create
, update
, delete
polls
read
create
update
delete
authenticated
creator
authenticated
creator
authenticated
creator
authenticated
authenticated
creator
creator
admin
, editor
, user
) that are unnecessary for this tutorial.admin
editor
user
authenticated
can read
and create
pollscreator
can read
, update
, and delete
polls
user123
is creator
of poll456
)
authenticated
can read
and create
pollscreator
can read
, update
, and delete
polls
authenticated
can read
and create
pollscreator
can read
, update
, and delete
polls
authenticated
can read
and create
pollscreator
can read
, update
, and delete
polls
-
authenticated
canread
andcreate
polls -
creator
canread
,update
, anddelete
polls -
authentified
がread
とcreate
polls -
creator
read
、update
、および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
iscreator
ofpoll456
)
-
-
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
iscreator
ofpoll456
)
-
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
iscreator
ofpoll456
)
- Sync new users
- Assign creator roles
- Check access on demand
- Sync new users
- Assign creator roles
- Check access on demand
(オプション) 投票権限をこれの一部として、または2番目のリソース経由で構成できます。
authenticated
can read
and create
polls
authenticated
read
create
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
リソースインスタンスを追加
リソースインスタンスを追加Go to Directory → Instances
ディレクトリ→インスタンスリソースインスタンスとして個々のアンケート ID を追加 (新しいアンケートが作成された後でこれを自動化します)
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.ioSetting 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 FunctionsCreate Functions in 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" } },
);
}
});
Assigning the Creator Role (updateCreatorRole.ts
)
updateCreatorRole.ts
Once a user creates a poll, this function is called to:
エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: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
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" } },
);
}
});
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" } },
);
}
});
チェック権限(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" } }
);
}
});
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>
例:
<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.
このセクションでは、主要なUIコンポーネントを更新して、これらの機能を呼び出し、許可チェックに基づいて投票やアンケートの管理などのユーザーアクションを条件で許可またはブロックします。
NewPoll.tsx
: Creator Role Assign 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."); } };
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.");
}
};
Assign the current user the creator
role for that specific poll
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
ユーザーがアンケートで投票することを許可する前に、 checkPermission
関数を呼び出して、votes
リソースで create
権限があることを確認します。checkPermission
create
votes
“A creator cannot vote on their own poll.”
投票権の確認:
投票許可をチェックする:
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]);
ユーザーが許可されていない場合は投票ボタンを無効にします:
ユーザーが許可されていない場合の投票ボタンを無効にします:
<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>
Show a message if the user is not allowed to vote:
ユーザーが投票できない場合のメッセージを表示する:
{user && !canVote && (
<p className="mt-4 text-gray-600">You cannot vote on your own poll</p>
)}
{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
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 && (
With:
{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
Logged out users can view polls but not interact
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’tCreators cannot vote on their own polls
Creators cannot vote on their own polls
cannot vote
Creators only 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.
結論
In this tutorial, we explored how to implement Supabase authentication and authorization in a real-world Next.js application.
Supabase 認証と認証Next.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 機能Policy Decision Point(PDP)
Supabase Authと柔軟なアクセス制御を組み合わせることにより、我々は以下のことを可能にしました。サプベース・アウト
- 電子メールとパスワードでユーザーの認証
- 権限のあるユーザーに投票およびアンケート管理を制限
- Creators from voting on their own polls
- Assign and evaluate user roles based on relationships to data
この設定は、認証と精密認証の両方を必要とするアプリケーションを構築するためのスケーラブルな基盤を提供します。
Further Reading
Permit + Authentication Providers
Permit + Authentication ProvidersPermit Elements: Embedded UI for Role Management
Permit Elements: Embedded UI for Role Management
Got questions? Join our Slack community, where hundreds of developers are building and discussing authorization.
Slack communitySlack community