SiamCafe · Blog
Supabase Realtime Home Lab Setup — ตั้ง Lab
บทความ

Supabase Realtime Home Lab Setup — ตั้ง Lab

เผยแพร่ 28 พฤษภาคม 2569

Supabase Realtime Lab

Supabase Realtime Home Lab Self-hosted PostgreSQL WebSocket Subscription Broadcast Presence RLS Edge Functions Chat Dashboard Collaboration

FeatureProtocolUse CaseLatencyScale
Postgres ChangesWebSocket + LISTEN/NOTIFYLive data updates50-200ms10K connections
BroadcastWebSocketChat, cursor sharing10-50ms100K messages/s
PresenceWebSocket + CRDTOnline users, typing100-500ms10K users/room
Edge FunctionsDeno DeployServer-side logic5-50msAuto-scale
AuthJWT + GoTrueUser authentication50-200msUnlimited

Self-hosted Setup

=== Supabase Self-hosted with Docker ===

Clone and setup

git clone --depth 1 https://github.com/supabase/supabase

cd supabase/docker

cp .env.example .env

Edit .env — Important settings

POSTGRES_PASSWORD=your-super-secret-password

JWT_SECRET=your-super-secret-jwt-token-at-least-32-chars

ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

DASHBOARD_USERNAME=admin

DASHBOARD_PASSWORD=admin123

SITE_URL=http://localhost:3000

Start all services

docker compose up -d

Services started:

  • PostgreSQL :5432
  • Supabase Studio :3000 (Dashboard)
  • Kong Gateway :8000 (API)
  • GoTrue :9999 (Auth)
  • PostgREST :3001 (REST API)
  • Realtime :4000 (WebSocket)
  • Storage :5000 (File Storage)
  • Meta :8080 (Metadata)

Create table with Realtime enabled

CREATE TABLE messages (

id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,

user_id UUID REFERENCES auth.users NOT NULL,

content TEXT NOT NULL,

room TEXT NOT NULL DEFAULT 'general',

created_at TIMESTAMPTZ DEFAULT NOW()

);

ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- Enable Realtime

ALTER PUBLICATION supabase_realtime ADD TABLE messages;

from dataclasses import dataclass

@dataclass

class ServiceConfig:

service: str

image: str

port: int

resource: str

purpose: str

services = [

ServiceConfig("PostgreSQL", "supabase/postgres", 5432, "2 CPU 4GB RAM", "Database"),

ServiceConfig("Realtime", "supabase/realtime", 4000, "1 CPU 1GB RAM", "WebSocket server"),

ServiceConfig("PostgREST", "postgrest/postgrest", 3001, "0.5 CPU 512MB", "Auto REST API"),

ServiceConfig("GoTrue", "supabase/gotrue", 9999, "0.5 CPU 512MB", "Authentication"),

ServiceConfig("Kong", "kong", 8000, "1 CPU 1GB RAM", "API Gateway"),

ServiceConfig("Studio", "supabase/studio", 3000, "1 CPU 1GB RAM", "Dashboard UI"),

ServiceConfig("Storage", "supabase/storage-api", 5000, "0.5 CPU 512MB", "File storage"),

]

print("=== Supabase Services ===")

for s in services:

print(f" [{s.service}] Port: {s.port} | Image: {s.image}")

print(f" Resource: {s.resource} | Purpose: {s.purpose}")

Realtime Client Code

=== Realtime Subscription Examples ===

JavaScript Client

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(

'http://localhost:8000',

'your-anon-key'

)

1. Postgres Changes — Listen to INSERT

const channel = supabase

.channel('messages-channel')

.on('postgres_changes',

{ event: 'INSERT', schema: 'public', table: 'messages',

filter: 'room=eq.general' },

(payload) => {

console.log('New message:', payload.new)

addMessageToUI(payload.new)

}

)

.subscribe()

2. Broadcast — Send/receive custom events

const broadcastChannel = supabase

.channel('cursor-room')

.on('broadcast', { event: 'cursor-move' }, (payload) => {

moveCursor(payload.payload.x, payload.payload.y, payload.payload.user)

})

.subscribe()

Send cursor position

broadcastChannel.send({

type: 'broadcast',

event: 'cursor-move',

payload: { x: 100, y: 200, user: 'alice' }

})

3. Presence — Track online users

const presenceChannel = supabase

.channel('online-users')

.on('presence', { event: 'sync' }, () => {

const state = presenceChannel.presenceState()

updateOnlineUsers(Object.keys(state))

})

.subscribe(async (status) => {

if (status === 'SUBSCRIBED') {

await presenceChannel.track({

user: 'alice', online_at: new Date().toISOString()

})

}

})

Python Client

from supabase import create_client

supabase = create_client("http://localhost:8000", "your-anon-key")

def handle_change(payload):

print(f"Change: {payload}")

channel = supabase.channel("messages")

channel.on_postgres_changes(

event="INSERT",

schema="public",

table="messages",

callback=handle_change

).subscribe()

@dataclass

class RealtimeFeature:

feature: str

event_types: str

use_case: str

example: str

features = [

RealtimeFeature("Postgres Changes", "INSERT UPDATE DELETE",

"Live data sync, dashboard updates",

"Chat messages, order status, stock prices"),

RealtimeFeature("Broadcast", "Custom events",

"Peer-to-peer messaging, no persistence",

"Cursor sharing, typing indicator, game state"),

RealtimeFeature("Presence", "sync join leave",

"Track online users, user state",

"Online indicators, collaborative editing"),

]

print("\n=== Realtime Features ===")

for f in features:

print(f" [{f.feature}] Events: {f.event_types}")

print(f" Use case: {f.use_case}")

print(f" Example: {f.example}")

RLS and Security

=== Row Level Security Policies ===

-- Users can read messages in rooms they belong to

CREATE POLICY "Users can read room messages"

ON messages FOR SELECT

USING (

room IN (

SELECT room_id FROM room_members

WHERE user_id = auth.uid()

)

);

-- Users can insert their own messages

CREATE POLICY "Users can insert own messages"

ON messages FOR INSERT

WITH CHECK (auth.uid() = user_id);

-- Users can update their own messages

CREATE POLICY "Users can update own messages"

ON messages FOR UPDATE

USING (auth.uid() = user_id);

-- Users can delete their own messages

CREATE POLICY "Users can delete own messages"

ON messages FOR DELETE

USING (auth.uid() = user_id);

-- Public read for announcements

CREATE POLICY "Anyone can read announcements"

ON announcements FOR SELECT

USING (true);

Edge Function — Server-side validation

supabase/functions/validate-message/index.ts

import { serve } from 'https://deno.land/std/http/server.ts'

import { createClient } from 'https://esm.sh/@supabase/supabase-js'

serve(async (req) => {

const { content, room } = await req.json()

if (content.length > 1000) {

return new Response('Message too long', { status: 400 })

}

Moderate content

if (containsBadWords(content)) {

return new Response('Inappropriate content', { status: 400 })

}

const supabase = createClient(

Deno.env.get('SUPABASE_URL'),

Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')

)

const { data, error } = await supabase

.from('messages')

.insert({ content, room, user_id: req.auth.uid })

return new Response(JSON.stringify(data))

})

@dataclass

class SecurityCheck:

check: str

status: str

fix: str

checklist = [

SecurityCheck("RLS enabled on all tables", "REQUIRED",

"ALTER TABLE x ENABLE ROW LEVEL SECURITY"),

SecurityCheck("RLS policies defined", "REQUIRED",

"CREATE POLICY for SELECT INSERT UPDATE DELETE"),

SecurityCheck("JWT Secret rotated", "RECOMMENDED",

"Change JWT_SECRET in .env, restart services"),

SecurityCheck("Service Role Key protected", "REQUIRED",

"Never expose in client code, use only server-side"),

SecurityCheck("Realtime filtered by RLS", "AUTOMATIC",

"Realtime respects RLS policies automatically"),

SecurityCheck("Rate limiting configured", "RECOMMENDED",

"Kong rate-limiting plugin, 100 req/min per user"),

SecurityCheck("HTTPS enabled", "REQUIRED for production",

"Use reverse proxy with SSL certificate"),

]

print("Security Checklist:")

for c in checklist:

print(f" [{c.status}] {c.check}")

print(f" Fix: {c.fix}")

เคล็ดลับ

  • RLS: เปิด RLS ทุก Table ก่อน Enable Realtime ป้องกัน Data Leak
  • Filter: ใช้ Filter ใน Subscription ฟังเฉพาะข้อมูลที่ต้องการ
  • Presence: อย่าเก็บข้อมูลมากใน Presence State เก็บเฉพาะที่จำเป็น
  • Unsubscribe: อย่าลืม Unsubscribe เมื่อ Component Unmount
  • Backup: ตั้ง pg_dump Backup PostgreSQL ทุกวัน

Supabase Realtime คืออะไร

Real-time Client Database PostgreSQL LISTEN NOTIFY WebSocket Postgres Changes Broadcast Presence Chat Dashboard Collaboration Notification