SETUP.md ishga tushirish bo‘yicha qisqacha. Bu fayl — backend’ning to‘liq
ichidan tushuntirilishi: arxitektura, baza, JWT, RLS, storage, realtime,
webhooks, edge funksiyalar, monitoring va xavfsizlik.
Texnik tildagi qismlari ingliz tilida — chunki SQL/JWT/HTTP standartlari shunday. Tushuntirishlari o‘zbekcha.
- Arxitektura
- Lokal dev environment (Supabase CLI)
- Database — schema, indekslar, triggerlar
- Row-Level Security (RLS)
- Auth — JWT, session, refresh, custom claims
- Role-Based Access Control (RBAC)
- Storage — buckets, signed URL, fayl yuklash
- Realtime — kanallar va obunalar
- Email — Resend SMTP, transactional shablonlar
- Server actions va API qatlam
- Edge functions (zarur bo‘lganda)
- Webhooks va database triggerlar
- Backup, restore, migratsiya
- Monitoring va loglar
- Xavfsizlik checklist
┌──────────────────────────────────────────────────────────────┐
│ Vercel (Edge Network) │
│ ├─ Next.js App Router │
│ │ ├─ React Server Components (server-rendered) │
│ │ ├─ Client Components (interactivity) │
│ │ ├─ Server Actions (mutations, no API needed) │
│ │ └─ middleware.ts (auth gate, /admin role check) │
│ └─ Static assets (favicon, OG, sitemap) │
└──────────────┬───────────────────────────────────────────────┘
│ HTTPS (PostgREST + GoTrue + Storage + Realtime)
▼
┌──────────────────────────────────────────────────────────────┐
│ Supabase (managed Postgres + services) │
│ ├─ PostgreSQL 15 (your database, RLS-protected) │
│ ├─ GoTrue (auth: email, OAuth, magic link, JWT) │
│ ├─ PostgREST (auto REST API on schema) │
│ ├─ Realtime (Postgres logical replication → WebSocket) │
│ ├─ Storage (S3-compatible, RLS-protected files) │
│ └─ Edge Functions (Deno, optional) │
└──────────────┬───────────────────────────────────────────────┘
│ SMTP (587/465)
▼
┌──────────────────────────────────────────────────────────────┐
│ Resend (email delivery) │
│ └─ Transactional email — confirm signup, magic link, recovery│
└──────────────────────────────────────────────────────────────┘
Asosiy xulosa: Siyoh’da “backend serveri” yo‘q — siz to‘g‘ridan-to‘g‘ri Supabase’dan o‘qiysiz/yozasiz. Auth to‘g‘ri sozlanganida, RLS har bir so‘rov xavfsizligini bazani ichida ta‘minlaydi.
Lokalda baza, auth, storage’ni Docker’da ishga tushirish — production’ga o‘xshash muhit + bepul.
# Windows
scoop install supabase
# yoki
npm i -g supabase
# macOS / Linux
brew install supabase/tap/supabasecd siyoh-web
supabase init # supabase/ papkasini yaratadi (allaqachon bor)
supabase start # Docker konteynerlar ishga tushadiCLI sizga lokal API URL, anon key, service_role key beradi. Ularni
.env.local ga yozing — production keylarini ezmaydi.
# yangi migratsiya yaratish
supabase migration new add_user_settings
# editorda yozing, keyin
supabase db reset # lokalni qayta yaratadi (drop + apply)
supabase db push # production’ga yuboradiAvval
supabase login+supabase link --project-ref <ref>kerak.
supabase/schema.sql — to‘liq sxema. Asosiy jadvallar:
| Jadval | Maqsad | Muhim ustunlar |
|---|---|---|
profiles |
auth.users ga 1:1 ulangan public profil |
id (UUID, FK to auth.users), username (unique), role (enum) |
stories |
Hikoyalar (matn / audio / both) | slug (unique), status, author_id, tags[], published_at |
comments |
Hikoyaga sharhlar | story_id, author_id, body |
likes |
Yoqtirishlar (kompozit PK) | (story_id, user_id) |
bookmarks |
Saqlanganlar | (story_id, user_id) |
follows |
Kuzatuvlar | (follower_id, following_id), check <> |
contests |
Tanlovlar | status, starts_at, ends_at, prize |
contest_entries |
Tanlov ishtirokchilari | (contest_id, story_id) unique |
notifications |
Foydalanuvchi bildirishnomalari | user_id, kind, payload (JSONB), read_at |
reports |
Moderatsiya navbati | target_kind, target_id, reason, status |
profiles_username_idx -- unique constraint = avtomatik index
stories_author_idx -- profil sahifasidagi list uchun
stories_status_idx -- published filter
stories_published_idx -- order by published_at DESC
stories_tags_idx (GIN) -- tags @> ARRAY['Esse'] kabi so‘rovlar uchun
comments_story_idx -- detail sahifa
notifications_user_idx -- user_id, created_at DESC| Trigger | Vazifa |
|---|---|
on_auth_user_created (auth.users) |
Yangi signup → profiles qatori avtomatik (display_name, username metadata’dan) |
likes_bump |
Like qo‘shilsa/o‘chsa → stories.likes counteri yangilanadi (denormalizatsiya) |
*_touch |
Har qanday update’da updated_at = now() |
public.is_admin() — har bir RLS siyosatida ishlatish uchun helper funksiya
(STABLE, security definer):
create or replace function public.is_admin()
returns boolean language sql stable as $$
select exists (select 1 from profiles where id = auth.uid() and role = 'admin');
$$;Asosiy printsip: har bir public jadvalda RLS yoqilgan. Hech bir foydalanuvchi boshqasining ma‘lumotini ko‘rmasin/o‘zgartirmasin, agar siyosat ruxsat bermasa.
-- 1) Hammaga publish qilingan hikoyalarni ko‘rish, mualifga + adminga hammasini
create policy "stories public read published" on public.stories for select
using (status = 'published' or author_id = auth.uid() or public.is_admin());
-- 2) Faqat o‘zining hikoyasini insert qilish (author_id ni boshqa user qila olmaydi)
create policy "stories author insert" on public.stories for insert
with check (author_id = auth.uid());
-- 3) Faqat muallif yoki admin update/delete qila oladi
create policy "stories author update" on public.stories for update
using (author_id = auth.uid() or public.is_admin())
with check (author_id = auth.uid() or public.is_admin());-- Lokal psql session’da yoki Supabase SQL Editor’da
set role authenticated;
set request.jwt.claim.sub to '00000000-0000-0000-0000-000000000001'; -- user UUID
select * from stories; -- bu user nimani ko‘rishi kerak bo‘lsa, o‘shanigina chiqaradi- Anonymous so‘rov (anon key bilan, sessionsiz) —
null. - Server-side,
service_rolebilan — RLS umuman bypass bo‘ladi.
Supabase har bir foydalanuvchiga 2 ta token beradi:
| Token | Yashash muddati | Maqsad |
|---|---|---|
| Access token (JWT) | 1 soat (sozlanadi) | Har bir API so‘rovga Authorization: Bearer ... |
| Refresh token | 30 kun (sozlanadi) | Access token tugaganida yangisini olish |
JWT payload misoli:
{
"aud": "authenticated",
"exp": 1777400000,
"sub": "00000000-0000-0000-0000-000000000001",
"email": "elena@siyoh.app",
"role": "authenticated",
"session_id": "...",
"user_metadata": {
"display_name": "Elena Marquez",
"username": "elena.writes"
},
"app_metadata": {
"provider": "email",
"providers": ["email", "google"]
}
}sub= foydalanuvchi UUID. RLS siyosatlaridaauth.uid()aynan shu.role=authenticated(login qilgan) yokianon. RLS uchun PostgREST roli.user_metadata= signup’da yuborilgan ma’lumot. Foydalanuvchi o‘zi o‘zgartira oladi.app_metadata= faqat server (service_role) yozadi. Trust qilinadigan ma‘lumot uchun (masalan, plan, role).
@supabase/ssr — Next.js uchun rasmiy library:
- Cookies’da (HTTP-only, secure, samesite=lax) — XSS’dan himoya
- Browser’da
localStorageishlatilmaydi (XSS xavfli) - Har bir RSC/server action
cookies()orqali sessionni o‘qiydi
src/lib/supabase/{client,server}.ts aynan shuni ta‘minlaydi.
middleware.ts har bir so‘rovda cookielarni yangilaydi (refresh).
Supabase Dashboard → Settings → Auth:
- JWT expiry: 3600 (1 soat) — default.
- Refresh token reuse interval: 10s.
- Refresh token rotation: ON (tavsiya).
RLS’da is_admin() har safar DB’ga so‘rov yuboradi. Trafik kattalashganda
roleni JWT ichiga qo‘yish samaraliroq. Buning uchun auth.users ga trigger:
-- profiles.role o‘zgarsa, auth.users.raw_app_meta_data ga sinxronlash
create or replace function public.sync_role_to_jwt()
returns trigger language plpgsql security definer as $$
begin
update auth.users
set raw_app_meta_data =
coalesce(raw_app_meta_data, '{}'::jsonb) || jsonb_build_object('role', new.role)
where id = new.id;
return new;
end $$;
drop trigger if exists profiles_role_sync on public.profiles;
create trigger profiles_role_sync
after insert or update of role on public.profiles
for each row execute function public.sync_role_to_jwt();Endi RLS’da:
create or replace function public.is_admin()
returns boolean language sql stable as $$
select coalesce(auth.jwt() ->> 'app_metadata' ->> 'role', '') = 'admin';
$$;Foydalanuvchi yangi rolni olishi uchun logout/login yoki refresh kerak.
SUPABASE_SERVICE_ROLE_KEY — RLS’ni butunlay bypass qiladi. Hech qachon
clientga chiqmasin. Faqat:
- Edge function’da
- Next.js server action’da (clientga qaytmasdan)
- CLI/cron skriptida
profiles.role enum: 'reader' | 'writer' | 'admin'.
| Rol | Nima qila oladi |
|---|---|
reader |
O‘qish, like, bookmark, kuzatish, sharh yozish |
writer |
Yuqoridagilar + hikoya nashr qilish, qoralama saqlash |
admin |
Hammasi + /admin/* paneli, har qanday hikoyani edit/delete |
writer ↔ reader farq: schema’da hozir bir xil ruxsat (har kim yozishi
mumkin). Agar “verified writer” oqimi qo‘shmoqchi bo‘lsangiz, RLS’ga shart
qo‘shing:
-- Faqat writer/admin nashr qila oladi
drop policy if exists "stories author insert" on public.stories;
create policy "stories author insert" on public.stories for insert
with check (
author_id = auth.uid()
and exists (
select 1 from profiles
where id = auth.uid() and role in ('writer', 'admin')
)
);Adminni qo‘yish:
update profiles set role = 'admin' where username = 'sizning_username';schema.sql ikki bucket yaratadi:
covers(public read) — hikoya muqovalariaudio(public read) — audio yozuvlar
Yo‘l konventsiyasi: <user_id>/<filename>.
-- Ochiq read
create policy "covers public read" on storage.objects for select
using (bucket_id in ('covers', 'audio'));
-- Faqat o‘zining papkasiga yozish
create policy "covers user upload" on storage.objects for insert
with check (
bucket_id in ('covers', 'audio')
and auth.role() = 'authenticated'
and (storage.foldername(name))[1] = auth.uid()::text
);import { createClient } from '@/lib/supabase/client';
async function uploadCover(file: File, userId: string) {
const sb = createClient()!;
const path = `${userId}/${Date.now()}-${file.name}`;
const { data, error } = await sb.storage
.from('covers')
.upload(path, file, { contentType: file.type, cacheControl: '3600' });
if (error) throw error;
return sb.storage.from('covers').getPublicUrl(path).data.publicUrl;
}Agar audioga faqat sotib olganlar kira olsin desangiz, bucket’ni private qilib qo‘ying va talab paytida signed URL bering:
const { data } = await sb.storage
.from('audio')
.createSignedUrl(path, 3600); // 1 soat amal qiladiSupabase logical replication orqali Postgres o‘zgarishlarini WebSocket bilan clientga yetkazadi.
'use client';
import { useEffect } from 'react';
import { createClient } from '@/lib/supabase/client';
useEffect(() => {
const sb = createClient();
if (!sb) return;
const ch = sb
.channel('notif')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'notifications', filter: `user_id=eq.${userId}` },
(payload) => {
// toast yoki badge yangilash
console.log('new notification:', payload.new);
})
.subscribe();
return () => { sb.removeChannel(ch); };
}, [userId]);sb.channel(`story-${storyId}`)
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'comments', filter: `story_id=eq.${storyId}` },
(payload) => setComments(c => [payload.new, ...c]))
.subscribe();- Realtime publication’ga jadval qo‘shilishi kerak. Schema’da
notificationsvacommentsdefaultda qo‘shilmagan — agar realtime kerak bo‘lsa:alter publication supabase_realtime add table notifications; alter publication supabase_realtime add table comments;
- Free planda 200 ta concurrent connection.
SETUP.md → Auth bo‘limida to‘liq. Qisqacha:
- Resend → Domains → Add
siyoh.app→ DNS yozuvlarini qo‘shing → tasdiqlang. - Resend → API Keys → Create → key olib oling.
- Supabase → Integrations → Resend (yoki Settings → Auth → SMTP):
Host: smtp.resend.com Port: 465 User: resend Password: <Resend API key> Sender: Siyoh <team@siyoh.app> - Authentication → Email Templates —
supabase/email-templates/*.htmlshablonlarini joylashtiring.
- Supabase ichidagi SMTP: 30 email / soat / loyiha (juda past — faqat dev).
- Resend free: 100/kun, 3000/oy. Domen tasdiqlangan bo‘lsa.
- Production’da albatta o‘zingizning SMTP (Resend, Postmark, SendGrid).
Resend → Webhooks → endpoint URL:
https://siyoh.app/api/webhooks/resend
Event’lar: email.delivered, email.bounced, email.complained. Bu URL’ni
src/app/api/webhooks/resend/route.ts ga qo‘shib, notifications jadvalga
yozishingiz mumkin.
Siyoh API endpoint yozmaydi. Ma‘lumotni o‘zgartirish — Server Actions orqali
(src/lib/actions.ts):
'use server';
export async function toggleLike(storyId: string, currentlyLiked: boolean) {
const { sb, userId } = await requireUser();
if (currentlyLiked) {
await sb.from('likes').delete().eq('story_id', storyId).eq('user_id', userId);
} else {
await sb.from('likes').insert({ story_id: storyId, user_id: userId });
}
revalidatePath('/feed');
return { ok: true, liked: !currentlyLiked };
}Client tomondan:
const [pending, start] = useTransition();
function onClick() {
start(async () => {
const r = await toggleLike(id, liked);
if (!r.ok) push({ kind: 'error', title: r.error });
});
}Afzalliklari:
- API route yozish kerak emas
- Authentication cookie orqali avtomatik
- Type-safe (function signature client+server bir)
- Optimistic UI bilan birga ishlaydi
- Tashqi tizimdan webhook (Resend, Stripe, Telegram bot)
- OG image, sitemap, robots — Next.js metadata API
- Cron job ichidan chaqiriladigan endpoint
src/app/api/<name>/route.ts faylida GET/POST export qiling.
Aksariyat ish Server Action’da hal bo‘lsa kerak emas. Kerak bo‘lgan vaqt:
- Stripe checkout (server-side secret bilan)
- AI moderatsiya (OpenAI key)
- 3rd-party API’lar uchun proxy
- Cron-based ishlar (har soatda email digest)
supabase functions new digest-weekly
# supabase/functions/digest-weekly/index.ts
supabase functions deploy digest-weeklyCron triggers pg_cron orqali:
select cron.schedule(
'weekly-digest',
'0 9 * * 0', -- yakshanba 09:00
$$ select net.http_post(
url:='https://<project>.functions.supabase.co/digest-weekly',
headers:='{"Authorization":"Bearer <SERVICE_ROLE>"}'::jsonb
);
$$
);Dashboard → Database → Webhooks → yangi qoida:
- Table:
stories - Events: INSERT
- HTTP method: POST
- URL:
https://siyoh.app/api/webhooks/new-story - Headers:
Authorization: Bearer <internal-secret>
Hikoya nashr qilinganida sizga POST keladi → Telegram kanaliga, Twitter’ga yuborishingiz mumkin.
-- Yangi follower kelganda push notification
create or replace function public.notify_followed_user()
returns trigger language plpgsql as $$
begin
insert into notifications (user_id, kind, payload)
values (new.following_id, 'follow', jsonb_build_object('from', new.follower_id));
return new;
end $$;
drop trigger if exists follows_notify on public.follows;
create trigger follows_notify after insert on public.follows
for each row execute function public.notify_followed_user();Sxema’da actions.ts notification’ni JS tomondan yozadi — siz bu vazifani
DB triggerga ko‘chirishingiz mumkin (transaktsion kafolat uchun).
Supabase → Database → Backups:
- Free: kuniga, 7 kun ushlanadi
- Pro: kuniga, 14 kun
- Pro + PITR: point-in-time recovery, 7 kun
supabase db dump -f backup.sql # full schema + data
supabase db dump --data-only -f data.sql # faqat data
supabase db dump --schema-only -f schema.sql # faqat schemasupabase link --project-ref xxxxxxxx
supabase db push # supabase/migrations/* ni qo‘llaydischema.sql ni o‘zgartirish o‘rniga migratsiya yozing — versiyalanadi va
qaytarib oluvchi (rollback) bo‘ladi.
- Logs → Postgres — slow query, error
- Logs → Auth — login attempt, signup, password reset
- Logs → API — PostgREST so‘rov logi
- Reports → Database — connection count, table size, index hit rate
- Analytics — sahifa view, web vitals
- Logs — Server Action xatolari, middleware
- Speed Insights — INP, LCP, CLS
npm i @sentry/nextjs
npx @sentry/wizard@latest -i nextjsProduction’ga oldin:
- Hech qanday
.env.localgit’ga commit qilinmagangit ls-files | grep -E "\.env\.local$" # bo‘sh chiqsin
-
SUPABASE_SERVICE_ROLE_KEYfaqat server kodidagrep -r "SERVICE_ROLE" src/ | grep -v "from.*supabase/server" # bo‘sh chiqsin
- Har bir public jadvalda
alter table ... enable row level security -
policyhar bir SELECT/INSERT/UPDATE/DELETE uchun yozilgan - Resend API key faqat Supabase SMTP’da (kodda yo‘q)
- Email domen Resend’da tasdiqlangan (DKIM/SPF passing)
- Site URL + Redirect URLs faqat ishonchli domen (lokal’dan tashqari)
- Auth → Rate limit to‘g‘ri sozlangan (signup spam himoya)
- OAuth client secret faqat Supabase ichida — kod’da yo‘q
- Vercel env’lar production-only (preview deploy’larga test loyihasi)
-
X-Frame-Options,X-Content-Type-Options—vercel.jsonda bor - PostgreSQL
search_path— security definer funksiyalardaset search_path = public(CVE-2018-1058 himoya) - Backup test — bir marta restore qilib ko‘ring
Anonymous so‘rov qilib ko‘ring (anon key bilan):
curl -H "apikey: $ANON_KEY" \
"$URL/rest/v1/profiles?role=eq.admin&select=*"
# Bu so‘rov hech narsa qaytarmasligi kerak — RLS adminlarni yashirsin.curl -X POST -H "apikey: $ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"author_id":"some-other-uuid","title":"hack"}' \
"$URL/rest/v1/stories"
# Bu so‘rov 401 yoki 403 bo‘lishi kerak.GitHub: github.com/AzizX-coder/Siyoh-Web
Sekinlashishga arziydigan hikoyalar.