Browser-VPN SaaS

Finalna specyfikacja i plan implementacji · 2026-07-23

Spis treści

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)

  1. Landing page z formularzem zakupu (browser, region, plan, discount code).
  2. Tworzenie zamówienia i generowanie ord-XXXXXX-XXXXXX.
  3. Integracja Trocador AnonPay (create payment, webhook, polling fallback).
  4. Kody rabatowe, w tym 100% discount do testowania.
  5. PostgreSQL: orders, sessions, vpn_configs, discount_codes, tickets, system_logs, server_metrics, admin_audit_logs.
  6. Start/stop kontenerów Docker z wybraną przeglądarką i WireGuard.
  7. Pool konfiguracji WireGuard per region.
  8. Magic link + device binding przez httpOnly cookie.
  9. Recovery przez order number + 6-cyfrowy PIN (bcrypt, max 3 próby, blokada 24h).
  10. Sesja odliczana od pierwszego dostępu, 1h buffer, idle timeout 30 min.
  11. Strona podsumowania po wygaśnięciu przez 30 dni.
  12. Limit 30 instancji, kolejka gdy CPU/RAM > 80% lub brak slotów.
  13. Panel admina: dashboard, zamówienia, kontenery, VPN configs, tickety, ustawienia, raporty CSV.
  14. Podstawowe monitoring i alerty Telegram.
  15. Codzienne backupy lokalne, off-site co 6h.
  16. CSP/HSTS, rate limiting, TOTP admina, audit log.

2.2 Out of Scope for MVP


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

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

  1. User wybiera browser, region, plan, opcjonalny discount code.
  2. POST /api/v1/orders tworzy order pending.
  3. Backend wywołuje Trocador AnonPay (direct=false):
  4. ticker_to=xmr
  5. network_to=Mainnet
  6. address=<TROCADOR_XMR_ADDRESS>
  7. fiat_equiv=USD lub amount USD przeliczone przez Trocador
  8. webhook=https://<domain>/api/v1/payments/trocador/webhook
  9. webhook_key=<TROCADOR_WEBHOOK_KEY>
  10. Backend wyświetla order number, QR code, payment URL i ostrzega przed zamknięciem karty.
  11. User płaci dowolną kryptowalutą; Trocador zamienia na XMR.
  12. Trocador wysyła webhook na każdą zmianę statusu.
  13. Backend weryfikuje Webhook-Key i aktualizuje order do paid lub underpaid.
  14. Fallback: polling co 30s gdy webhook nie przyjdzie > 15 min.
  15. Scheduler anuluje zamówienia pending starsze niż 30 min.

5.2 Activation Flow

  1. User wraca na /order/:orderNumber.
  2. Jeśli paid, backend sprawdza zasoby:
  3. CPU < 80%
  4. RAM < 80%
  5. active containers < MAX_INSTANCES
  6. Jeśli zasoby dostępne:
  7. wybiera nieużywany VPN config dla regionu,
  8. tworzy per-container network --internal,
  9. uruchamia kontener z Traefik labels,
  10. tworzy sesję i magic link https://domain.com/ses-abc123,
  11. recovery PIN generowany i wyświetlany raz,
  12. zwraca magic link userowi.
  13. Jeśli brak zasobów:
  14. status queued,
  15. user widzi pozycję w kolejce i szacowany czas.

5.3 Session Usage Flow

  1. User klika magic link.
  2. Backend weryfikuje: session exists, not expired, device matches or no device bound yet.
  3. Przy pierwszej wizycie:
  4. generuje deviceToken,
  5. zapisuje hash w sesji i w orderze,
  6. ustawia httpOnly cookie Secure; SameSite=Strict; Path=/ses-abc123.
  7. User widzi stream Selkies przez Traefik (port 3000 kontenera, ścieżka /websocket dla danych).
  8. Sesja liczy czas od pierwszego dostępu.
  9. Heartbeat co 30s od frontendu do backendu; idle timeout 30 min → stop kontenera.
  10. First access timeout 1h: jeśli user nie otworzy magic linka → stop + zwolnij config.

5.4 Expiration Flow

  1. Scheduler stopuje i usuwa kontener po upływie czasu.
  2. User widzi "Session expired" w przeglądarce.
  3. Przez 30 dni magic link pokazuje summary page.
  4. 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

9.2 Admin Authentication

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

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


12. API Specification

12.1 Versioning

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


14. Smoke Tests & Acceptance Criteria

14.1 Smoke Test Framework

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

18.2 Staging

18.3 Production


19. Open Questions

  1. Czy Trocador wymaga osobnego TROCADOR_API_KEY poza TROCADOR_WEBHOOK_KEY?
    Odpowiedź: Nie, AnonPay działa bez API key.
  2. 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.