Spis treści
- Browser-VPN SaaS — Finalna Specyfikacja i Plan Implementacji
- 1. Executive Summary
- 2. MVP Scope
- 3. Architecture
- 4. Data Model
- 5. Purchase and Session Flow
- 6. Payments (MVP: Trocador)
- 7. Container Runtime
- 8. VPN Config Pool Lifecycle
- 9. Security
- 10. Monitoring & SLO
- 11. Backup & Disaster Recovery
- 12. API Specification
- 13. Frontend
- 14. Smoke Tests & Acceptance Criteria
- 15. Implementation Plan
- 15.1 Phase 0 — Infrastructure (Days 1–2)
- 15.2 Phase 1 — Backend API (Days 3–6)
- 15.3 Phase 2 — Frontend (Days 7–9)
- 15.4 Phase 3 — Security & Monitoring (Day 10)
- 15.5 Phase 4 — Backup & DR (Day 11)
- 15.6 Phase 5 — CI/CD & Smoke Tests (Day 12)
- 15.7 Phase 6 — v2 Preparation (post-MVP)
- 15.8 Timeline Summary
- 16. Conscious Limitations & Risks
- 17. Appendix: Environment Variables
- 18. Environments
- 19. Open Questions
Browser-VPN SaaS — Finalna Specyfikacja i Plan Implementacji
Data: 2026-07-23
Status: Finalna specyfikacja do weryfikacji
Bazuje na: v1 Product Design (738 linii), v2 (296 linii), v3 (278 linii), v4 (245 linii) + audyt v7 (9.3/10)
Scope: Produkcja MVP Browser-VPN SaaS — hostowana prywatna przeglądarka w kontenerze Docker + WireGuard, dostęp przez krypto-płatności.
Publikacja: https://10s.pl/pb-final-design/
Decyzje otwarte rozstrzygnięte: backup=SSH/rsync, admin=subdomena, staging=tak
1. Executive Summary
1.1 Cel
Zbudować produkcyjny SaaS, w którym użytkownik bez rejestracji wybiera przeglądarkę, region VPN i plan czasowy, płaci kryptowalutą i otrzymuje dostęp do prywatnej przeglądarki działającej w izolowanym kontenerze Docker. Cały ruch wychodzi przez WireGuard. Brak przechowywania IP, User-Agent ani historii przeglądania po stronie aplikacji.
1.2 Target
Użytkownicy ceniący totalną prywatność — bez kont, bez emaili, bez Stripe.
1.3 Status dokumentu
| Wersja | Ocena | Uwagi |
|---|---|---|
| v1 | 6.3/10 | Pierwsza pełna wizja, Caddy, Trocador only |
| v2 | 6.0/10 | Traefik, Trocador + BTCPay, NET_ADMIN+SYS_PTRACE |
| v3 | 7.7/10 | PostgreSQL, SLO, admin panel, rate limiting |
| v4 | 9.2/10 (samoocena) / 9.3/10 (audyt v7) | Delta poprawiająca v3: izolacja sieciowa, retencja danych, ERD, DR plan |
| Final | Do weryfikacji | Scalony v1+v2+v3+v4 + OpenAPI + smoke tests + SLO targets |
1.4 Kluczowe decyzje MVP
| Obszar | Decyzja |
|---|---|
| Reverse proxy | Traefik v3 z Docker provider |
| Streaming | Selkies WebSocket mode: nginx na porcie 3000 serwuje frontend, /websocket proxy do Selkies (port 8082 wewnątrz kontenera). WebRTC/port 3001 → v2 |
| Płatności MVP | Trocador AnonPay (XMR) |
| Płatności v2 | BTCPay Server self-hosted jako fallback |
| Baza danych | PostgreSQL 16 + migracje SQL |
| Frontend | Nuxt 3 + Vue 3 + TypeScript |
| Backend | Node.js + Fastify |
| Kontenery | linuxserver/{chrome,brave,firefox,mullvad-browser}:latest |
| VPN | WireGuard, pool konfiguracji per region |
| Auth użytkownika | Magic link + httpOnly cookie + device binding |
| Auth admin | Hasło bcrypt + obowiązkowy TOTP |
| Secrets | .env (600) + bind mount; Docker Swarm secrets → v2 |
| Hosting | Single server, max 30 aktywnych kontenerów |
2. MVP Scope
2.1 In Scope (Must Have)
- Landing page z formularzem zakupu (browser, region, plan, discount code).
- Tworzenie zamówienia i generowanie
ord-XXXXXX-XXXXXX. - Integracja Trocador AnonPay (create payment, webhook, polling fallback).
- Kody rabatowe, w tym 100% discount do testowania.
- PostgreSQL: orders, sessions, vpn_configs, discount_codes, tickets, system_logs, server_metrics, admin_audit_logs.
- Start/stop kontenerów Docker z wybraną przeglądarką i WireGuard.
- Pool konfiguracji WireGuard per region.
- Magic link + device binding przez httpOnly cookie.
- Recovery przez order number + 6-cyfrowy PIN (bcrypt, max 3 próby, blokada 24h).
- Sesja odliczana od pierwszego dostępu, 1h buffer, idle timeout 30 min.
- Strona podsumowania po wygaśnięciu przez 30 dni.
- Limit 30 instancji, kolejka gdy CPU/RAM > 80% lub brak slotów.
- Panel admina: dashboard, zamówienia, kontenery, VPN configs, tickety, ustawienia, raporty CSV.
- Podstawowe monitoring i alerty Telegram.
- Codzienne backupy lokalne, off-site co 6h.
- CSP/HSTS, rate limiting, TOTP admina, audit log.
2.2 Out of Scope for MVP
- BTCPay Server (przesunięte do v2).
- WebRTC / Selkies port 3001 (noVNC WebSocket wystarcza dla MVP).
- Multi-server / auto-scaling.
- Prometheus + Grafana (curl-based SLO w MVP).
- Docker Swarm secrets.
- Custom seccomp profile (Docker default + cap-drop v MVP).
- Stripe / email / konta użytkowników.
- Refundy, rozszerzenia planów, add-ons.
- Wielojęzyczny UI.
- Cloudflare Tunnel — direct domain only.
3. Architecture
3.1 High-Level Diagram
User → HTTPS :443 → Traefik v3
├── admin.<DOMAIN>/* → frontend:3000 (Nuxt 3 admin layout)
├── /api/* → backend:3000 (Node.js Fastify)
├── / → frontend:3000 (Nuxt 3 public layout)
├── /ses-abc123 → browser container :3000 (Selkies frontend + /websocket)
└── /.well-known/acme-challenge → Let's Encrypt
Backend ↔ PostgreSQL
Backend ↔ Docker socket (uruchamianie/kontrola kontenerów)
Backend ↔ Trocador API
Each browser container → per-container internal Docker network → WireGuard tunnel → Internet
3.2 Components
| Component | Technology | Responsibility |
|---|---|---|
| Frontend | Nuxt 3 + Vue 3 + TypeScript | Landing, order page, session page, admin panel |
| Backend API | Node.js + Fastify | Orders, payments, container orchestration, VPN pool, sessions |
| Scheduler | node-cron inside backend | Heartbeats, expiry, queue, metrics, cleanup, backups |
| Database | PostgreSQL 16 | Persistence |
| Reverse Proxy | Traefik v3 | HTTPS, routing, rate limiting, middlewares |
| Browser Containers | linuxserver images | Chrome/Brave/Firefox/Mullvad + noVNC |
| VPN | WireGuard | Routing container traffic |
| Monitoring | curl + Telegram | SLO checks, alerts |
3.3 Network Architecture
- Traefik działa w sieci
traefik-network. - PostgreSQL i backend w
backend-network. - Każdy kontener przeglądarki dostaje własną sieć Docker
browser-<sessionId>z flagą--internal. - Kontener ma
--cap-add=NET_ADMINi uruchamia WireGuard; cały ruch wychodzi przez tunel. - Kontenery nie widzą siebie nawzajem ani sieci backend.
- Traefik jest jedynym komponentem z dostępem do sieci browser kontenerów.
- BTCPay (v2) będzie w osobnej sieci
btcpay-network, niepołączonej z sieciami browser.
3.4 Container Runtime Spec
image: linuxserver/<browser>:latest
cap_drop: [ALL]
cap_add: [NET_ADMIN, SYS_PTRACE]
security_opt:
- seccomp=default
- no-new-privileges:true
memory: 2g
cpus: 1.0
pids_limit: 200
restart: "no" # managed by backend
networks:
- browser-<sessionId> # per-container internal network created by backend
labels:
traefik.enable: "true"
traefik.http.routers.<sessionId>.rule: "PathPrefix(`/<sessionId>`)"
traefik.http.services.<sessionId>.loadbalancer.server.port: "3000"
Uwaga: Kontener linuxserver ma wewnętrzny nginx na porcie 3000, który serwuje frontend Selkies i proxy'uje /websocket do Selkies data server na porcie 8082. Traefik proxy'uje cały ruch do portu 3000 kontenera — WebSocket upgrade działa automatycznie na podstawie nagłówka Upgrade od klienta. Port 3001 (SSL Selkies) i port 8082 nie muszą być expose'owane na zewnątrz.
4. Data Model
4.1 ERD
orders ||--o{ sessions : "1:1 po aktywacji"
orders ||--o| vpn_configs : "przypisany config"
orders ||--o{ tickets : "1:N"
discount_codes ||--o{ orders : "użyty kod"
orders ||--o{ system_logs : "logi"
server_metrics }|--|| orders : "opcjonalnie"
admin_audit_logs }|--|| orders : "opcjonalnie"
4.2 Tables
orders
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
order_number |
VARCHAR(32) | unique, human-readable ord-XXXXXX-XXXXXX |
browser |
VARCHAR(32) | chrome, brave, firefox, mullvad |
region |
VARCHAR(16) | country/region code |
plan |
VARCHAR(8) | 1d, 1w |
price_usd |
DECIMAL(10,2) | final price |
discount_code |
VARCHAR(64) | nullable, FK do discount_codes |
status |
VARCHAR(16) | pending, paid, queued, active, expired, cancelled, underpaid |
trocador_transaction_id |
VARCHAR(128) | nullable |
trocador_status |
VARCHAR(32) | nullable |
payment_timeout_at |
TIMESTAMP | created_at + 30 min |
paid_at |
TIMESTAMP | nullable |
activated_at |
TIMESTAMP | nullable |
expires_at |
TIMESTAMP | nullable |
archived_at |
TIMESTAMP | nullable |
device_token |
VARCHAR(128) | nullable, set on first access |
recovery_pin_hash |
VARCHAR(128) | nullable, bcrypt |
session_id |
VARCHAR(32) | nullable |
container_id |
VARCHAR(128) | nullable |
vpn_config_id |
UUID | nullable, FK → vpn_configs |
created_at |
TIMESTAMP | default now() |
updated_at |
TIMESTAMP | default now() |
sessions
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
order_id |
UUID | FK → orders |
session_url |
VARCHAR(64) | unique path, e.g. ses-abc123 |
device_token |
VARCHAR(128) | nullable until first visit |
started_at |
TIMESTAMP | nullable |
last_seen_at |
TIMESTAMP | nullable |
expires_at |
TIMESTAMP | nullable |
container_id |
VARCHAR(128) | |
local_port |
INT | port inside traefik (unused for docker provider) |
created_at |
TIMESTAMP | default now() |
vpn_configs
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
region |
VARCHAR(16) | e.g. de, pl, us-east |
config_file_path |
VARCHAR(256) | path to .conf file |
in_use |
BOOLEAN | default false |
last_used_at |
TIMESTAMP | nullable |
fail_count |
INT | default 0 |
is_active |
BOOLEAN | default true |
created_at |
TIMESTAMP | default now() |
discount_codes
| Column | Type | Notes |
|---|---|---|
code |
VARCHAR(64) | PK |
discount_percent |
INT | 0-100 |
valid_from |
TIMESTAMP | |
valid_until |
TIMESTAMP | |
max_uses |
INT | |
used_count |
INT | default 0 |
is_active |
BOOLEAN | default true |
tickets
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
order_id |
UUID | FK → orders |
ticket_number |
VARCHAR(32) | unique tck-XXXXXX |
category |
VARCHAR(32) | payment, browser, vpn, other |
message |
TEXT | |
status |
VARCHAR(16) | open, resolved |
created_at |
TIMESTAMP | default now() |
resolved_at |
TIMESTAMP | nullable |
system_logs
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
level |
VARCHAR(16) | debug, info, warn, error |
component |
VARCHAR(64) | |
message |
TEXT | |
metadata |
JSONB | nullable |
created_at |
TIMESTAMP | default now() |
server_metrics
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
cpu_percent |
FLOAT | |
ram_percent |
FLOAT | |
disk_percent |
FLOAT | |
active_containers |
INT | |
queued_orders |
INT | |
avg_startup_time_ms |
INT | nullable |
failed_starts |
INT | default 0 |
created_at |
TIMESTAMP | default now() |
admin_audit_logs
| Column | Type | Notes |
|---|---|---|
id |
UUID | PK |
admin_id |
VARCHAR(64) | "owner" for single admin |
action |
VARCHAR(64) | login, order_cancel, container_stop, etc. |
target |
VARCHAR(128) | affected entity |
ip_address |
VARCHAR(64) | nullable |
metadata |
JSONB | nullable |
created_at |
TIMESTAMP | default now() |
4.3 Initial Migration
Plik: migrations/001_initial.sql — tworzy wszystkie tabele, indeksy, FK i initial admin audit setup.
5. Purchase and Session Flow
5.1 Purchase Flow
- User wybiera browser, region, plan, opcjonalny discount code.
POST /api/v1/orderstworzy orderpending.- Backend wywołuje Trocador AnonPay (
direct=false): ticker_to=xmrnetwork_to=Mainnetaddress=<TROCADOR_XMR_ADDRESS>fiat_equiv=USDlub amount USD przeliczone przez Trocadorwebhook=https://<domain>/api/v1/payments/trocador/webhookwebhook_key=<TROCADOR_WEBHOOK_KEY>- Backend wyświetla order number, QR code, payment URL i ostrzega przed zamknięciem karty.
- User płaci dowolną kryptowalutą; Trocador zamienia na XMR.
- Trocador wysyła webhook na każdą zmianę statusu.
- Backend weryfikuje
Webhook-Keyi aktualizuje order dopaidlubunderpaid. - Fallback: polling co 30s gdy webhook nie przyjdzie > 15 min.
- Scheduler anuluje zamówienia
pendingstarsze niż 30 min.
5.2 Activation Flow
- User wraca na
/order/:orderNumber. - Jeśli
paid, backend sprawdza zasoby: - CPU < 80%
- RAM < 80%
- active containers < MAX_INSTANCES
- Jeśli zasoby dostępne:
- wybiera nieużywany VPN config dla regionu,
- tworzy per-container network
--internal, - uruchamia kontener z Traefik labels,
- tworzy sesję i magic link
https://domain.com/ses-abc123, - recovery PIN generowany i wyświetlany raz,
- zwraca magic link userowi.
- Jeśli brak zasobów:
- status
queued, - user widzi pozycję w kolejce i szacowany czas.
5.3 Session Usage Flow
- User klika magic link.
- Backend weryfikuje: session exists, not expired, device matches or no device bound yet.
- Przy pierwszej wizycie:
- generuje
deviceToken, - zapisuje hash w sesji i w orderze,
- ustawia httpOnly cookie
Secure; SameSite=Strict; Path=/ses-abc123. - User widzi stream Selkies przez Traefik (port 3000 kontenera, ścieżka
/websocketdla danych). - Sesja liczy czas od pierwszego dostępu.
- Heartbeat co 30s od frontendu do backendu; idle timeout 30 min → stop kontenera.
- First access timeout 1h: jeśli user nie otworzy magic linka → stop + zwolnij config.
5.4 Expiration Flow
- Scheduler stopuje i usuwa kontener po upływie czasu.
- User widzi "Session expired" w przeglądarce.
- Przez 30 dni magic link pokazuje summary page.
- Po 30 dniach link pokazuje "Session expired — buy a new session".
6. Payments (MVP: Trocador)
6.0 Decisions Resolved
| Question | Decision |
|---|---|
| Admin panel location | Separate subdomain: admin.<DOMAIN> |
| Off-site backup method | SSH/rsync to backup server |
| Staging environment | Yes, separate staging server/domain |
6.1 Trocador Configuration
TROCADOR_XMR_ADDRESS=<owner XMR address>
TROCADOR_WEBHOOK_KEY=<random 32+ chars>
TROCADOR_FIAT_CURRENCY=USD
Uwaga: AnonPay działa jako publiczny URL bez osobnego API key. TROCADOR_WEBHOOK_KEY służy do weryfikacji webhooków.
6.2 Trocador Flow
POST /api/v1/orders
→ create order in DB (status=pending)
→ call Trocador AnonPay
← store transaction_id
← return payment_url + order_number to user
Trocador webhook POST /api/v1/payments/trocador/webhook
→ verify Webhook-Key header
→ update order status (paid / underpaid / expired / invalid)
→ if paid: enqueue activation
Polling fallback (every 30s):
→ for pending orders older than 15 min with no webhook
→ call Trocador API to check status
6.3 Status Mapping
| Trocador status | Order status | Action |
|---|---|---|
| completed | paid | activate |
| underpaid | underpaid | alert admin, show user; manual resolve |
| expired | cancelled | release resources if any |
| invalid | cancelled | log |
6.4 BTCPay Server (v2)
BTCPay przesunięte do fazy v2. W MVP Trocador jest jedynym providerem z polling fallback. W v2 dodać:
- kontener btcpayserver/btcpayserver:latest z BTCPAY_PRUNE=10000,
- osobną sieć btcpay-network,
- webhook POST /api/v1/payments/btcpay/webhook,
- automatyczny failover po 2 min niedostępności Trocador.
7. Container Runtime
7.1 Start Container Flow
containerManager.start(order):
checkResources()
vpnConfig = vpnPool.assign(order.region)
networkName = `browser-${sessionId}`
docker network create --driver bridge --internal ${networkName}
container = docker run(
image: `linuxserver/${order.browser}:latest`,
network: networkName,
capDrop: ['ALL'],
capAdd: ['NET_ADMIN', 'SYS_PTRACE'],
securityOpt: ['seccomp=default', 'no-new-privileges:true'],
memory: '2g',
cpus: '1.0',
pidsLimit: 200,
env: { VPN_CONFIG_PATH: vpnConfig.path, ... },
labels: {
'traefik.enable': 'true',
'traefik.http.routers.<sessionId>.rule': `PathPrefix(\`/${sessionId}\`)`,
'traefik.http.services.<sessionId>.loadbalancer.server.port': '3000',
'traefik.http.routers.<sessionId>.tls.certresolver': 'letsencrypt',
}
)
waitForHealthy(container, timeout=60s)
return sessionUrl
7.2 Stop Container Flow
containerManager.stop(sessionId):
container = findBySessionId(sessionId)
docker stop container
docker rm container
docker network rm browser-${sessionId}
vpnPool.release(order.vpn_config_id)
session.ended_at = now()
7.3 Security Flags
--cap-drop=ALL
--cap-add=NET_ADMIN
--cap-add=SYS_PTRACE
--security-opt=seccomp=default
--security-opt=no-new-privileges:true
--memory=2g
--cpus=1.0
--pids-limit=200
--network=<per-container-internal-network>
7.4 WireGuard Inside Container
Obraz linuxserver/chrome (i pokrewne) nie zawiera preinstalowanego wireguard-tools. Konieczna jest instalacja przy pierwszym starcie kontenera.
Entrypoint / init:
#!/bin/bash
set -e
# Install wireguard-tools if missing
if ! command -v wg-quick &> /dev/null; then
apt-get update -qq
apt-get install -y --no-install-recommends wireguard-tools
fi
# Load WireGuard config and bring up tunnel
wg-quick up /config/wg0.conf
# Route all traffic through WireGuard (if AllowedIPs is not 0.0.0.0/0)
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
# Verify connectivity through VPN
ping -c 1 -W 5 1.1.1.1
# Start browser / desktop environment (delegated to original LSIO init)
exec /init "$@"
Uwagi z testów:
- wg-quick up działa z samym --cap-add=NET_ADMIN — nie wymaga SYS_ADMIN ani --privileged.
- Pole DNS = w WireGuard config powoduje błąd resolvconf: command not found. Nie ustawiaj DNS = w configu MVP; zarządzaj DNS osobno lub zainstaluj openresolv.
- iptables jest już dostępny w obrazie linuxserver.
8. VPN Config Pool Lifecycle
1. Upload → admin upload .conf, zapis path, region, status active
2. Verify → backend ping endpoint VPN przez config
3. Assign → scheduler wybiera nieużywany config dla regionu, in_use=true
4. Use → kontener startuje z configiem
5. Release → po stopie kontenera in_use=false
6. Rotate → co 90 dni lub na żądanie admina
7. Fail → fail_count >= 3 → is_active=false, alert Telegram
8.1 Admin Endpoints
GET /api/v1/admin/vpn-configs → lista + stats
POST /api/v1/admin/vpn-configs → upload pliku .conf
POST /api/v1/admin/vpn-configs/:id/verify → verify config
POST /api/v1/admin/vpn-configs/:id/disable → disable broken
DELETE /api/v1/admin/vpn-configs/:id → soft delete
9. Security
9.1 Device Binding
- Magic link zawiera
sessionId. - Pierwsza wizyta ustawia
deviceTokenw httpOnly cookie. - Cookie:
Secure; HttpOnly; SameSite=Strict; Path=/<sessionId>. - Drugie urządzenie bez tokena → blokada.
- Fingerprint serwerowy (User-Agent + IP hash) jako druga warstwa.
- Recovery: order number + 6-cyfrowy PIN, bcrypt w DB, max 3 próby, blokada 24h.
9.2 Admin Authentication
- Panel admina dostępny tylko pod subdomeną
admin.<DOMAIN>. - Login: hasło bcrypt (min 14 znaków) + obowiązkowy TOTP.
- Allowlista IP/VPN zalecana dla
admin.<DOMAIN>(np. tylko z VPN operatora). - Wszystkie akcje logowane do
admin_audit_logs. - Certyfikat Let's Encrypt dla
admin.<DOMAIN>przez Traefik.
9.3 Rate Limiting (Traefik middleware)
http:
middlewares:
rate-limit-general:
rateLimit:
average: 10
burst: 20
period: 1m
rate-limit-orders:
rateLimit:
average: 5
burst: 5
period: 1h
rate-limit-tickets:
rateLimit:
average: 3
burst: 3
period: 1h
rate-limit-admin:
rateLimit:
average: 30
burst: 50
period: 1m
9.4 HTTP Security Headers
http:
middlewares:
security-headers:
headers:
contentSecurityPolicy: "default-src 'self'; connect-src 'self' ws: wss:; img-src 'self' blob: data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
strictTransportSecurity: "max-age=31536000; includeSubDomains; preload"
customFrameOptionsValue: "DENY"
referrerPolicy: "no-referrer"
contentTypeNosniff: true
browserXssFilter: true
9.5 Secrets Management
| Secret | Storage | Notes |
|---|---|---|
DATABASE_URL |
.env |
postgres://bvpn:***@postgres/browser_vpn |
COOKIE_SECRET |
.env |
generated by setup.sh, openssl rand -base64 32 |
TROCADOR_XMR_ADDRESS |
.env |
owner's XMR address |
TROCADOR_WEBHOOK_KEY |
.env |
random 32+ chars |
ADMIN_PASSWORD_HASH |
.env |
bcrypt hash |
TOTP_SECRET |
.env |
base32 secret for owner TOTP |
TELEGRAM_BOT_TOKEN |
.env |
optional |
TELEGRAM_CHAT_ID |
.env |
optional |
.envpermissions600.- Plik
.env.examplew repo, bez realnych wartości. setup.shgeneruje sekrety i pyta o domenę / XMR address.
9.6 Data Retention Policy
| Data type | Retention | Notes |
|---|---|---|
| Order personal data | 90 days after archived_at | device_token, recovery_pin_hash cleared |
| System logs | 7 days | |
| Tickets | anonimized after 90 days | message replaced with hash, status kept |
| Server metrics | aggregated after 24h, kept 30 days | |
| IP address | never stored | |
| User-Agent | never stored | |
| Browsing history | never leaves container | deleted with container |
| Session metadata | 30 days summary | after expiry |
Scheduler data-retention.js uruchamiany co 1h.
10. Monitoring & SLO
10.1 SLO Targets
| Metric | Target | Measured by |
|---|---|---|
| Uptime API + frontend | 99.5% monthly | Health check GET /api/v1/health every 30s |
| Container startup time | < 60s (P95) | server_metrics.avg_startup_time_ms |
| API response time | < 500ms (P95) | curl -w %{time_total} every 30s |
| Streaming latency | < 2s heartbeat roundtrip | Container /healthz every 30s |
10.2 Metrics Collection
// co 30s
sloMonitor.collect():
http_code = curl http://localhost:3000/api/v1/health
response_time = curl -w %{time_total}
active_containers = docker ps --filter label=traefik.enable=true | count
queued_orders = SELECT COUNT(*) FROM orders WHERE status='queued'
cpu, ram, disk = read /proc/stat, /proc/meminfo, df
insert into server_metrics
10.3 Telegram Alerts
| Condition | Severity | Action |
|---|---|---|
| CPU > 80% | WARN | alert |
| RAM > 80% | WARN | alert |
| Disk < 10% | CRIT | alert + auto-cleanup old logs |
| Container start > 60s | WARN | alert |
| Container start failed > 3x | CRIT | alert + block region |
| Trocador API down > 2 min | CRIT | alert (BTCPay failover in v2) |
| VPN endpoint down > 5 min | WARN | alert + disable region |
| Backup failed | CRIT | alert |
| Cert expires < 7 days | WARN | alert |
| PostgreSQL unavailable | CRIT | alert |
| fail_count >= 3 on VPN config | WARN | alert + disable config |
11. Backup & Disaster Recovery
11.1 Backup Strategy
| Type | Frequency | Retention | Method |
|---|---|---|---|
| PostgreSQL dump | daily | 7 days local | pg_dump to /backups/db-YYYYMMDD.sql |
| WireGuard configs | daily | 7 days local | copy wg-configs/ to /backups/wg-configs-YYYYMMDD/ |
| Off-site full | every 6h | 30 days | gpg --encrypt + rsync over SSH to backup server |
11.2 DR Scenarios
| Scenario | RTO | RPO | Procedure |
|---|---|---|---|
| Container crash | < 5 min | 0 | auto-restart via backend; >3 crashes → stop + alert |
| Container OS restart | < 30 min | < 6h | docker compose up -d, restore latest DB if needed |
| Full server loss | < 48h | < 6h | provision new server, run setup.sh, restore DB + wg-configs, update DNS |
| PostgreSQL corruption | < 2h | < 6h | restore from latest off-site backup |
| VPN config pool exhaustion | < 10 min | 0 | alert admin, block region until configs added |
| Trocador outage | < 5 min | 0 | alert, polling fallback (BTCPay failover in v2) |
| Certificate expiry | < 1h | 0 | auto-renew via Traefik; alert if < 7 days |
11.3 Backup Verification
- Test restore co miesiąc na staging.
backup.shirestore.shw repo.
12. API Specification
12.1 Versioning
- Base path:
/api/v1/ X-API-Version: 1.0in response headers.- Minor bump = new endpoints/fields, still
/api/v1/. - Major bump = new prefix
/api/v2/, old supported 3 months.
12.2 Public Endpoints
POST /api/v1/orders
Create order.
Request:
{
"browser": "chrome|brave|firefox|mullvad",
"region": "de|pl|us|...",
"plan": "1d|1w",
"discount_code": "optional-string"
}
Response 201:
{
"order_number": "ord-abc123-def456",
"status": "pending",
"payment_url": "https://trocador.app/...",
"payment_timeout_at": "2026-07-23T12:00:00Z",
"price_usd": "5.00",
"discount_percent": 0
}
Errors: - 400 invalid_region, invalid_browser, invalid_plan, invalid_discount_code - 429 rate_limit
GET /api/v1/orders/:orderNumber
Get order status.
Response 200:
{
"order_number": "ord-abc123-def456",
"status": "paid",
"browser": "chrome",
"region": "de",
"plan": "1d",
"price_usd": "5.00",
"paid_at": "2026-07-22T12:05:00Z",
"expires_at": null,
"session_url": null,
"queue_position": null
}
POST /api/v1/orders/:orderNumber/activate
Launch browser session. Returns magic link. Sets recovery PIN on first call.
Response 200:
{
"session_url": "https://domain.com/ses-abc123",
"recovery_pin": "123456"
}
Response 202 (queued):
{
"status": "queued",
"queue_position": 3,
"estimated_wait_seconds": 180
}
GET /api/v1/sessions/:sessionId
Validate and return session details for frontend. Requires device cookie.
Response 200:
{
"session_id": "ses-abc123",
"status": "active",
"expires_at": "2026-07-23T12:05:00Z",
"remaining_seconds": 86340
}
Response 403: device not bound / second device.
POST /api/v1/sessions/:sessionId/recover
Recover access with order number + PIN.
Request:
{
"order_number": "ord-abc123-def456",
"pin": "123456"
}
Response 200: sets device cookie, returns session details.
Response 403: invalid PIN or max attempts exceeded.
POST /api/v1/payments/trocador/webhook
Trocador webhook.
Headers: Webhook-Key: <TROCADOR_WEBHOOK_KEY>
Response 200 / 400 / 401.
POST /api/v1/support/tickets
Create support ticket.
Request:
{
"order_number": "ord-abc123-def456",
"category": "payment|browser|vpn|other",
"message": "...",
"attachment": "<base64 optional, max 5MB>"
}
Response 201:
{
"ticket_number": "tck-abc123",
"status": "open"
}
GET /api/v1/health
System health.
Response 200:
{
"status": "ok",
"database": "ok",
"docker": "ok",
"trocador": "ok"
}
12.3 Admin Endpoints
All require Authorization: Bearer <jwt> + TOTP verified.
GET /api/v1/admin/dashboard → metrics, active containers, revenue
GET /api/v1/admin/orders → list with filters
GET /api/v1/admin/orders/:id → detail
POST /api/v1/admin/orders/:id/cancel → cancel order
GET /api/v1/admin/containers → list active
POST /api/v1/admin/containers/:id/stop → stop container
GET /api/v1/admin/vpn-configs → list + stats
POST /api/v1/admin/vpn-configs → upload .conf
POST /api/v1/admin/vpn-configs/:id/verify → verify
POST /api/v1/admin/vpn-configs/:id/disable → disable
GET /api/v1/admin/tickets → list tickets
POST /api/v1/admin/tickets/:id/reply → reply
GET /api/v1/admin/settings → prices, limits, regions
PUT /api/v1/admin/settings → update settings
GET /api/v1/admin/reports/sales → CSV export
GET /api/v1/admin/logs → audit log
POST /api/v1/admin/auth/login → password + TOTP → JWT
POST /api/v1/admin/auth/refresh → refresh JWT
12.4 Error Response Format
{
"error": "invalid_region",
"message": "Region 'xy' is not supported",
"retry_after": 3600
}
13. Frontend
13.1 Pages
| Route | Purpose |
|---|---|
/ |
Landing page + order form |
/order/[orderNumber] |
Order status, payment, launch browser |
/session/[sessionId] |
Browser session stream (Selkies iframe) |
/expired/[sessionId] |
Post-expiration summary |
/support |
Support ticket form |
/admin/** |
Admin panel (protected) |
13.2 Order Page States
| Status | UI |
|---|---|
| pending | payment instructions / Trocador iframe |
| paid | "Launch Browser" button |
| queued | queue position + ETA |
| active | magic link + timer + "End session" |
| expired | summary + "Buy new session" |
| cancelled | cancellation info |
13.3 Session Page
- Selkies iframe ładuje
https://domain.com/<sessionId>/(frontend Selkies) i łączy się przez WebSocket dohttps://domain.com/<sessionId>/websocket. - Timer pokazujący remaining time.
- Przyciski: "Copy magic link", "End session".
- Heartbeat co 30s do backendu.
14. Smoke Tests & Acceptance Criteria
14.1 Smoke Test Framework
- Framework: Playwright + pytest (lub Node test runner).
- Environment: staging with real Trocador sandbox / 100% discount code.
- CI: GitHub Actions (v2).
14.2 Critical Flow Tests
| # | Flow | Steps | Pass Criteria |
|---|---|---|---|
| 1 | Purchase | Select browser/region/plan → POST /api/v1/orders | 201, order_number returned, status=pending |
| 2 | Discount 100% | Use TEST100 code → order | status=paid immediately, no payment_url |
| 3 | Activation | Paid order → activate | container starts < 60s, magic link shown, session active |
| 4 | Session access | Open magic link | httpOnly cookie set, Selkies loads, stream visible |
| 5 | Device binding | Try second browser/device | 403 forbidden |
| 6 | Recovery | Use order number + PIN on second device | access granted, new cookie set |
| 7 | Expiry | Wait / fast-forward expiry | container stopped, expired page shown |
| 8 | Queue | Fill max_instances → new order | status=queued, position shown, activates when slot free |
| 9 | Trocador webhook | Simulate completed webhook | order status=paid |
| 10 | Backup | Run backup.sh | files exist in /backups, off-site rsync succeeds |
14.3 Acceptance Criteria (SLO)
| Flow | Criterion 1 | Criterion 2 | Criterion 3 |
|---|---|---|---|
| Zakup | User wybiera browser/region/plan → POST zwraca 201 z order_number | User widzi QR code / payment link | Payment timeout 30 min działa |
| Płatność | Webhook completed → order paid | Polling fallback po 15 min | Underpaid → status underpaid |
| Aktywacja | Order paid → container start < 60s | Magic link wyświetlony | Queue działa gdy brak zasobów |
| Sesja | Magic link → cookie + device bound | Drugie urządzenie → blokada | Recovery PIN działa (max 3 próby) |
| Wygaśnięcie | Container stop po czasie | Summary page 30 dni | Idle timeout 30 min → stop |
| Backup | Codzienny lokalny backup | Off-site co 6h | Retention 7 dni lokalny, 30 dni off-site |
15. Implementation Plan
15.1 Phase 0 — Infrastructure (Days 1–2)
| Task | Files | Verify |
|---|---|---|
| 0.1 Docker Compose + Traefik | docker-compose.yml, traefik/traefik.yml, traefik/dynamic.yml |
docker compose config valid |
| 0.2 PostgreSQL migrations | migrations/001_initial.sql |
\dt shows 8 tables |
| 0.3 Environment setup | .env.example, setup.sh |
generates .env with 600 perms |
| 0.4 Data retention scheduler | backend/src/services/data-retention.js |
logs cleanup works |
15.2 Phase 1 — Backend API (Days 3–6)
| Task | Files | Verify |
|---|---|---|
| 1.1 Backend skeleton + health | backend/src/index.js, backend/src/plugins/* |
GET /api/v1/health 200 |
| 1.2 Orders API | backend/src/routes/orders.js |
POST creates order, GET returns status |
| 1.3 Discount codes | backend/src/services/discount-service.js |
TEST100 works |
| 1.4 Trocador integration | backend/src/services/trocador.js |
webhook + polling |
| 1.5 VPN config pool | backend/src/services/vpn-pool.js |
lifecycle works |
| 1.6 Container manager | backend/src/services/container-manager.js |
start/stop with isolation |
| 1.7 Container health | backend/src/services/container-health.js |
heartbeat + idle timeout |
| 1.8 Session / magic link | backend/src/routes/sessions.js |
device binding + recovery |
| 1.9 Tickets API | backend/src/routes/tickets.js |
create/list/reply |
| 1.10 SLO monitor | backend/src/services/slo-monitor.js |
metrics recorded |
| 1.11 Admin API + TOTP | backend/src/routes/admin.js, backend/src/services/auth.js |
login + TOTP + audit |
15.3 Phase 2 — Frontend (Days 7–9)
| Task | Files | Verify |
|---|---|---|
| 2.1 Landing + order form | frontend/pages/index.vue, frontend/components/OrderForm.vue |
form submits |
| 2.2 Order status page | frontend/pages/order/[orderNumber].vue |
all states render |
| 2.3 Session page | frontend/pages/session/[sessionId].vue |
noVNC loads |
| 2.4 Admin dashboard | frontend/pages/admin/index.vue |
protected, metrics |
| 2.5 Admin orders/containers | frontend/pages/admin/orders.vue, frontend/pages/admin/containers.vue |
CRUD works |
| 2.6 Admin tickets/settings | frontend/pages/admin/tickets.vue, frontend/pages/admin/settings.vue |
reply + update |
| 2.7 Support page | frontend/pages/support/index.vue |
ticket form |
15.4 Phase 3 — Security & Monitoring (Day 10)
| Task | Files | Verify |
|---|---|---|
| 3.1 Traefik middlewares | traefik/dynamic.yml |
rate limit, CSP, HSTS |
| 3.2 Container scanning script | scripts/trivy-scan.sh |
runs weekly |
| 3.3 Telegram alerts | backend/src/services/telegram.js |
alerts sent |
15.5 Phase 4 — Backup & DR (Day 11)
| Task | Files | Verify |
|---|---|---|
| 4.1 Backup scripts | scripts/backup.sh, scripts/restore.sh |
backup created, restore works |
| 4.2 DR runbook | docs/DR.md |
documented |
15.6 Phase 5 — CI/CD & Smoke Tests (Day 12)
| Task | Files | Verify |
|---|---|---|
| 5.1 GitHub Actions pipeline | .github/workflows/deploy.yml |
build, test, deploy staging |
| 5.2 Smoke tests | tests/smoke/*.spec.ts |
10 critical flows pass |
15.7 Phase 6 — v2 Preparation (post-MVP)
| Task | Priority |
|---|---|
| BTCPay Server integration | High |
| WebRTC / Selkies port 3001 | High |
| Custom seccomp profile | High |
| Prometheus + Grafana | Medium |
| Docker Swarm secrets | Medium |
| Cosign image signing | Medium |
| Encryption at rest (LUKS) | Medium |
| Multi-server scaling | Low |
15.8 Timeline Summary
| Phase | Duration | Cumulative |
|---|---|---|
| 0 Infrastructure | 1.5 d | 1.5 d |
| 1 Backend API | 4 d | 5.5 d |
| 2 Frontend | 2.5 d | 8 d |
| 3 Security | 1 d | 9 d |
| 4 Backup/DR | 0.5 d | 9.5 d |
| 5 CI/CD + Smoke | 0.5 d | 10 d |
| MVP ready | ~8 d | |
| Pre-production ready | ~10 d |
16. Conscious Limitations & Risks
16.1 Limitations
| Decision | Reason |
|---|---|
| Single server | Faster MVP, easier debugging |
| Crypto-only payments | Privacy audience requirement |
| No user accounts | Less data, less complexity |
| No refunds | Simpler policy |
| English only | Faster MVP |
| Direct domain, no Cloudflare | Simpler infrastructure |
| BTCPay in v2 | Reduce MVP complexity |
| Selkies WebSocket mode zamiast WebRTC | Port 3000 to nginx z frontendem + /websocket proxy do Selkies (port 8082 wewnątrz). WebRTC/port 3001 wymaga UDP i STUN/TURN → v2 |
16.2 Risks
| Risk | Mitigation |
|---|---|
| Trocador unavailable | Polling fallback; BTCPay failover in v2 |
| No VPN configs for region | Spare pool, disable region, alert |
| Server overloaded | Queue, 30-instance limit, alerts |
| User loses order number | No recovery by design; PIN is secondary safeguard |
| Abuse / illegal activity | Operational logs, ability to block regions, no browsing retention |
| Container escape | cap-drop=ALL, minimal caps, no-new-privileges, internal network |
17. Appendix: Environment Variables
# Core
DOMAIN=privatebrowser.example.com
NODE_ENV=production
# Database
DATABASE_URL=postgresql://bvpn:DB_PASSWORD@postgres:5432/browser_vpn
DB_PASSWORD=<generated>
# Secrets
COOKIE_SECRET=<openssl rand -base64 32>
ADMIN_PASSWORD_HASH=<bcrypt hash>
TOTP_SECRET=<base32>
# Payments (MVP)
TROCADOR_XMR_ADDRESS=<XMR address>
TROCADOR_WEBHOOK_KEY=<random 32+>
TROCADOR_FIAT_CURRENCY=USD
# Notifications
TELEGRAM_BOT_TOKEN=<optional>
TELEGRAM_CHAT_ID=<optional>
# Capacity
MAX_INSTANCES=30
CPU_THRESHOLD=80
RAM_THRESHOLD=80
CONTAINER_MEM_LIMIT=2g
CONTAINER_CPU_LIMIT=1.0
CONTAINER_PIDS_LIMIT=200
# Timeouts
PAYMENT_TIMEOUT_MINUTES=30
FIRST_ACCESS_TIMEOUT_MINUTES=60
IDLE_TIMEOUT_MINUTES=30
CONTAINER_START_TIMEOUT_SECONDS=60
# Backup
BACKUP_RETENTION_DAYS=7
OFFSITE_BACKUP_ENABLED=true
OFFSITE_BACKUP_DESTINATION=user@backup-server:/backups/browser-vpn/
GPG_RECIPIENT=<KEY_ID>
18. Environments
18.1 Local
- Docker Compose na lokalnej maszynie deweloperskiej.
- Testowy Trocador AnonPay + 100% discount code.
- Nie wymaga realnych domen ani certyfikatów.
18.2 Staging
- Osobny serwer lub subdomena
staging.<DOMAIN>. - Własna instancja PostgreSQL, Traefik z Let's Encrypt staging.
- Pełny flow testowy z realnym Trocador (małe kwoty).
- Smoke tests uruchamiane przed deployem do produkcji.
18.3 Production
- Główna domena
<DOMAIN>+admin.<DOMAIN>. - Let's Encrypt production.
- Off-site backup co 6h przez SSH/rsync.
- Telegram alerts aktywne.
19. Open Questions
- Czy Trocador wymaga osobnego
TROCADOR_API_KEYpozaTROCADOR_WEBHOOK_KEY?
Odpowiedź: Nie, AnonPay działa bez API key. - Jaki dokładny format response Trocador dla
direct=false?
Odpowiedź: Niezweryfikowany — docs 503, do potwierdzenia w implementacji.
Finalna specyfikacja gotowa do weryfikacji. 2026-07-23.