Trystpilot - System Architecture
Trystpilot — System Architecture
Version: 0.5.0 · Last updated: 2026-02-28 See also:
design/architecture/for Mermaid diagrams
Executive Summary
Trystpilot is a Next.js 15 / App Router SaaS platform deployed on Vercel with a PostgreSQL database. All compute runs serverless (Vercel Functions + Edge Middleware). The architecture is intentionally simple for MVP: a monorepo, no separate microservices, no GraphQL layer. Complexity is deferred until traffic forces it.
1. Domain & Subdomain Strategy
| Domain | Purpose |
|---|---|
trystpilot.xyz | Primary — production |
*.vercel.app | PR preview deployments |
localhost:3000 | Local development |
Future subdomains (not yet provisioned):
| Subdomain | Purpose |
|---|---|
api.trystpilot.xyz | Dedicated API domain (Phase 4) |
admin.trystpilot.xyz | Admin panel (Phase 2) |
blog.trystpilot.xyz | Blog / SEO content (Phase 3) |
2. Frontend Architecture
Stack: Next.js 15 (App Router), React 19, Tailwind CSS v4
Key Decisions
- App Router only — no Pages Router. All routes in
app/. - Server Components by default — client components (
'use client') only where interactivity requires it (search dropdown, submit form, locale switcher). - No client-side routing library — Next.js Link/Router is sufficient.
- i18n: Custom language pack provider; no
next-intldependency. Five packs: en, de, es, fr, pt. Selected byAccept-Languageheader + timezone heuristic in server component. - Analytics: Vercel Analytics (
@vercel/analytics/next) in root layout. Cloudflare beacon injected vianext/scriptafterInteractivestrategy.
Component Hierarchy
app/layout.tsx ← analytics, locale, fonts├── app/page.tsx ← Home (Hero, Search, Feeds)├── app/profile/[alias]/ ← SEO profile (SSG candidate)├── app/submit/ ← Client form├── app/moderation/ ← Admin-gated server component└── app/legal/* ← Static MDX or TSX pagesPerformance Targets
| Metric | Target |
|---|---|
| LCP | < 2.5 s |
| CLS | < 0.1 |
| INP | < 200 ms |
| Profile page TTFB | < 200 ms (ISR cache hit) |
3. Backend Architecture
Stack: Next.js App Router API Routes, Node.js 20, pg (PostgreSQL client)
All server logic lives in app/api/. Service libraries in app/lib/ are shared across
API routes, middleware, and server components.
Service Library Responsibilities
| Library | File | Responsibility |
|---|---|---|
| Text filter | lib/moderation/textFilter.ts | Regex-based PII, threat, explicit content detection |
| Flagging | lib/moderation/flagging.ts | Composite risk score (0–1) per review |
| Reputation scorer | lib/reputation/scorer.ts | Weighted rating average with recency decay |
| Fingerprinter | lib/anonymity/fingerprint.ts | Deterministic hash from request signals |
| DB client | lib/db/client.ts | pg.Pool singleton, max 10 connections |
API Design
All APIs follow REST conventions. No GraphQL. No tRPC.
GETendpoints are public (no auth).POST /api/reviewsrequires valid hCaptcha token (planned).PATCH /api/reviews/[id]/moderaterequiresADMIN_SECRETheader.POST /api/profilesrequiresADMIN_SECRETheader.
Responses use standard HTTP status codes. Errors return { error: string }.
4. Database Architecture
Engine: PostgreSQL (Railway or Supabase)
Client: pg with connection pool (max 10, idle timeout 30 s)
Schema: lib/db/schema.sql
Connection Strategy
Single pg.Pool singleton instantiated once per serverless cold-start. Pool is shared across
all API routes in the same function instance. Max connections set conservatively (10) to avoid
exhausting Railway’s free-tier limits.
Warning: Serverless functions can spawn many parallel instances. If traffic is high, migrate to PgBouncer or use Supabase’s connection pooler before scaling.
Known Schema Issues
| Issue | File | Fix |
|---|---|---|
respect_rating not in scorer | lib/reputation/scorer.ts:L18 | Add 0% now, plan weight redistribution |
| No migration tooling | lib/db/schema.sql | Add Flyway or db-migrate — Phase 1 |
5. Authentication Model
MVP uses a single ADMIN_SECRET environment variable checked in server components and API
routes. There are no user accounts, sessions, or JWTs in the current implementation.
Known critical issue: The moderation dashboard fails open if ADMIN_SECRET is not set.
Fix is M1-T2 in the ROADMAP.
Phase 2 target: NextAuth.js with credentials provider for a proper admin session model, with bcrypt-hashed admin credentials stored in the database.
6. Rate Limiting
Current: In-memory Map in middleware.ts — non-functional on serverless (resets
per cold-start, no cross-instance sharing).
Target: Upstash Redis with @upstash/ratelimit. Sliding window, 5 requests per 60 s
per IP on POST /submit. Environment vars: UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN.
7. Caching Strategy
| Surface | Strategy | Status |
|---|---|---|
| Profile pages | ISR (revalidate: 300 s) | Planned — Phase 2 |
| Static legal / blog | Static generation | ✅ |
| API responses | No cache (dynamic) | Current |
| Rate limit counters | Upstash Redis (TTL-based) | Planned — Phase 1 |
8. Security Hardening
| Control | Status |
|---|---|
| Security headers (CSP, HSTS, X-Frame-Options) | ✅ middleware.ts |
| SQL injection prevention (parameterised queries) | ✅ |
| XSS prevention (React escaping) | ✅ |
| CSRF protection | ⚠️ Not explicit — Add SameSite=Strict cookies + origin check |
| Admin route protection | ⚠️ Fails open — M1-T2 |
| Rate limiting | ❌ M1-T3 |
| CAPTCHA | ❌ M1-T4 |
| Dependency scanning | ❌ Phase 1 CI |
| Secret scanning | ❌ Phase 1 CI |
9. Observability
| Layer | Tool | Status |
|---|---|---|
| Frontend analytics | Vercel Analytics | ✅ |
| CDN analytics | Cloudflare | ✅ |
| Error tracking | None (Sentry planned) | ❌ |
| Server logs | Vercel Function logs | ✅ (basic) |
| Uptime monitoring | None (Checkly planned) | ❌ |
| Database metrics | Railway dashboard | ✅ (basic) |
10. Scalability Path
Current (MVP) Phase 2 Phase 3+───────────────── ──────────────── ─────────────────Vercel Functions + ISR caching + Edge Functionspg.Pool (10 conn) + PgBouncer pooler + Read replicasIn-memory rate limit + Upstash Redis + Multi-regionSingle admin + Admin session auth + RBAC ModeratorsNo search index + Full-text search + Elasticsearch/Typesense