新しい歴史

DIY Real-Time Polling App が Supabase と Permit.io でアクセスをブロック

Permit.io40m2025/04/16
Read on Terminal Reader

長すぎる; 読むには

セキュアでリアルタイムなアンケートアプリケーションを構築するためのフルスタックガイドで、認証、ユーザーごとの許可、およびダイナミック ポリシー チェックを Supabase + Permit.io を使用します。
featured image - DIY Real-Time Polling App が Supabase と Permit.io でアクセスをブロック
Permit.io HackerNoon profile picture
0-item


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.

Supabase 認証と認証Next.js

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).

Supabase Authライセンス規則Relationship-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

このガイドでは、実時投票アプリケーションを SupabaseNext.js を使用して構築し、アクションで認証と認証の両方を示します。SupabaseNext.js


アプリは、ユーザーがアンケートを作成し、他者に投票し、独自のコンテンツのみを管理することを可能にします。 Supabase Auth をログイン/サインアップのために実装する方法と、投票、編集、削除を制御する 認証ポリシーの実施方法を示します。Supabase Authライセンスポリシー


Supabaseのコア機能AuthPostgresRLSRealtime、および Edge Functionsを、Relationship-Based Access Control (ReBAC)

モデルと組み合わせて、ユーザーごとにおよびリソースごとにアクセスルールを適用します。AuthPostgresRLSRealtimeEdge 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
  • Supabase – Backend-as-a-service for database, authentication, realtime, and edge functions
  • SupabaseSupabase
  • Next.js – Frontend framework for building the app UI and API routes
  • Next.jsNext.js
  • Permit.io – (for ReBAC) to define and evaluate authorization logic via PDP
  • Permit.ioPermit.io
  • Supabase CLI – To manage and deploy Edge Functions locally and in production
  • Supabase CLISupabase CLI

    Prerequisites

  • Node.js がインストールされました
  • Supabase アカウント
  • Permit.io account
  • Permit.io
  • Familiarity with React/Next.js
  • Starter project repo
  • 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
  • Every 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
  • can't vote on a poll they created 彼らが作成したアンケートで投票することはできません。
  • Only the creator of a poll can edit or delete it
  • creator of a poll

    Tutorial Overview

    We’ll follow these general steps:


    1. Set up Supabase project, schema, auth, and RLS
    2. Build core app features like poll creation and voting
    3. Model authorization rules define roles and rules in Permit.io
    4. Create Supabase Edge Functions for syncing users, assigning roles, and checking permissions
    5. Enforce policies in the app frontend using those edge functions
  • Set up Supabase project, schema, auth, and RLS
  • Set up Supabase
  • Build core app features like poll creation and voting
  • Build core app features を参照してください。
  • Model authorization rules define roles and rules in Permit.io
  • Model authorization rules
  • Create Supabase Edge Functions for syncing users, assigning roles, and checking permissions
  • ユーザーの同期、役割の割り当て、および権限のチェックCreate Supabase Edge Functions
  • Enforce policies in the app frontend using those edge functions
  • Enforce policies ポリシーの強化


    Let's get started -

    プロジェクトにおける Supabase の設定

    Supabase in the Project の設定

    Optional: 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 templateGitHub


    You can clone the project by running the following command:


    git clone <https://github.com/permitio/supabase-fine-grained-authorization>
    
    git clone <https://github.com/permitio/supabase-fine-grained-authorization>


    Once you have cloned the project, navigate to the project directory and install the dependencies:


    cd realtime-polling-app-nextjs-supabase-permitio
    npm install
    
    cd realtime-polling-app-nextjs-supabase-permitio npm install

    Creating a new Project in Supabase

    >

    >


    • 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.com
  • 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.

    Project Settings → APIプロジェクトURLアノン・キー


    Supabase の認証とデータベースの設定

    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

    Authentication → Providers
  • Enable the Email provider

  • Enable the Email provider

    電子メール
  • (オプション) テスト用電子メール確認を無効にしますが、生産用に有効にします


  • (Optional) Disable email confirmation for testing, but keep it enabled for production


    Database Schemaの作成

    This app uses three main tables: polls, options, and votes. Use the SQL Editor in the Supabase dashboard and run the following:

    pollsoptionsvotesSQLエディター


    -- テーブルを作成するアンケートテーブルを作成する ( 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)

    Enable RLS for each table and define policies:

    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() ) );


    Supabaseのリアルタイム機能を使用するには:

    • 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


  • サイドバーで 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):

    pollsoptionsvotes
    • Click the three dots → Edit Table

    • Toggle "Enable Realtime"

    • Save changes


  • Click the three dots → Edit Table

  • Click the three dots → Edit Table

    Edit テーブル
  • Toggle "Enable Realtime"

  • Toggle "Enable Realtime"

    "Enable Realtime"
  • Save changes


  • Save Changes



    Implementing Supabase Email Authentication in the App

    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 .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;


    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.

    signInWithPasswordsignUpuser_name


    You can also use 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;


    Here, we are using the signOut method from Supabase to log out the user and redirect them to the home page.

    signOut

    ユーザーの認証状態の変更について聴く

    ユーザーの認証状態の変更に耳を傾けることで、ユーザーの認証状態に基づいてUIを更新できます。


    • 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 UI elements like login/logout buttons
  • Conditionally restrict access to protected pages (like voting or managing polls)
  • Assure only authenticated users can perform restricted actions
  • 認証されたユーザーのみが制限されたアクションを実行できるようにします。


    We’ll use supabase.auth.onAuthStateChange() to listen to these events and update the app accordingly.

    supabase.auth.onAuthStateChange()


    In the Layout.tsx file: Track Global Auth State

    In the Layout.tsxfile: Track Global Auth State


    平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年 平成30年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;

    保護されたページへのアクセス制限

    On pages like poll details or poll management, you should also listen for authentication state changes to prevent unauthenticated users from accessing them.

    poll detailspoll management


    This is 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;


    これらのパターンは、ユーザーの現在の認証状態を UI が反映し、後で追加する認証チェックの基盤を形成することを保証します. たとえば、後で user オブジェクトを使用して checkPermission Edge 関数を呼び出して、ユーザーが投票または特定のアンケートを管理できるかどうかを決定します。usercheckPermission

    Building the Polling App Functionality

    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
  • Implementing a voting system

  • This gives us the basic app behavior that we will soon protect with fine-grained permissions.

    これは、私たちがすぐに細かい許可で保護するアプリケーションの基本的な動作を提供します。

    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.

    <強力>アクティブ

    past 前の記事


    Example from 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 ( ... ); }

    ユーザーアンケートの表示と管理

    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.

    pollspolls


    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;


    ここでは、ユーザーによって作成されたアンケートのみを取得し、最新のアンケートデータでUIを更新するために 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;


    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.

    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
  • Only one vote per user per poll
  • Creators cannot vote on their own polls
  • クリエイターは自らのアンケートに投票できない。
  • Votes are stored in the votes table
  • votes
  • 結果はリアルタイムで表示され、更新されます

  • Let's break down how this works in the ViewPoll.tsx コンポーネント:

    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 Status Once we have the user, we pick:

    Load Poll Details and Check Voting Status
    • このユーザーがすでに投票したかどうか(オプションと投票数を含む)
    • The poll itself (including options and votes counts)
    • このユーザーがすでに投票したかどうか

    • 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 Updates

      We 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]);


      投票の提出を処理する

      Handle the Vote Submission

      ユーザーが投票しなかった場合、投票が許可されている場合(後で許可チェックを追加します)、彼らの投票を挿入します。


        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); } };


      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.

      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;
      


      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.

      authorization checksPermit.io Supabase Edge 機能


      それを行う前に、最初に実装する権限レイヤーの種類を見てみましょう。

      Understanding ReBAC (Relationship-Based Access Control)

      Supabase handles authentication and basic row-level permissions well, but it doesn’t support complex rules like:

      • ユーザーが独自のアンケートで投票するのを防ぐ
      • リソースごとに役割を割り当てる(特定のアンケートのための「Creator」など)
      • 外部ポリシーによるアクセス管理
    • Preventing users from voting on their own polls
    • リソースごとに役割を割り当てる(特定のアンケートの「Creator」など)
    • 外部ポリシーによるアクセス管理

    • これらの種類の関係ベースの許可をサポートするために、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.

      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
    • created
    • A user cannot vote on their own poll
    • cannotown
    • Other 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 docs

      Access Control Design

      For our Polling app, we will define:

      • 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 in polls. Can
        • date
      actions in polls. Can
    • create: Can cre
    • One resource with resource-specific actions:
      • polls: create, read, delete, update.
      • polls: create, read, delete, update.
      トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ >pollscreatereaddeleteupdate
    • 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.
    • authenticated:createreaddeleteupdateトップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ > トップ >creator: createreaddeleteupdatereadcreatecreate

      Setting up Permit.io

      Let’s walk through setting up the authorization model in 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 polls resource
      • > Resources
      • を定義するpollsresource
        • Go to the Policy → Resources tab
        • Click “Create Resource”
        • Name it polls, and add the actions: read, create, update, delete
      • Go to Policy → Resources tab
      • ポリシー→リソース
      • Click “Create Resource”
      • “Create Resource”
      • Name it polls, and add the actions: read, create, update, delete
      • pollsreadcreateupdatedelete
      • 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
      • authenticated
      • creator
      • creator














        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.

        admineditoruser


        • 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



      • アクセスポリシーの定義

        Define access policies
        • Go to the Policy → Policies tab
        • Visual matrix to define:
          • authenticated can read and create polls

          • creator can read, update, and delete polls

          • (オプション) 投票許可をこの部分として、または2番目のリソース経由で設定できます。

          • Go to the Policy → Policies tab
          • Policy → Policies
          • 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


            • authenticated can read and create polls

            • creator can read, update, and delete polls

            • (オプション) 投票権限をこれの一部として、または2番目のリソース経由で構成できます。

            • authentifiedreadcreate polls

            • authenticated can read and create polls

              authenticatedreadcreate
            • creator readupdate、および delete polls

            • creator can read, update, and delete polls

              creatorreadupdatedelete
            • (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)


            • リソースインスタンスを追加

              リソースインスタンスを追加
              • 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)


            • Go to Directory → Instances

            • Go to Directory → Instances

              ディレクトリ→インスタンス
            • Add individual poll IDs as resource instances (you’ll automate this later when new polls are created)

            • リソースインスタンスとして個々のアンケート ID を追加 (新しいアンケートが作成された後でこれを自動化します)

            • 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)

              user123creatorpoll456


              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 new users
              • Assign creator roles
              • Check access on demand
            • Sync new users
            • Assign creator roles
            • Check access on demand
            • Setting up Permit in the Polling Application

              Setting up Permit in the Polling Application

              Permit 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 Functions

              Create Functions in Supabase

              CREATE FUNCTIONS IN SUPABASE

              Initialise 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.

              これで、すべてのエンドポイントのコードを作成します。functionssupabase

              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_UPauthenticated


              エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: エージェント: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.

              createreadupdatedelete


              エクスプローチ・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コード・コ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 権限があることを確認します。checkPermissioncreatevotes“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.

              updatedelete


              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’t
            • Creators 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.js

              We 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
            • メールとパスワードでユーザーの認証
            • Voting and poll management to authorized users
            • Prevent creators from voting on their own polls
            • データとの関係に基づいてユーザーの役割を割り当て、評価する

            • この設定は、認証と精密認証の両方を必要とするアプリケーションを構築するためのスケーラブルな基盤を提供します。

              Further Reading

            • Permit.io ReBAC Guide

            • Permit.io ReBAC Guide

              Permit.io ReBAC Guide
            • Permit + Authentication Providers

            • Permit + Authentication Providers

              Permit + Authentication Providers
            • Permit Elements: Embedded UI for Role Management

            • Permit Elements: Embedded UI for Role Management

              Permit Elements: Embedded UI for Role Management
            • Data Filtering with Permit

            • Data Filtering with Permit

              Data Filtering with Permit
            • Audit Logs


            • Audit Logs

              Audit Logs


              Got questions? Join our Slack community, where hundreds of developers are building and discussing authorization.

              Slack communitySlack community

    Trending Topics

    blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks