Laravel กับ Supabase เป็นคู่ที่เหมาะสำหรับการสร้างเว็บแอปและ API ที่ต้องการพัฒนาเร็ว แต่ยังได้ความสามารถของ PostgreSQL, Authentication, Storage และระบบจัดการฐานข้อมูลแบบ Managed Service

บทความนี้อธิบายแนวทางที่ใช้งานได้จริงกับ Laravel รุ่นปัจจุบัน โดยเน้นการใช้ Supabase เป็น PostgreSQL หลักของแอป Laravel และสรุปทางเลือกสำหรับ Auth, Data API และ Storage

#1. ภาพรวมสถาปัตยกรรม

Laravel สามารถเชื่อมต่อ Supabase ได้ 2 แนวทางหลัก

  1. เชื่อมต่อ PostgreSQL โดยตรง ผ่าน PDO และใช้ Eloquent/Query Builder ซึ่งเหมาะกับแอป Laravel ทั่วไป
  2. เรียก Supabase Data API ผ่าน HTTP ซึ่งเหมาะกับกรณีที่ต้องการใช้ Row Level Security (RLS) และ API ของ Supabase โดยตรง

สำหรับระบบ Laravel ที่มี Controller, Model, Migration และ Business Logic อยู่แล้ว แนะนำให้เริ่มจากแนวทางที่ 1 เพราะเข้ากับโครงสร้าง Laravel มากที่สุด

ผู้ใช้
  │
  ▼
Laravel Application
  ├── Routes / Controllers
  ├── Eloquent ORM
  └── Laravel Auth หรือ Supabase Auth
          │
          ▼
Supabase PostgreSQL
  ├── Tables / Indexes
  ├── Extensions
  ├── Row Level Security
  └── Storage / Data API (เลือกใช้ตามความเหมาะสม)

#2. สิ่งที่ต้องเตรียม

  • PHP และ Composer
  • Laravel
  • PostgreSQL driver สำหรับ PHP (pdo_pgsql)
  • บัญชี Supabase และ Project
  • Database password ของ Supabase
  • Connection string จากเมนู Connect ใน Supabase Dashboard

ตรวจสอบ PHP extension ได้ด้วยคำสั่งต่อไปนี้

php -m | grep -E "pdo_pgsql|pgsql"

ถ้ายังไม่มี pdo_pgsql ให้ติดตั้งตามระบบปฏิบัติการ เช่น Ubuntu

sudo apt update
sudo apt install php-pgsql

#3. สร้างโปรเจกต์ Laravel

composer create-project laravel/laravel laravel-supabase
cd laravel-supabase

php artisan key:generate

หากต้องการระบบ Login/Register แบบพื้นฐาน สามารถติดตั้ง Laravel Breeze ได้

composer require laravel/breeze --dev
php artisan breeze:install blade

สามารถเปลี่ยน blade เป็น react หรือ vue ได้ตาม Stack ที่เลือกใช้

#4. ตั้งค่าการเชื่อมต่อฐานข้อมูล

จาก Supabase Dashboard ให้เปิด Connect แล้วเลือก Connection String แบบ Session Pooler หรือ Direct Connection ตามสภาพแวดล้อมของระบบ

ตัวอย่างไฟล์ .env

APP_NAME=LaravelSupabase
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000

DB_CONNECTION=pgsql
DB_URL=postgres://postgres.PROJECT_REF:YOUR_PASSWORD@POOLER_HOST:5432/postgres

ถ้ารหัสผ่านมีอักขระพิเศษ เช่น &, #, ?, / หรือช่องว่าง ต้องทำ URL encoding ก่อนนำไปใส่ใน DB_URL

จากนั้นแก้ config/database.php ในส่วน pgsql

'pgsql' => [
    'driver' => 'pgsql',
    'url' => env('DB_URL'),
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '5432'),
    'database' => env('DB_DATABASE', 'laravel'),
    'username' => env('DB_USERNAME', 'root'),
    'password' => env('DB_PASSWORD', ''),
    'charset' => 'utf8',
    'prefix' => '',
    'prefix_indexes' => true,
    'search_path' => env('DB_SCHEMA', 'public'),
    'sslmode' => env('DB_SSLMODE', 'require'),
],

ล้าง Configuration Cache แล้วทดสอบการเชื่อมต่อ

php artisan config:clear
php artisan migrate
php artisan db:show

#ข้อควรระวังเรื่อง Pooler

ไม่ควรนำ Transaction Pooler พอร์ต 6543 มาใช้เป็นแหล่งข้อมูลหลักของ ORM โดยไม่ตรวจสอบข้อจำกัดของ prepared statements ก่อน สำหรับ Laravel และ Eloquent ให้เริ่มจาก Session Pooler พอร์ต 5432 หรือ Direct Connection ตามคำแนะนำใน Supabase Dashboard

สำหรับ Production ควรเก็บ Connection String ใน Secret หรือ Environment Variable ของแพลตฟอร์มที่ใช้ Deploy และไม่ Commit ไฟล์ .env เข้า Git

#5. เลือก Schema ให้เหมาะสม

Supabase เปิด public schema ให้ใช้งานผ่าน Data API ได้ ดังนั้นระบบที่ใช้ Laravel เชื่อมต่อโดยตรงอาจแยกตารางของแอปไปอยู่ใน schema เช่น laravel เพื่อควบคุมขอบเขตให้ชัดเจน

สร้าง Schema จาก SQL Editor ของ Supabase

create schema if not exists laravel;

แล้วกำหนดใน .env

DB_SCHEMA=laravel

ถ้าใช้ public schema และต้องการเปิด Data API/RLS ให้ทบทวนสิทธิ์ของแต่ละตารางอย่างละเอียดก่อนนำขึ้น Production

#6. สร้าง Model และ Migration

ตัวอย่างระบบจัดการงาน (Tasks)

php artisan make:model Task -m

แก้ Migration ใน database/migrations/..._create_tasks_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->boolean('completed')->default(false);
            $table->timestampsTz();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

แก้ Model ใน app/Models/Task.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $fillable = [
        'title',
        'description',
        'completed',
    ];

    protected function casts(): array
    {
        return [
            'completed' => 'boolean',
        ];
    }
}

รัน Migration

php artisan migrate

#7. สร้าง Controller และ API

php artisan make:controller Api/TaskController --api

ตัวอย่าง Controller แบบย่อ

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    public function index()
    {
        return Task::query()
            ->latest()
            ->paginate(20);
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'title' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
        ]);

        return response()->json(
            Task::create($data),
            201
        );
    }

    public function show(Task $task)
    {
        return $task;
    }

    public function update(Request $request, Task $task)
    {
        $data = $request->validate([
            'title' => ['sometimes', 'required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'completed' => ['sometimes', 'boolean'],
        ]);

        $task->update($data);

        return $task->refresh();
    }

    public function destroy(Task $task)
    {
        $task->delete();

        return response()->noContent();
    }
}

กำหนด Route ใน routes/api.php

use App\Http\Controllers\Api\TaskController;
use Illuminate\Support\Facades\Route;

Route::apiResource('tasks', TaskController::class);

ทดสอบด้วยคำสั่ง

php artisan serve
curl http://127.0.0.1:8000/api/tasks

#8. Authentication: Laravel Auth หรือ Supabase Auth

#ทางเลือก A: ใช้ Laravel Auth

เหมาะกับระบบที่ใช้ Laravel เป็นศูนย์กลางอยู่แล้ว เช่น Breeze, Fortify, Sanctum และ Session Cookie

  • ตารางผู้ใช้เก็บใน users ของ Laravel
  • Login/Register จัดการด้วย Laravel
  • API ใช้ Laravel Sanctum ได้
  • Supabase ทำหน้าที่เป็น PostgreSQL และ Storage ตามที่เลือกใช้

ข้อดีคือโค้ด Laravel เป็นแนวทางมาตรฐานและดูแลต่อได้ง่าย โดย Quickstart ของ Supabase สำหรับ Laravel ก็ใช้ Laravel Authentication เป็นค่าเริ่มต้น

#ทางเลือก B: ใช้ Supabase Auth

เหมาะกับกรณีที่มี Frontend หรือ Mobile App หลายตัว และต้องการให้ Supabase เป็นศูนย์กลางของ Identity

แนวทางที่ควรวางแผนให้ชัดเจน

  1. Frontend Login ผ่าน Supabase Auth
  2. Frontend ได้ Access Token แบบ JWT
  3. ส่ง JWT มาที่ Laravel API ผ่าน Authorization: Bearer ...
  4. Laravel ตรวจสอบ Token และกำหนดสิทธิ์การใช้งาน
  5. PostgreSQL/RLS ใช้ auth.uid() ได้เมื่อคำขอเข้าสู่ Data API ตามรูปแบบที่ Supabase รองรับ

ไม่ควรมีทั้ง Laravel users และ Supabase Auth users โดยไม่มีการกำหนดแหล่งข้อมูลหลัก เพราะอาจทำให้ข้อมูลผู้ใช้และสิทธิ์ไม่ตรงกัน

#9. ใช้ Supabase Data API จาก Laravel

ถ้าต้องการเรียก Data API สามารถใช้ Laravel HTTP Client ได้ โดยเก็บ URL และ Key ไว้ใน .env

SUPABASE_URL=https://PROJECT_REF.supabase.co
SUPABASE_ANON_KEY=your-anon-key

ตัวอย่าง Service

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class SupabaseDataService
{
    public function listTasks(): array
    {
        return Http::withHeaders([
            'apikey' => config('services.supabase.anon_key'),
            'Authorization' => 'Bearer '.config('services.supabase.anon_key'),
        ])
            ->get(config('services.supabase.url').'/rest/v1/tasks', [
                'select' => '*',
                'order' => 'created_at.desc',
            ])
            ->throw()
            ->json();
    }
}

เพิ่มใน config/services.php

'supabase' => [
    'url' => env('SUPABASE_URL'),
    'anon_key' => env('SUPABASE_ANON_KEY'),
],

anon key ไม่ใช่สิทธิ์ที่ทำให้ข้าม RLS ได้ ดังนั้นต้องเขียน Policy ให้ถูกต้อง ส่วน service_role key มีสิทธิ์สูงและต้องใช้เฉพาะฝั่ง Server เท่านั้น ห้ามส่งไปยัง Browser, Mobile App หรือเก็บไว้ในโค้ดฝั่ง Client

#10. ใช้ Supabase Storage กับ Laravel

มี 2 แนวทางทั่วไป

#อัปโหลดผ่าน Laravel Filesystem

เหมาะเมื่อ Laravel เป็นผู้ควบคุมการอัปโหลดและต้องการบันทึก Metadata ในฐานข้อมูล Laravel

$path = $request->file('avatar')->store('avatars', 's3');

Supabase Storage รองรับ S3-compatible API ในกรณีที่ต้องการเชื่อมผ่าน S3 adapter ควรตรวจสอบ Endpoint, Region, Access Key และ Secret Key จาก Dashboard ให้ตรงกับรูปแบบที่ Supabase กำหนด

#เรียก Supabase Storage API

เหมาะเมื่อใช้ระบบ Bucket และ Policy ของ Supabase เป็นหลัก ควรแยก Service สำหรับการ Upload, Download และ Signed URL ไม่ควรปนกับ Controller โดยตรง

ไม่ว่าจะเลือกวิธีใด ควรตรวจสอบชนิดไฟล์, ขนาดไฟล์, ชื่อไฟล์, สิทธิ์การเข้าถึง และการป้องกัน Path Traversal

#11. Row Level Security (RLS)

RLS เป็นหัวใจสำคัญเมื่อเปิดให้ Client เรียก Supabase Data API โดยตรง ตัวอย่าง Policy ต้องออกแบบให้สัมพันธ์กับข้อมูลผู้ใช้ เช่น มีคอลัมน์ user_id ในตาราง

alter table public.tasks enable row level security;

create policy "Users can read own tasks"
on public.tasks
for select
to authenticated
using (user_id = auth.uid());

create policy "Users can insert own tasks"
on public.tasks
for insert
to authenticated
with check (user_id = auth.uid());

ถ้า Laravel เชื่อมต่อฐานข้อมูลโดยตรงด้วย Database Password การควบคุมสิทธิ์จะอยู่ที่ Laravel และ PostgreSQL Role เป็นหลัก ไม่ควรคิดว่า RLS จะป้องกันทุกกรณีโดยอัตโนมัติ ให้ทดสอบสิทธิ์ทั้งจาก Laravel และจาก Data API แยกกัน

#12. แนวทาง Production ที่แนะนำ

  • บังคับ sslmode=require
  • ใช้ Secret Manager หรือ Environment Variables
  • ไม่ Commit DB_URL, Database Password หรือ service_role key
  • ใช้ Migration ใน CI/CD แทนการแก้ Schema ด้วยมือ
  • แยก public schema กับ schema ภายในแอปเมื่อเหมาะสม
  • สร้าง Index สำหรับคอลัมน์ที่ใช้ค้นหาและ Foreign Key
  • ใช้ Pagination แทนการดึงข้อมูลทั้งหมด
  • ตรวจสอบ Query ด้วย Laravel Telescope, Log และ Database Monitoring
  • ตั้งค่า Backup และ Point-in-Time Recovery ตามระดับความสำคัญของระบบ
  • ทดสอบ RLS ด้วยผู้ใช้หลายบทบาท
  • จำกัด CORS และตรวจสอบ Token ก่อนเข้าถึง API
  • อย่าใช้ service_role key ใน JavaScript ที่ส่งให้ผู้ใช้

#13. ปัญหาที่พบบ่อย

#could not find driver

แปลว่ายังไม่มี pdo_pgsql

php -m | grep pgsql

#password authentication failed

ตรวจสอบ Password, Project Reference, Username และ Host จาก Connection String ใหม่ อย่าพิมพ์ Host เองจากความจำ

#ต่อฐานข้อมูลได้ แต่ Migration ล้มเหลว

ตรวจสอบว่าใช้ Pooler แบบใด หากใช้ Transaction Pooler ให้เปลี่ยนเป็น Session Pooler/Direct Connection สำหรับงานที่ต้องใช้ prepared statements หรือ schema operations

#relation does not exist

ตรวจสอบ search_path, ชื่อ Schema, ชื่อตาราง และ Migration ที่รันไปแล้ว

php artisan migrate:status
php artisan config:clear

#Data API ได้ 401 หรือ 403

ตรวจสอบ API Key, Bearer Token, RLS Policy, Role (anon หรือ authenticated) และชื่อ Schema ที่เปิดให้ Data API เข้าถึง

#14. สรุปการเลือกแนวทาง

ความต้องการ แนวทางที่เหมาะสม
Laravel CRUD ทั่วไป PostgreSQL ผ่าน Eloquent
Laravel เป็นศูนย์กลาง Auth Laravel Breeze/Fortify/Sanctum
Frontend หลายแพลตฟอร์มใช้ Identity ร่วมกัน Supabase Auth
ต้องการ RLS ที่ Data API Supabase Data API + Policy
งาน Migration/ORM จำนวนมาก Session Pooler หรือ Direct Connection
อัปโหลดไฟล์แบบควบคุมจาก Laravel Laravel Filesystem หรือ Supabase Storage API
Production ที่ปลอดภัย SSL + Secret Management + RLS/Authorization ที่ทดสอบแล้ว

#บทสรุป

รูปแบบที่เริ่มต้นได้ง่ายและเหมาะกับแอป Laravel ส่วนใหญ่คือ ใช้ Supabase เป็น PostgreSQL แล้วเชื่อมต่อด้วย Eloquent ผ่าน Session Pooler หรือ Direct Connection พร้อมเปิด SSL จากนั้นใช้ Laravel Auth เป็นตัวจัดการผู้ใช้ และค่อยเพิ่ม Supabase Storage หรือ Data API เมื่อมีเหตุผลด้านสถาปัตยกรรม

ถ้าระบบมี Frontend หรือ Mobile หลายแพลตฟอร์มและต้องการระบบ Identity กลาง จึงค่อยพิจารณา Supabase Auth โดยกำหนดให้ชัดว่า Token, User และสิทธิ์ถูกตรวจสอบที่ชั้นใด

#แหล่งอ้างอิง