Dari frontend-only ke full-stack architecture dengan PostgreSQL, Row Level Security, dan real-time subscriptions.
HTML, CSS, JavaScript vanilla dengan localStorage.
AI prompting, debugging, React, dan Next.js SSG.
Database persistence, backend API, multi-user collaboration.
"Semua aplikasi Anda sejauh ini frontend-only. Data tidak persistent di luar browser individual. Saatnya membangun sesuatu yang nyata."
localStorage = data hanya di satu browser, satu device. Clear cache? Data hilang. Tidak bisa share antar user.
MDX hardcoded = edit file, rebuild, redeploy. Tidak ada user submission atau interaksi multi-user.
Single source of truth. Data accessible dari semua devices. Real-time sync. Secure access control.
Built on PostgreSQL — mature, powerful, relational.
Fokus di app logic, bukan infrastructure.
Relations, transactions, complex queries.
Access control di database level.
Live subscriptions untuk collaborative apps.
Great docs, dashboard, TypeScript support.
Production apps tanpa bayar.
SQL dan pattern berlaku di stack manapun.
Sebelum coding, mari bangun pemahaman konseptual tentang relational database dan SQL.
| ID | Title | Completed | User | Created At |
|---|---|---|---|---|
| 1 | Buy groceries | FALSE | Alice | 2026-02-08 |
| 2 | Finish report | TRUE | Bob | 2026-02-07 |
| 3 | Call dentist | FALSE | Alice | 2026-02-09 |
"Alice" muncul berulang. Update nama? Edit semua rows.
Tidak ada yang cegah tanggal invalid atau teks di kolom number.
User profile info harus duplikat atau manual lookup.
Tidak bisa restrict "Alice hanya lihat tasks miliknya."
CREATE TABLE tasks ( id BIGSERIAL PRIMARY KEY, -- Auto-increment, unique title TEXT NOT NULL, completed BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW() );
CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT UNIQUE NOT NULL, name TEXT ); CREATE TABLE tasks ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, user_id UUID REFERENCES users(id) ON DELETE CASCADE );
REFERENCES users(id) = foreign key constraint. Nilai di user_id harus exist di users.id.ON DELETE CASCADE = jika user dihapus, semua tasks miliknya otomatis dihapus.CREATE TABLE tasks ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL CHECK(length(title) > 0), completed BOOLEAN DEFAULT false, priority INTEGER CHECK(priority BETWEEN 1 AND 5), user_id UUID REFERENCES users(id) NOT NULL );
Kolom tidak boleh kosong.
Nilai harus unique di table.
Custom validation rule.
Reference integrity.
Database reject insert/update yang violate constraint. Ini mencegah garbage data.
All-or-nothing. Error di tengah? Rollback semua.
Database selalu dalam state valid (constraints satisfied).
Concurrent transactions tidak interfere.
Setelah commit, data guaranteed persisted.
BEGIN; INSERT INTO tasks (title, user_id) VALUES ('Task A', '123'); INSERT INTO tasks (title, user_id) VALUES ('Task B', '999'); -- Error! ROLLBACK; -- 'Task A' tidak masuk database
"Iterate array, filter items, map values, reduce result..."
"Give me all tasks where completed is false, sorted by created_at."
Anda describe apa yang diinginkan; database engine yang tentukan bagaimana mendapatkannya secara optimal.
CREATE TABLE tasks (...) ALTER TABLE tasks ADD COLUMN priority INT DROP TABLE tasks
INSERT INTO tasks VALUES (...) UPDATE tasks SET completed = true DELETE FROM tasks WHERE id = 1
SELECT * FROM tasks WHERE completed = false SELECT users.name, tasks.title FROM tasks JOIN users ON tasks.user_id = users.id
GRANT SELECT ON tasks TO user_role CREATE POLICY "..." ON tasks FOR SELECT USING (auth.uid() = user_id)
POST /api/tasks
{ "title": "Buy milk" }
// Response bisa apa saja
INSERT INTO tasks (title) VALUES ('Buy milk'); -- Guaranteed: types valid, -- constraints satisfied, -- transaction atomic
Database adalah contract enforcement layer. Anda define schema (contract), database enforce. Data selalu konsisten.
supabase.com dan sign up (gratis)task-manager, generate strong password, pilih region terdekat✓ Anda sekarang punya PostgreSQL database yang running di cloud dengan auto-generated API endpoints.
GUI untuk view dan edit data seperti spreadsheet.
Write dan execute SQL queries.
User management (Bab 9).
File storage untuk images, docs.
Auto-generated docs based on schema.
Visual diagram relationships.
Navigate ke SQL Editor di sidebar, copy-paste SQL ini, dan klik "Run":
-- Create tasks table CREATE TABLE tasks ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL CHECK(length(title) > 0), completed BOOLEAN DEFAULT false, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Enable Row Level Security ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
✓ Success! Navigate ke Table Editor → tasks untuk lihat table kosong Anda.
INSERT INTO tasks (title, user_id) VALUES ('Setup Supabase project', auth.uid()), ('Create database schema', auth.uid()), ('Integrate with Next.js', auth.uid());
Catatan: auth.uid() akan return NULL jika belum ada authenticated user. Untuk testing, bisa hardcode UUID atau tunggu implementasi auth di Bab 9.
id auto-generated oleh BIGSERIALcompleted default falsecreated_at auto-set ke NOW()-- Get all tasks SELECT * FROM tasks; -- Get incomplete tasks only SELECT id, title, created_at FROM tasks WHERE completed = false ORDER BY created_at DESC; -- Count completed tasks SELECT COUNT(*) as total_completed FROM tasks WHERE completed = true;
WHERE untuk filter rowsORDER BY untuk sortingCOUNT(*) untuk aggregate-- Mark task as completed UPDATE tasks SET completed = true WHERE id = 1; -- Update multiple columns UPDATE tasks SET title = 'Updated title', completed = true WHERE id = 2; -- Delete task DELETE FROM tasks WHERE id = 3;
⚠️ Hati-hati: UPDATE atau DELETE tanpa WHERE akan affect semua rows!
-- Get tasks with user information SELECT users.name, users.email, tasks.title, tasks.completed, tasks.created_at FROM tasks JOIN auth.users ON tasks.user_id = users.id WHERE tasks.completed = false ORDER BY tasks.created_at DESC;
JOIN menggabungkan rows dari dua tables berdasarkan conditionON tasks.user_id = users.id adalah matching condition"Security di database level, bukan di application code. Users hanya bisa akses data yang seharusnya mereka akses."
auth.uid())-- Policy untuk SELECT CREATE POLICY "Users can view their own tasks" ON tasks FOR SELECT USING (auth.uid() = user_id); -- Policy untuk INSERT CREATE POLICY "Users can create their own tasks" ON tasks FOR INSERT WITH CHECK (auth.uid() = user_id); -- Policy untuk UPDATE CREATE POLICY "Users can update their own tasks" ON tasks FOR UPDATE USING (auth.uid() = user_id);
auth.uid() return ID dari authenticated user (dari JWT token)✓ Defense in depth: security di database level, bukan hanya di app logic.
Install Supabase client library:
npm install @supabase/supabase-js
Create lib/supabase.js:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Buat .env.local di root project (jangan commit ke git!):
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_ prefix membuat env var accessible di client-sideanon key safely exposed di client karena protected by RLSimport { supabase } from '@/lib/supabase';
import { useEffect, useState } from 'react';
export default function TaskList() {
const [tasks, setTasks] = useState([]);
useEffect(() => {
async function fetchTasks() {
const { data, error } = await supabase
.from('tasks')
.select('*')
.order('created_at', { ascending: false });
if (error) console.error(error);
else setTasks(data);
}
fetchTasks();
}, []);
return (
<ul>
{tasks.map(task => <li key={task.id}>{task.title}</li>)}
</ul>
);
}
// INSERT const { data, error } = await supabase .from('tasks') .insert({ title: 'New task', user_id: userId }); // UPDATE await supabase .from('tasks') .update({ completed: true }) .eq('id', taskId); // DELETE await supabase .from('tasks') .delete() .eq('id', taskId);
.from('tasks') select table.eq('id', value) adalah WHERE conditionerror and handle gracefully"Perubahan data langsung stream ke clients. Perfect untuk collaborative apps."
useEffect(() => {
const channel = supabase
.channel('tasks-channel')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'tasks' },
(payload) => {
console.log('Change received!', payload);
// Update local state based on event type
if (payload.eventType === 'INSERT') {
setTasks(prev => [payload.new, ...prev]);
}
}
)
.subscribe();
return () => { supabase.removeChannel(channel); };
}, []);
async function toggleTask(taskId) {
// 1. Optimistically update UI immediately
setTasks(prev => prev.map(t =>
t.id === taskId ? { ...t, completed: !t.completed } : t
));
// 2. Send request to database
const { error } = await supabase
.from('tasks')
.update({ completed: true })
.eq('id', taskId);
// 3. Rollback if error
if (error) {
setTasks(prev => prev.map(t =>
t.id === taskId ? { ...t, completed: !t.completed } : t
));
alert('Update failed');
}
}
Apakah data structure menghindari redundancy? Apakah update anomalies bisa terjadi?
Apakah foreign keys benar? Apakah cascade delete/update appropriate?
Apakah column types optimal? INT vs BIGINT, VARCHAR vs TEXT?
Apakah ada NOT NULL, UNIQUE, CHECK yang diperlukan?
Apakah kolom yang sering di-query punya index?
## Schema Review: tasks table ### Normalization: ✅ User data dipisah ke tabel terpisah (auth.users). Tidak ada redundant fields. ### Relationships: ✅ Foreign key ke auth.users dengan ON DELETE CASCADE appropriate (delete user → delete tasks miliknya). ### Data Types: ✅ BIGSERIAL untuk id (support hingga 9 quintillion rows). TEXT untuk title (unlimited length, appropriate untuk user input). TIMESTAMPTZ untuk timezone-aware timestamps. ### Constraints: ⚠️ Missing: priority validation jika ada kolom priority. Consider: ADD CHECK (priority BETWEEN 1 AND 5). ### Indexing: ⚠️ Missing index on user_id untuk fast queries per user. Recommendation: CREATE INDEX idx_tasks_user_id ON tasks(user_id); ### Keputusan: REQUEST CHANGES (add index)
Edge cases, error handling.
SQL injection, auth checks.
N+1 queries, missing indexes.
Readable, documented.
Loading states, error messages.
Pagination, rate limiting.
Supabase best practices.
AI sering lupa NOT NULL, UNIQUE, CHECK. Result: garbage data masuk.
Query slow untuk table besar. Add index on frequently queried columns.
ON DELETE CASCADE tanpa pertimbangan bisa accidental mass delete.
VARCHAR(255) untuk email saat TEXT lebih flexible, atau INT saat BIGINT needed.
Table dengan RLS enabled tapi no policies = semua access denied.
N+1 problem: fetch di loop bukan batch. Use JOIN atau single query.
Test di production-like scenario: slow network, multiple users, edge cases. Jangan hanya happy path.
Add category support dengan many-to-many relationship.
Add kolom priority dan due_date dengan constraints.
Full-text search dengan PostgreSQL to_tsvector.
Shared tasks dengan permissions table.
Audit trail table untuk track perubahan.
Generate CSV atau JSON export dari tasks.
Authentication & Authorization — real user accounts, sessions, role-based access control, dan protected routes.
Deployment & Production — environment setup, CI/CD, monitoring, dan scalability considerations.
"Aplikasi Anda bukan lagi demonstrasi teknis di satu browser. Ini adalah sistem yang nyata—data persisten, multi-user, secure, dan scalable."
Anda sekarang seorang full-stack developer. Frontend, backend, dan database—semua dalam satu stack yang cohesive.
Bab 8 selesai. Anda telah membangun fondasi full-stack yang solid.
Next: Authentication, user sessions, dan protected routes di Bab 9.