Budowanie SaaS-a w 30 dni jest możliwe. Przeżycie z nim kolejnych 18 miesięcy — to już inna historia. Większość produktów nie ginie z powodu złej idei. Giną dlatego, że architektura, która była wystarczająca na 50 użytkowników, kompletnie się sypie przy 1000. A migracja żywego systemu z płatącymi klientami to scenariusz, którego chcesz uniknąć za wszelką cenę.
Poniżej jest lista decyzji architektonicznych, które musisz podjąć świadomie — najlepiej zanim napiszesz pierwszą linię kodu.
Dlaczego większość SaaS-ów pada przy 1000 użytkownikach
Przy 50 użytkownikach możesz bezkarnie robić wszystko synchronicznie. Użytkownik klika przycisk, backend coś robi, odpowiedź wraca w 200ms. Świetnie. Przy 1000 aktywnych użytkownikach, z których każdy wysyła żądania co kilka sekund, zaczyna się problem.
**Zły schemat bazy danych.** Najczęstszy grzech to brak separacji danych między najemcami (tenants). Tabela `users` bez kolumny `tenant_id`, wspólna tabela `projects` bez Row-Level Security — w najlepszym razie masz wyciek danych między klientami. W najgorszym — utratę klientów i problemy prawne.
**Brak kolejki.** Wysyłanie emaili synchronicznie w handlerze HTTP. Generowanie PDF-ów w requestcie. Wywołania zewnętrznych API bez retry. Każda z tych operacji może się zawiesić na 3–10 sekund i zablokuje cały wątek. Przy tysiącu użytkowników timeout staje się normą.
**Synchroniczne tam, gdzie trzeba asynchronicznie.** Eksport danych, przetwarzanie plików, wysyłka do integracji zewnętrznych — to są zadania dla workerów, nie dla handlerów HTTP.
Schemat bazy: tenants i Row-Level Security
Podstawa architektury multi-tenant to tabela `tenants` i kolumna `tenant_id` w każdej tabeli z danymi użytkowników. Nie ma drogi na skróty.
```sql -- Tabela najemców CREATE TABLE tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, plan TEXT NOT NULL DEFAULT 'free', created_at TIMESTAMPTZ DEFAULT NOW() );
-- Przykład tabeli z danymi przypisanymi do tenanta CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() );
-- Indeks niezbędny dla wydajności CREATE INDEX projects_tenant_id_idx ON projects(tenant_id);
-- Row-Level Security w Supabase/Postgres ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Tenant isolation" ON projects FOR ALL USING (tenant_id = (current_setting('app.current_tenant_id'))::UUID); ```
W Supabase ustawiasz `app.current_tenant_id` przez konfigurację sesji przed każdym zapytaniem. Możesz to zrobić w middleware Next.js lub w Server Action. RLS gwarantuje, że nawet jeśli programista zapomni o `WHERE tenant_id = ?` — baza danych go ochroni.
Supabase Auth dobrze integruje się z tym modelem: każdy użytkownik należy do tenanta, a JWT może zawierać `tenant_id` jako claim.
Kolejka zadań: kiedy dodać BullMQ lub Trigger.dev
Na początku możesz odkładać kolejkę. Ale sygnały, że czas ją dodać, są jednoznaczne: timeouty na endpointach, skargi użytkowników na "ładowanie", retry bez mechanizmu, operacje trwające ponad 2 sekundy w handlerze HTTP.
**BullMQ** to dojrzałe rozwiązanie oparte na Redis. Działa świetnie, gdy masz kontrolę nad infrastrukturą.
```typescript import { Queue, Worker } from 'bullmq'; import IORedis from 'ioredis';
const connection = new IORedis(process.env.REDIS_URL!);
// Definicja kolejki export const emailQueue = new Queue('email', { connection });
// Dodanie zadania await emailQueue.add('send-welcome', { to: user.email, tenantId: tenant.id, template: 'welcome', });
// Worker — uruchamiany osobno const worker = new Worker( 'email', async (job) => { const { to, template } = job.data; await sendEmail({ to, template }); }, { connection } );
worker.on('failed', (job, err) => { console.error(`Job ${job?.id} failed:`, err); }); ```
**Trigger.dev** to alternatywa hostowana — nie musisz zarządzać Redisem. Świetne dla zespołów, które chcą zero-ops od dnia pierwszego. Droższe przy większej skali, tańsze w maintenance.
Wybór prosty: jeśli masz już VPS lub korzystasz z Railway/Render — BullMQ. Jeśli all-in na Vercel + Supabase i nie chcesz zarządzać kolejnym serwisem — Trigger.dev.
Auth: Clerk vs Supabase Auth vs JWT własny
**Supabase Auth** to oczywisty wybór, gdy już korzystasz z Supabase. Obsługuje email/hasło, magic link, OAuth (GitHub, Google, etc.). Integruje się bezpośrednio z RLS przez JWT. Bezpłatny w ramach projektu Supabase. Ograniczenie: customizacja emaili i zaawansowane przepływy wymagają więcej pracy.
**Clerk** ma najlepszy DX spośród wszystkich opcji. Komponenty UI gotowe do osadzenia, dashboard do zarządzania użytkownikami, zaawansowana obsługa organizacji (tenants!) bez pisania kodu. Kosztuje — od 25 USD/miesiąc po przekroczeniu darmowego progu. Dla B2B SaaS z modelem organizacji — warte każdego dolara. Dla B2C z prostym auth — przepłacasz.
**JWT własny** to opcja tylko dla przypadków, gdy masz specyficzne wymagania (własny provider tożsamości, specjalne claimy, integracja z legacy systemem). Nie buduj własnego auth bez powodu — to pułapka na czas i bezpieczeństwo.
Rekomendacja: zacznij od Supabase Auth. Jeśli model organizacji (jedna firma = wielu użytkowników z rolami) jest rdzeniem produktu — kup Clerk od dnia 1.
Storage: Supabase Storage vs S3 + CloudFront
**Supabase Storage** to wygodny punkt startowy. Integruje się z RLS (możesz ograniczyć dostęp do plików per-tenant), API jest proste, masz to w jednym projekcie. Limit na free tierze to 1GB. Przy małej skali — idealne.
**S3 + CloudFront** to opcja, gdy potrzebujesz: dużych wolumenów (setki GB), niskich kosztów per-GB, globalnego CDN dla plików statycznych, szczegółowej kontroli cache i signed URLs z wygasaniem. CloudFront przed S3 redukuje latency dla użytkowników z różnych regionów i kosztuje ułamek transferu bezpośrednio z S3.
Migracja z Supabase Storage do S3 jest możliwa, ale wymaga czasu. Jeśli pliki są rdzeniem produktu (storage app, document management, media platform) — zacznij od S3. Jeśli pliki to poboczna funkcja (avatary, załączniki) — Supabase Storage wystarczy długo.
Monitoring od dnia 1
Monitoring dodany po fakcie to monitoring, którego nie masz w krytycznym momencie. Dwie integracje, które warto ustawić przed deploymentem:
**Sentry** łapie wyjątki z frontendu i backendu. Konfiguracja w Next.js zajmuje 15 minut. Bez tego jesteś ślepy na błędy, które użytkownicy napotykają, ale nie raportują.
```typescript // sentry.client.config.ts import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 0.1, // 10% requestów z tracing environment: process.env.NODE_ENV, }); ```
**Vercel Analytics** daje page views, Web Vitals i performance metrics bez konfiguracji — jeśli deployujesz na Vercel. Jeden import w `layout.tsx`:
```typescript import { Analytics } from '@vercel/analytics/react';
// W layout.tsx
Dodaj do tego Vercel Speed Insights i masz pełny obraz wydajności bez dodatkowego toolingu.
Deployment: Vercel + Supabase — ścieżka zero-ops
Vercel + Supabase to aktualnie najlepsza ścieżka zero-ops dla SaaS-a w TypeScript/Next.js.
Co dostajesz bez zarządzania infrastrukturą: automatyczny CI/CD z GitHub, preview deployments dla każdego PR, edge functions, automatyczne certyfikaty SSL, globalna sieć CDN, zarządzana baza PostgreSQL z backupami, S3-compatible storage, realtime przez WebSocket.
Koszt startowy jest niski: Vercel Pro to 20 USD/miesiąc, Supabase Pro to 25 USD/miesiąc. Łącznie 45 USD/miesiąc za infrastrukturę, która obsłuży pierwsze kilkaset płatnych klientów.
Kiedy outgrowujesz Vercel + Supabase? Gdy potrzebujesz długo działających procesów (Edge Functions mają limit 30s), gdy koszty Vercel stają się niewspółmierne do ruchu, gdy potrzebujesz pełnej kontroli nad bazą (własne rozszerzenia Postgres, custom konfiguracja). Wtedy migrujesz — ale do tego momentu nie tracisz czasu na DevOps.
Co zapamiętać
Architektura, która przeżyje wzrost, nie jest skomplikowana. Wymaga podjęcia kilku świadomych decyzji wcześnie: separacja tenantów od dnia 1, RLS jako standard, kolejka zanim pierwsze timeouty, monitoring przed pierwszym użytkownikiem.
Nie musisz budować tego wszystkiego w dniu pierwszym. Ale musisz wiedzieć, gdzie jesteś i gdzie idziesz. "Dodamy to później" bez planu to "nigdy nie dodamy, bo będziemy gasić pożary na żywym systemie".
Potrzebujesz podobnego rozwiązania?
Porozmawiajmy o Twoim projekcie
Pierwsza rozmowa jest bezpłatna. Opisz nam swój pomysł — odpowiemy w ciągu jednego dnia roboczego.
Umów bezpłatną rozmowę