# Što ćeš izgraditi i zašto je to važno#
Ovaj vodič prikazuje siguran, end-to-end model autorizacije koristeći Supabase Row Level Security i Next.js App Router. Cilj je jednostavan: bez obzira što korisnik radi u pregledniku, može čitati i pisati samo one retke kojima smije pristupiti.
“Supabase RLS s Next.js” najbolje radi kada bazu tretiraš kao točku provedbe (enforcement), a Next.js kao sloj orkestracije. Ako podatke filtriraš samo u React komponentama ili API rutama, korisnik te može zaobići pozivajući Supabase direktno s istom sesijom.
Za dublje multi-tenant detalje i dodatne primjere, pročitaj i:
# Model prijetnji: opasne zamke protiv kojih moraš dizajnirati#
Bugovi u autorizaciji su skupi. IBM-ovo izvješće Cost of a Data Breach konzistentno pokazuje prosječne troškove proboja u milijunima USD, a broken access control i dalje je među najvišim OWASP kategorijama rizika. U multi-tenant SaaS-u, jedna propuštena tenant provjera može prerasti u izlaganje podataka između tenantova.
U kombinaciji Supabase + Next.js, većina incidenata dolazi iz nekoliko predvidljivih izvora:
- Filteri na klijentu koji skrivaju podatke u UI-u, ali ne sprječavaju upite.
- Curenje service role ključa koje trenutno zaobilazi RLS.
- Nesiguran RPC gdje funkcija koristi
SECURITY DEFINERili zaboravi tenant provjere. - “Prečaci” na serveru gdje developeri koriste admin klijente radi praktičnosti i zaborave ponovno provjeriti autorizaciju.
🎯 Ključna poruka: Ako se do baze može doći s korisničkom sesijom, baza mora provoditi autorizaciju. RLS je granica provedbe.
# Preduvjeti#
| Zahtjev | Verzija | Napomene |
|---|---|---|
| Next.js | 14 ili 15 | Pretpostavlja se App Router |
| Supabase projekt | Bilo koji aktualni | Postgres s RLS-om |
| Auth | Supabase Auth | Email, OAuth ili SSO |
| Znanje Postgresa | Osnovno | Tablice, strani ključevi, politike |
# Sigurna osnovna arhitektura#
U praksi tvoja aplikacija ima tri “uloge”:
| Kontekst | Supabase ključ | Može zaobići RLS | Tipična upotreba |
|---|---|---|---|
| Klijent u pregledniku | anon | Ne | Korisnik čita i piše pod RLS-om |
| Server u korisničkom kontekstu | anon + korisnička sesija | Ne | Server Components i Route Handlers koji djeluju kao korisnik |
| Server u admin kontekstu | service role | Da | Pozadinski poslovi, webhookovi, support alati |
Najsigurniji default je: nikad nemoj koristiti service role u putanji zahtjeva koja potječe od korisničkog klika. Ako baš moraš, tretiraj to kao pisanje vlastitog authorization sloja i računaj na greške.
⚠️ Upozorenje: Service role ključ u
NEXT_PUBLIC_*, client bundleovima, logovima ili error telemetryju je praktički potpuni kompromis baze. Odmah ga rotiraj ako je izložen.
# Model podataka za multi-tenant aplikaciju#
Pouzdan multi-tenant model izbjegava “magične” claimove i sve drži eksplicitno u tablicama. Koristi:
tenantskao top-level workspace ili organizaciju.tenant_membersza povezivanje korisnika s tenantima i ulogama.- Tenant-scoped tablice koje sadrže
tenant_id.
Primjer sheme#
| Tablica | Svrha | Ključni stupci |
|---|---|---|
| tenants | Workspace spremnik | id, name, created_at |
| tenant_members | Članstvo i uloga | tenant_id, user_id, role |
| projects | Resurs vezan uz tenant | id, tenant_id, name |
| tasks | Dijete od projects | id, tenant_id, project_id, title |
Koristi tenant_id na svakoj tenant-scoped tablici čak i ako ga možeš izvesti preko project_id. To pojednostavljuje politike, poboljšava performanse i smanjuje broj bugova u politikama.
SQL: shema i ograničenja#
-- tenants
create table public.tenants (
id uuid primary key default gen_random_uuid(),
name text not null,
created_at timestamptz not null default now()
);
-- membership
create table public.tenant_members (
tenant_id uuid not null references public.tenants(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
role text not null check (role in ('owner','admin','member')),
created_at timestamptz not null default now(),
primary key (tenant_id, user_id)
);
-- projects
create table public.projects (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
name text not null,
created_by uuid not null references auth.users(id),
created_at timestamptz not null default now()
);
-- tasks
create table public.tasks (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
project_id uuid not null references public.projects(id) on delete cascade,
title text not null,
created_by uuid not null references auth.users(id),
created_at timestamptz not null default now()
);
create index on public.tenant_members (user_id, tenant_id);
create index on public.projects (tenant_id);
create index on public.tasks (tenant_id, project_id);# Uključi RLS i definiraj provjeru članstva koja se može ponovno koristiti#
Uključi RLS za svaku tenant-scoped tablicu, zatim centraliziraj logiku članstva. U Postgresu je najčišći obrazac mala stable SQL funkcija koja provjerava članstvo koristeći auth.uid().
SQL: uključi RLS#
alter table public.tenants enable row level security;
alter table public.tenant_members enable row level security;
alter table public.projects enable row level security;
alter table public.tasks enable row level security;SQL: pomoćna funkcija za tenant pristup#
create or replace function public.is_tenant_member(p_tenant_id uuid)
returns boolean
language sql
stable
as $$
select exists (
select 1
from public.tenant_members tm
where tm.tenant_id = p_tenant_id
and tm.user_id = auth.uid()
);
$$;Ova funkcija se izvršava u kontekstu pozivatelja i i dalje poštuje RLS na tenant_members osim ako ga eksplicitno ne zaobiđeš. To je dobro, ali znači da moraš napisati ispravne politike i za tenant_members.
ℹ️ Napomena: Ako provjere članstva djeluju “kružno”, to obično znači da politika na tablici
tenant_membersnedostaje ili je previše restriktivna. Kreni od toga da membership politike rade kako treba, pa onda gradi dalje.
# Dizajn politika: najmanje privilegije po operaciji#
Dizajniraj politike po operaciji, ne po tablici. Želiš odvojene politike za select, insert, update i delete. Time se smanjuje rizik da slučajno omogućiš write kada si namjeravao samo read.
Politike za tenant_members#
Tipična pravila:
- Korisnik može vidjeti vlastite membership retke.
- Owneri i admini mogu upravljati članstvima unutar svog tenanta.
- Samo owneri mogu obrisati drugog ownera.
Evo pragmatične baze za početak.
-- read your own memberships
create policy "tenant_members_read_own"
on public.tenant_members
for select
using (user_id = auth.uid());
-- owners and admins can read all members in their tenant
create policy "tenant_members_read_tenant"
on public.tenant_members
for select
using (
public.is_tenant_member(tenant_id)
);
-- only owners can insert members, and only into their tenant
create or replace function public.is_tenant_owner(p_tenant_id uuid)
returns boolean
language sql
stable
as $$
select exists (
select 1 from public.tenant_members tm
where tm.tenant_id = p_tenant_id
and tm.user_id = auth.uid()
and tm.role = 'owner'
);
$$;
create policy "tenant_members_insert_owner_only"
on public.tenant_members
for insert
with check (
public.is_tenant_owner(tenant_id)
);
-- only owners can update roles, inside their tenant
create policy "tenant_members_update_owner_only"
on public.tenant_members
for update
using (public.is_tenant_owner(tenant_id))
with check (public.is_tenant_owner(tenant_id));Ovo nije kompletan “enterprise” set politika, ali je dovoljno siguran da na njemu iteriraš, i prisiljava te da budeš eksplicitan oko toga tko smije što.
Politike za projects#
Pravila:
- Svaki član tenanta može čitati projekte u svom tenantu.
- Svaki član tenanta može kreirati projekt u svom tenantu.
- Samo članovi mogu raditi update, po želji ograniči update na admine.
- Brisanje je tipično ograničeno na admine ili ownere.
create policy "projects_select_member"
on public.projects
for select
using (public.is_tenant_member(tenant_id));
create policy "projects_insert_member"
on public.projects
for insert
with check (
public.is_tenant_member(tenant_id)
and created_by = auth.uid()
);
create policy "projects_update_member"
on public.projects
for update
using (public.is_tenant_member(tenant_id))
with check (public.is_tenant_member(tenant_id));Politike za tasks uz konzistentnost s roditeljem#
Čest bug je dopuštanje da se tenant_id arbitrarnno postavi na child retku. Uvijek osiguraj da child red pripada istom tenantu kao i njegov parent.
create or replace function public.project_belongs_to_tenant(p_project_id uuid, p_tenant_id uuid)
returns boolean
language sql
stable
as $$
select exists (
select 1 from public.projects p
where p.id = p_project_id
and p.tenant_id = p_tenant_id
);
$$;
create policy "tasks_select_member"
on public.tasks
for select
using (public.is_tenant_member(tenant_id));
create policy "tasks_insert_member_with_parent_check"
on public.tasks
for insert
with check (
public.is_tenant_member(tenant_id)
and created_by = auth.uid()
and public.project_belongs_to_tenant(project_id, tenant_id)
);# Next.js App Router: obrasci provedbe na serveru#
RLS već provodi pristup na sloju podataka. Posao App Routera je osigurati:
- da slučajno ne zaobiđeš RLS koristeći service role
- da čitanja i pisanja strukturiraš tako da se izvršavaju pod korisničkom sesijom
- da ne curiš tenant identifikatore niti vjeruješ tenant inputu koji dolazi od korisnika
Obrazac 1: Server Components za čitanja, Route Handlers za pisanja#
Čitanja često dobro sjedaju u Server Components jer se izvršavaju na serveru i mogu koristiti korisničku sesiju temeljenu na cookiesima. Pisanja bi u pravilu trebala ići kroz Route Handlers gdje možeš validirati input i vraćati jasne greške.
Koristi jedan mehanizam odabira tenanta, tipično:
active_tenant_idspremljen u tabliciprofilespod RLS-om, ili- tenant slug u URL-u, a članstvo se provodi RLS-om.
Ako koristiš URL slugove, tretiraj ih kao routing identifikatore, ne kao authorization provjere. Autorizacija se i dalje događa u RLS-u.
Obrazac 2: Uvijek kreiraj “user-scoped” Supabase klijent na serveru#
U App Routeru želiš server klijent koji čita Supabase sesiju iz cookiesa i koristi anon ključ. To osigurava da se RLS primjenjuje.
// lib/supabase/server.ts
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";
export function createSupabaseServerClient() {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach((c) => cookieStore.set(c.name, c.value, c.options));
},
},
}
);
}Ovo je namjerno bazirano na anon ključu. Sesija ga čini da “djeluje kao korisnik”, i RLS se provodi.
💡 Savjet: Napravi i drugi helper koji zahtijeva korisničku sesiju i baca grešku ako korisnik nije prijavljen. Uklonit ćeš puno rubnih slučajeva “optional auth” i smanjiti broj neautoriziranih stanja.
// lib/auth/require-user.ts
import { createSupabaseServerClient } from "@/lib/supabase/server";
export async function requireUser() {
const supabase = createSupabaseServerClient();
const { data, error } = await supabase.auth.getUser();
if (error || !data.user) throw new Error("Unauthorized");
return { supabase, user: data.user };
}Obrazac 3: Nemoj prihvaćati tenant_id od klijenta osim ako ga validiraš#
Klasičan multi-tenant bug je API ruta koja prihvaća tenant_id i s njim umeće retke. Insert može proći ako su politike previše permisivne ili ako kod slučajno koristi service role.
Umjesto toga:
- izvedi
tenant_idiz URL-a i pusti RLS da blokira nečlanove, ili - dohvatite tenantove kojima korisnik smije pristupiti i provjeri članstvo prije nastavka.
Evo sigurnog obrasca za Route Handler koji čita tenant slug iz paramsa, resolvea ga, pa radi insert.
// app/api/tenants/[tenantId]/projects/route.ts
import { NextResponse } from "next/server";
import { requireUser } from "@/lib/auth/require-user";
export async function POST(req: Request, ctx: { params: Promise<{ tenantId: string }> }) {
const { supabase, user } = await requireUser();
const { tenantId } = await ctx.params;
const body = await req.json();
const name = String(body?.name ?? "").trim();
if (name.length < 2) return NextResponse.json({ error: "Invalid name" }, { status: 400 });
const { data, error } = await supabase
.from("projects")
.insert({ tenant_id: tenantId, name, created_by: user.id })
.select("id, tenant_id, name")
.single();
if (error) return NextResponse.json({ error: error.message }, { status: 403 });
return NextResponse.json({ project: data });
}Ovo i dalje prima tenantId iz URL-a, ali mu nikad ne vjeruje za autorizaciju. Ako korisnik nije član, RLS odbija insert.
Obrazac 4: Izbjegavaj service-role u Route Handlerima#
Ako koristiš service role u handleru iznad, insert će uvijek proći, čak i za nečlanove, osim ako ručno implementiraš provjere. To je najčešća “radilo je u devu” regresija autorizacije koju viđamo u auditima.
Ako ti trebaju admin-level operacije:
- izoliraj ih u zasebne endpointove
- zaštiti admin autentikacijom
- implementiraj eksplicitne provjere
- logiraj svaki poziv
- razmisli o prebacivanju u background jobs
# RPC i “nesigurne funkcije”: zona visokog rizika#
RPC je koristan za kompleksne operacije, ali je i mjesto gdje timovi slučajno isključe RLS.
Evo sigurnog baselinea:
| RPC izbor | Izvršava se kao | RLS se primjenjuje | Rizik |
|---|---|---|---|
| Default funkcija | invoker | Da | Nizak, ako postoje tenant provjere |
SECURITY DEFINER | definer | Ne, osim ako se prisili | Visok |
| Definer + ručne provjere | definer | Ne | Srednji, lako je propustiti provjere |
Ako koristiš RPC za write preko više tablica, preferiraj:
- invoker mode
- eksplicitne provjere tenant članstva unutar funkcije
- minimalnu površinu (surface area)
Primjer: relativno siguran invoker RPC#
create or replace function public.create_task(p_tenant_id uuid, p_project_id uuid, p_title text)
returns public.tasks
language plpgsql
as $$
declare
v_task public.tasks;
begin
if not public.is_tenant_member(p_tenant_id) then
raise exception 'not a tenant member';
end if;
if not public.project_belongs_to_tenant(p_project_id, p_tenant_id) then
raise exception 'project not in tenant';
end if;
insert into public.tasks (tenant_id, project_id, title, created_by)
values (p_tenant_id, p_project_id, p_title, auth.uid())
returning * into v_task;
return v_task;
end;
$$;Ovo se i dalje oslanja na RLS za čitanja tablica koja se rade drugdje, ali forsira tenant provjere odmah na početku.
⚠️ Upozorenje: Izbjegavaj
SECURITY DEFINERosim ako imaš jak razlog i temeljit proces reviewa. Mnoga produkcijska curenja nastanu jer definer funkcija dopušta cross-tenant čitanja bez uključivanja politika.
# End-to-End tok: sigurno multi-tenant čitanje i pisanje#
Dobar “E2E authorization” test je: može li korisnik s valjanom sesijom čitati podatke drugog tenanta mijenjanjem identifikatora.
Read tok u Server Componentu#
- Komponenta dobiva
tenantIdiz route paramsa. - Queryja
projectspotenant_id. - RLS osigurava da su vidljivi samo retci za članove.
// app/(app)/tenants/[tenantId]/projects/page.tsx
import { createSupabaseServerClient } from "@/lib/supabase/server";
export default async function ProjectsPage(props: { params: Promise<{ tenantId: string }> }) {
const { tenantId } = await props.params;
const supabase = createSupabaseServerClient();
const { data: projects, error } = await supabase
.from("projects")
.select("id, name, created_at")
.eq("tenant_id", tenantId)
.order("created_at", { ascending: false });
if (error) throw new Error("Not authorized or query failed");
return (
<main>
<h1>Projects</h1>
<ul>
{(projects ?? []).map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</main>
);
}Čak i ako korisnik promijeni URL na drugi tenant ID, upit vraća prazan skup ili authorization grešku, ovisno o tvojoj politici i ponašanju PostgREST-a.
Write tok u Route Handleru#
Insert primjer si već vidio. Ključna poanta je: server provodi autentikaciju i validaciju inputa, baza provodi autorizaciju.
# Učvršćivanje politika za stvarni SaaS#
Baseline politike iznad su ispravne, ali nisu kompletne. Evo čestih dodataka koji sprječavaju suptilne multi-tenant bugove.
Dodaj immutable ograničenja za tenant_id#
Spriječi update koji premješta retke između tenantova. Čak i ako UI to nikad ne radi, napadači će pokušati.
create policy "projects_prevent_tenant_move"
on public.projects
for update
using (public.is_tenant_member(tenant_id))
with check (tenant_id = (select tenant_id from public.projects p where p.id = id));Ovaj obrazac može biti nezgrapan. Mnogi timovi to rješavaju ovako:
- ograničavanjem updatea na određene stupce koristeći viewove, ili
- triggerima koji blokiraju promjene
tenant_id
Osiguraj da child retci ne mogu “odlutati” između tenantova#
Već si dodao project_belongs_to_tenant. Primijeni ga i na update, ne samo na insert.
create policy "tasks_update_member_with_parent_check"
on public.tasks
for update
using (public.is_tenant_member(tenant_id))
with check (
public.is_tenant_member(tenant_id)
and public.project_belongs_to_tenant(project_id, tenant_id)
);Izbjegni “policy sprawl” dosljednim imenovanjem i predlošcima#
U multi-tenant aplikacijama stvorit ćeš desetke politika. Koristi konvenciju imenovanja poput:
{table}_{op}_{who}_{constraints}
Primjer:tasks_insert_member_parent_check
To ubrzava audite i PR review.
# Kontrolna lista: česte greške koje lome autorizaciju#
Koristi ovo kao gate prije releasea. Hvata većinu stvarnih problema koje viđamo u Supabase + Next.js projektima.
Curenje service role ključa i pogrešna upotreba#
- Service role ključ nije ni u jednoj
NEXT_PUBLIC_*env varijabli. - Service role ključ se ne koristi ni u jednom Server Componentu ili Route Handleru koji radi u ime krajnjeg korisnika.
- Service role ključ se koristi samo u izoliranim admin jobovima, webhookovima ili pozadinskoj obradi.
- Logovi i error trackeri ne hvataju environment varijable ili headere koji sadrže tajne.
Nesiguran RPC i definer funkcije#
- Ne postoje
SECURITY DEFINERfunkcije bez dokumentiranog razloga i reviewa. - Svaki RPC koji prima
tenant_idprovjerava članstvo unutar funkcije. - RPC funkcije ne vraćaju cross-tenant agregate osim ako je to eksplicitno namijenjeno.
- Politike se i dalje primjenjuju na tablice koje RPC dira, ili ih provjere u potpunosti zamjenjuju.
Filtriranje na klijentu i “autorizacija samo u UI-u”#
- Nijedna stranica se ne oslanja na skrivanje UI elemenata kao jedinu autorizaciju.
- Svaki query koji uključuje tenant identifikator je siguran čak i ako se identifikator promijeni.
- Ne postoji “dohvati sve pa filtriraj u Reactu” za tenant-scoped podatke.
- API rute validiraju inpute, ali ne pokušavaju ponovno implementirati logiku row autorizacije.
RLS pokrivenost i ispravnost politika#
- RLS je uključen na svakoj tenant-scoped tablici.
- Svaka tablica ima politike za
select,insert,updateideletepo potrebi. - Politike provjeravaju i članstvo i konzistentnost parent/child za child tablice.
- Postoje indeksi koji podržavaju policy provjere, posebno na
tenant_members(user_id, tenant_id).
# Testiranje: dokaži da je cross-tenant pristup nemoguć#
Nemoj isporučiti bez barem ovih testova:
- 1Korisnik A kreira tenant, projekt i task.
- 2Korisnik B je u drugom tenantu.
- 3Provjeri da Korisnik B ne može:
- selektirati projekte Korisnika A mijenjanjem
tenant_id - ubaciti task u projekt Korisnika A
- uspješno pozvati RPC s
tenant_idKorisnika A
- 4Provjeri očekivana admin ponašanja, npr. owner upravlja članovima.
Jednostavan automatizirani pristup je pokrenuti integracijske testove koji:
- prijave se s dva testna korisnika
- izvrše iste requestove s različitim cookiesima
- asertaju prazne rezultate ili 403 greške
# Ključne poruke#
- Tretiraj RLS kao granicu autorizacije, a Next.js kao orkestraciju, ne kao provedbu.
- Koristi multi-tenant shemu s eksplicitnim
tenant_idna svakoj tenant-scoped tablici i provodi članstvo prekotenant_members. - Koristi server-side Supabase klijente s anon ključem + korisničkom sesijom kako bi RLS ostao aktivan u Server Components i Route Handlers.
- Izbjegavaj service role u user request putanjama, a admin operacije izoliraj iza eksplicitnih provjera i logiranja.
- Budi iznimno oprezan s RPC-om, posebno sa svime što nalikuje
SECURITY DEFINER, i validiraj tenant pristup unutar funkcija. - Koristi kontrolnu listu da uhvatiš tri velika kvara: curenje service role ključa, nesiguran RPC i filtere na klijentu.
# Zaključak#
Supabase RLS plus Next.js App Router je snažna kombinacija kada dosljedno izvršavaš upite pod korisničkom sesijom i prepuštaš bazi da provodi tenant granice. Najbrži put do sigurnog releasea je krenuti s čistom multi-tenant shemom, implementirati politike najmanjih privilegija po operaciji i zabraniti service-role prečace u endpointovima koje pokreću korisnici.
Ako želiš da pregledamo tvoje politike, učvrstimo tvoje App Router server obrasce ili dizajniramo multi-tenant authorization model koji skalira bez iznenađenja, kontaktiraj Samioda putem naše stranice za web development ili nastavi s našim vodičima o multi-tenant RLS-u, App Router authz obrascima i širom sigurnosnom checklistom.
FAQ
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Web razvoj
Sve →Rukovanje greškama u Reactu u 2026.: Error Boundaries, ponovni pokušaji i UX obrasci koji skaliraju
Praktičan, slojeviti vodič za obrasce rukovanja greškama u Reactu u 2026.: granice grešaka na razini komponente i rute, ponovni pokušaji u TanStack Query i SWR-u te UX poruke koje skaliraju kroz timove.
Next.js + Supabase Edge Functions: Praktična arhitektura za moderni SaaS (vodič za 2026.)
Produkcijski spremna Next.js + Supabase Edge Functions arhitektura za SaaS: što ide u Edge Functions, a što u API rute/server actions, kako strukturirati module te kako sigurno deployati na Vercel ili Cloudflare.
Testiranje ugovora za React komponente: MSW + Storybook kao živi API mockovi
Praktičan vodič za testiranje ugovora React komponenti pomoću zajedničkih MSW handlera u Storybooku i automatiziranim testovima kako biste spriječili razilaženje mockova, nestabilan UI i regresije u CI-u.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Autorizacija u Next.js App Routeru: RBAC vs ABAC uz middleware, RLS i obrasce politika
Praktičan vodič za autorizaciju u Next.js App Routeru uz RBAC i ABAC — s provjerama u middlewareu, zaštitama u server komponentama, RLS-om koji se provodi u bazi, matricom odluke i gotovim obrascima politika za copy-paste.
Next.js + Supabase RLS za multi‑tenant SaaS: pravila, uloge i siguran pristup podacima
Praktičan vodič za Next.js App Router i Supabase Row Level Security za multi-tenant SaaS: dizajn tablica, pravila, uloge, obrasci pristupa na serveru, česte zamke i kontrolna lista za deployment.
Next.js Supabase Realtime u 2026: End-to-End nacrt za chat, presence i suradnju
Izgradite production-ready realtime UI s Next.js App Routerom i Supabase Realtime: dizajn sheme, RLS, optimistička ažuriranja, presence, skaliranje i rješavanje problema s dupliciranim eventima i neusklađenim dozvolama.