PolarDB Supabase best practices: Build a web application

Updated at:

PolarDB Supabase is PolarDB for PostgreSQL's fully managed Supabase service. Centered around PolarDB for PostgreSQL as its core, it integrates and enhances key features, such as a Realtime database, RESTful APIs, GoTrue authentication, file storage, and log collection. This frees you from the complex parameter management and application O&M of Supabase and provides a flexible and high-performance backend solution. You can use PolarDB Supabase to quickly build modern applications, such as web applications, SaaS platforms, and AI-integrated applications.

This guide shows you how to use PolarDB Supabase to quickly build a meeting notes system.

Demo

When you open two browsers and join the same meeting, you will see information synchronize in real time across both. The core features of the meeting notes system include real-time collaborative editing, user presence management, file uploads and management, task tracking, activity logs, and a tagging system.

Core features

Feature

Description

Full database capabilities

Supabase is built on PolarDB for PostgreSQL and offers a full suite of database features:

  • Relational database: Supports complex table relationships, foreign key constraints, and transactions.

  • JSONB support: Stores semi-structured data, such as note content and activity data.

  • Full-text search: Built-in full-text search functionality.

  • Extension support: Supports PostgreSQL extensions for features such as UUID generation and timestamp handling.

-- Example: Use JSONB to store complex data
CREATE TABLE notes (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  content JSONB DEFAULT '{}',  -- Store rich-text content
  activity_data JSONB DEFAULT '{}'  -- Store activity metadata
);

Realtime

Uses PostgreSQL logical replication to enable real-time data synchronization:

  • Database change listening: Listens for INSERT, UPDATE, and DELETE events.

  • Channel management: Supports multiple independent channels for on-demand subscriptions.

  • Event filtering: Reduces unnecessary events by filtering with conditions.

  • Automatic reconnection: Automatically reconnects after network interruptions.

// Realtime subscription example
const channel = supabase
  .channel('meeting-updates')
  .on('postgres_changes', {
    event: 'UPDATE',
    schema: 'public',
    table: 'notes',
    filter: 'meeting_id=eq.123'
  }, (payload) => {
    console.log('updated note:', payload.new)
  })
  .subscribe()

Authentication

A complete built-in authentication and authorization system:

  • Multiple sign-in methods: Email and password, social logins, and magic links.

  • Session management: Automatically handles token refreshes and session persistence.

  • User management: User registration, password resets, and email verification.

  • Anonymous users: Supports access for temporary users.

// Authentication example
const { data: { user }, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password'
})

Row-level security (RLS)

Based on PostgreSQL row-level security policies:

  • Fine-grained access control: Controls access based on user, role, and data conditions.

  • Policy definition: Defines access rules using SQL.

  • Automatic enforcement: Automatically applies security policies to all queries.

  • Performance optimization: Filters data at the database level.

-- RLS policy example
CREATE POLICY "Users can only see their own meetings" ON meetings
  FOR SELECT USING (auth.uid() = created_by);

CREATE POLICY "Users can update their own meetings" ON meetings
  FOR UPDATE USING (auth.uid() = created_by);

Storage service

File storage and management based on the PolarDB file system:

  • File upload: Supports large file uploads and resumable uploads.

  • Access control: File access permissions based on RLS.

  • File management: Folder organization and metadata management.

// File upload example
const { data, error } = await supabase.storage
  .from('meeting-files')
  .upload('document.pdf', file, {
    cacheControl: '3600',
    upsert: false
  })

Edge functions

Server-side functions based on Deno:

  • TypeScript support: Native TypeScript support.

  • Database access: Direct access to the Supabase database.

  • Third-party integrations: Calls external APIs and handles webhooks.

// Edge function example
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? ''
  )
  
  const { data } = await supabase.from('meetings').select('*')
  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' }
  })
})

API (REST and GraphQL)

Auto-generated API endpoints:

  • REST API: Automatically generates CRUD endpoints.

  • GraphQL: Supports GraphQL queries (Enterprise Edition).

  • Real-time API: Supports real-time queries and subscriptions.

  • Type safety: Provides TypeScript types.

// REST API example
const { data, error } = await supabase
  .from('meetings')
  .select('*, notes(*)')
  .eq('id', meetingId)
  .single()

Build a meeting notes system

The meeting notes system primarily uses the following features:

Feature

Description

PolarDB for PostgreSQL cluster

Stores data in tables for meetings, notes, tasks, and user status.

Realtime

Enables real-time collaboration by synchronizing note edits and user presence.

Row-level security (RLS)

Ensures data security by controlling user access.

Storage service

Manages files for uploading and downloading meeting materials.

Authentication

Manages user sign-in and sessions.

Application tech stack

  • Frontend: Next.js 15 + React 18 + TypeScript.

  • Backend: PolarDB Supabase (PostgreSQL + authentication + Realtime + storage).

  • UI: Tailwind CSS + Radix UI.

  • State management: React Hooks + local state.

Database design

The database design for the meeting notes system includes tables for meetings, notes, user presence, tags, and tasks. Foreign key constraints ensure data consistency.

SQL Example

-- Create the meetings table
CREATE TABLE IF NOT EXISTS meetings (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create the notes table
CREATE TABLE IF NOT EXISTS notes (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  meeting_id UUID REFERENCES meetings(id) ON DELETE CASCADE,
  content JSONB DEFAULT '{}',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create the user presence table
CREATE TABLE IF NOT EXISTS user_presence (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  meeting_id UUID REFERENCES meetings(id) ON DELETE CASCADE,
  user_name VARCHAR(100) NOT NULL,
  user_color VARCHAR(7) DEFAULT '#1890ff',
  is_typing BOOLEAN DEFAULT FALSE,
  last_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  UNIQUE(meeting_id, user_name)
);
-- Create the tags table
CREATE TABLE IF NOT EXISTS tags (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  meeting_id UUID REFERENCES meetings(id) ON DELETE CASCADE,
  name VARCHAR(50) NOT NULL,
  color VARCHAR(7) DEFAULT '#1890ff',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create the tasks table
CREATE TABLE IF NOT EXISTS tasks (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  meeting_id UUID REFERENCES meetings(id) ON DELETE CASCADE,
  title VARCHAR(255) NOT NULL,
  description TEXT,
  assignee VARCHAR(100),
  status VARCHAR(20) DEFAULT 'pending',
  due_date TIMESTAMP WITH TIME ZONE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create the meeting activities log table
CREATE TABLE IF NOT EXISTS meeting_activities (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  meeting_id UUID REFERENCES meetings(id) ON DELETE CASCADE,
  user_name VARCHAR(100) NOT NULL,
  activity_type VARCHAR(50) NOT NULL, -- 'join', 'leave', 'edit', 'add_tag', 'add_task', etc.
  activity_data JSONB DEFAULT '{}',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Meeting files table: meeting_files
CREATE TABLE IF NOT EXISTS meeting_files (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  meeting_id UUID REFERENCES meetings(id) ON DELETE CASCADE,
  file_name VARCHAR(255) NOT NULL,
  file_url TEXT NOT NULL,
  uploader VARCHAR(100),
  uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  file_size BIGINT,
  mime_type VARCHAR(100)
);
-- Create an index on meeting_id
CREATE INDEX IF NOT EXISTS idx_meeting_files_meeting_id ON meeting_files(meeting_id);

Realtime configuration

Enable Realtime

-- Enable the Realtime feature
ALTER PUBLICATION supabase_realtime ADD TABLE meetings;
ALTER PUBLICATION supabase_realtime ADD TABLE notes;
ALTER PUBLICATION supabase_realtime ADD TABLE user_presence;
ALTER PUBLICATION supabase_realtime ADD TABLE tags;
ALTER PUBLICATION supabase_realtime ADD TABLE tasks;
ALTER PUBLICATION supabase_realtime ADD TABLE meeting_activities;
ALTER PUBLICATION supabase_realtime ADD TABLE meeting_files; 

Client-side subscriptions

// Create a custom hook to manage Realtime subscriptions
export function useRealtime(meetingId: string, callbacks: RealtimeCallbacks) {
  const channelsRef = useRef<any[ ]>([ ])
  const cleanup = useCallback(() => {
    channelsRef.current.forEach((channel) => {
      supabase.removeChannel(channel)
    })
    channelsRef.current = []
  }, [])
  useEffect(() => {
    if (!meetingId) return
    // Clean up previous connections
    cleanup()
    // Create multiple dedicated channels
    const presenceChannel = supabase
      .channel(`presence:${meetingId}`)
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "user_presence",
          filter: `meeting_id=eq.${meetingId}`,
        },
        (payload) => {
          console.log("User presence change:", payload)
          if (callbacks.onUserPresenceChange) {
            loadOnlineUsers()
          }
        },
      )
      .subscribe()
    // Save channel references for cleanup
    channelsRef.current = [presenceChannel, /* other channels */]
  }, [meetingId, callbacks])
  return { cleanup }
}

Realtime subscription design

  • Feature isolation: Isolate features into separate channels by scenario (for example, presence, document collaboration, and task notifications) to reduce logical coupling.

  • Event filtering: Use the filter parameter to scope events (for example, by specifying a meeting ID) to reduce redundant data transmission.

  • Resource cleanup: When a component unmounts or the page changes, call removeChannel to clean up all channel connections and release resources.

  • Error handling: Monitor the status of subscription channels to implement retry or fallback mechanisms for exceptions such as disconnections and timeouts.

Security

Row-level security (RLS)

-- Enable RLS
ALTER TABLE meetings ENABLE ROW LEVEL SECURITY;
-- Create security policies
CREATE POLICY "Users can view all meetings" ON meetings
  FOR SELECT USING (true);
CREATE POLICY "Users can create meetings" ON meetings
  FOR INSERT WITH CHECK (true);
CREATE POLICY "Users can update their own meetings" ON meetings
  FOR UPDATE USING (auth.uid() = created_by);

User authentication

  const login = useCallback(async (email: string, password: string) => {
    try {
      const { data, error } = await supabase.auth.signInWithPassword({ email, password })
      if (error) throw error
      if (data.user) {
        const userData = transformSupabaseUser(data.user)
        setUser(userData)
        localStorage.setItem("meeting_user", JSON.stringify(userData))
        return { success: true, user: userData }
      }
      return { success: false, error: 'Sign-in failed' }
    } catch (error: any) {
      return { success: false, error: error.message || 'Sign-in failed' }
    }
  }, [])
  const transformSupabaseUser = (supabaseUser: SupabaseUser): User => ({
    id: supabaseUser.id,
    email: supabaseUser.email || null,
    name: supabaseUser.user_metadata?.name || supabaseUser.email?.split('@')[0] || 'User',
    avatar_url: supabaseUser.user_metadata?.avatar_url || null,
    created_at: supabaseUser.created_at,
    is_anonymous: supabaseUser.user_metadata?.is_anonymous || false,
  })

Client integration

Client

// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Type definition
export interface Meeting {
  id: string
  created_at: string
  title: string
  description: string | null
}

Environment variables

# Production environment variables
NEXT_PUBLIC_SUPABASE_URL=<YOUR_SUPABASE_PUBLIC_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<YOUR_SUPABASE_ANON_KEY>

Quick start

  1. Download the sample project code: PolarDB-Supabase-App-Demo.

  2. Prepare the runtime environment: Install Node.js and pnpm in your local environment.

    Note
    • Node.js: Download and install it from the official Node.js website.

    • pnpm: After you install Node.js, you can use npm to install it globally. The command is npm install -g pnpm.

  3. Configure the environment: In the project's root directory, create a .env.local file and replace the following values with your PolarDB Supabase configuration.

    Note

    On the cluster's AI Capability > AI Application list page, click your Application ID to go to the application details page. The Topology and Configuration tabs contain the required configuration information.

    • <YOUR_SUPABASE_PUBLIC_URL> is the Public Endpoint of your application.

    • <YOUR_SUPABASE_ANON_KEY> is the value of the application parameter secret.jwt.anonKey.

    # Production environment variables
    NEXT_PUBLIC_SUPABASE_URL=<YOUR_SUPABASE_PUBLIC_URL>
    NEXT_PUBLIC_SUPABASE_ANON_KEY=<YOUR_SUPABASE_ANON_KEY>
  4. Initialize the database: In the scripts folder in the project's root directory, locate the 01-create-tables.sql file, and run the SQL in the SQL Editor on the right-side navigation bar of the Supabase Dashboard.

  5. Run the project: In the project's root directory, run the following commands to install dependencies and start the project. After the project starts, open http://localhost:3000 in your browser to access the application.

    pnpm install
    pnpm dev

    After accessing the application, you will see the homepage for the Smart Meeting Notes System. The page displays four cards for core features: Real-time Collaboration, Smart Recording, Activity Tracking, and Task Management. On the left is a Create New Meeting form with a Meeting Title input box and a Meeting Description text area. On the right, the Experience Demo Meeting option allows you to join the most recently created meeting. A yellow banner at the top of the page reminds you to sign in or join anonymously before creating or joining a meeting.

Summary

This project demonstrates the following best practices for using PolarDB Supabase:

Follow these best practices to build stable, secure, and high-performance PolarDB Supabase applications.