Production Vite + React + TypeScript website for VEZvision. Public CMS reads use a restricted PostgREST role, form writes use the Node API, and data is stored in dedicated PostgreSQL and MinIO services on Hetzner. Sentry and Google Analytics are loaded only after consent.
- React 19, React Router 7, Vite 8, TypeScript strict mode
- TanStack Query for public settings cache/invalidation
- PostgreSQL 18, PostgREST, Node API and MinIO
- Vitest + Testing Library for unit/regression tests
- Playwright for desktop/mobile smoke tests
- ESLint flat config with type-aware TypeScript and accessibility rules
Create .env from .env.example:
VITE_API_URL=http://localhost:8080
VITE_PUBLIC_ASSETS_URL=http://localhost:9000
VITE_SENTRY_DSN=https://your-sentry-dsn
VITE_GA_ID=G-XXXXXXXXXXOnly VITE_* values are exposed to the browser. The browser never receives a database password or administration token.
npm ci
npm run devThe local dev server runs on port 5174.
Run the same checks used by CI:
npm run check
npm run lint
npm run test:unit
npm run build
npm audit --audit-level=moderate
npm run test:e2eInstall Playwright browsers before the first E2E run:
npx playwright install chromiumE2E builds enable a hidden probe route (/__e2e__/error) via E2E_BUILD=1 so Playwright can verify RouteErrorBoundary without shipping that route in normal production builds (npm run build without E2E_BUILD).
Optional live API smoke:
E2E_LIVE_API=1 VITE_API_URL=... npm run test:e2e:live- TypeScript strict mode is enabled.
- Arbitrary CMS script/style injection is not allowed.
CodeInjectoronly accepts non-executablemetaand allowlistedlinktags in<head>, and sanitized non-script body markup. - Legacy service workers are unregistered at startup; there is no custom cache-first service worker.
- Analytics and Sentry initialize only after analytics consent.
- The Node API validates public input server-side, uses parameterized SQL, Turnstile and persistent rate limits.
- Dependency audit is part of CI.
Do not put Google Tag Manager, Meta Pixel, or other executable snippets in code_injection.head or code_injection.body. They are stripped by design.
Use instead:
- Google Analytics: set
VITE_GA_IDand rely on cookie-consent gating insrc/lib/googleAnalyticsConsent.ts. - Sentry: set
VITE_SENTRY_DSNand consent handling insrc/lib/sentryConsent.ts. - Domain verification / SEO tags: safe
<meta>and allowlisted<link rel="canonical|icon|…">entries incode_injection.head.
When maintenance is enabled in vv_site_settings, the public app verifies the state through the Node API. Browser origins are allowlisted with ALLOWED_ORIGINS; requests carrying a different Origin are rejected.
GitHub Actions workflow: .github/workflows/ci.yml.
It runs install, typecheck, lint, unit tests, production build, npm audit, and Chromium Playwright smoke tests.
| Endpoint | Purpose |
|---|---|
POST /functions/v1/check-maintenance-access |
Maintenance state |
POST /functions/v1/get-code-injection |
Public CMS snippets |
POST /functions/v1/increment-blog-view |
View counter with IP deduplication |
POST /functions/v1/submit-contact |
Contact form write |
POST /functions/v1/subscribe-newsletter |
Newsletter subscription |
POST /functions/v1/unsubscribe-newsletter |
Token unsubscribe |
These endpoints are served by the self-hosted Node API in backend/; public reads are served by PostgREST through a GET-only gateway. The browser only needs VITE_API_URL.
| Env (client) | VITE_TURNSTILE_SITE_KEY |
| Env (server) | TURNSTILE_SECRET_KEY |
Turnstile may be omitted for local development. Production starts fail closed unless
the site key is built into the frontend and both TURNSTILE_SECRET_KEY and
TURNSTILE_EXPECTED_HOSTNAMES are configured in the API environment.
Before every production build, npm run build verifies CSP sources (verify:security) and dist/ artifacts (verify-production-build.mjs).
- Frontend and API are built by Coolify from
frontend.Dockerfileandcoolify-compose.yml. - Production runs on Hetzner. The old Hostido deploy job is intentionally absent.
- Coolify environment values are the source of truth for production build and server secrets.
- Use separate
API_DATABASE_URLandPOSTGREST_DATABASE_URLvalues backed by dedicated least-privilege roles; never give the public PostgREST container the API write role or a database-owner credential. - Use npm (
npm ci) for installs; do not commit alternate lockfiles. - Keep Browserslist data current with
npm update caniuse-lite browserslistwhen build warnings appear. - Do not commit local files such as
.env,.DS_Store, Playwright traces, or reports.