Next.js 16, WordPress REST, Stripe, Docker
nuviaworld.hu
Full-stack developer · Apr 2026 – present
I built a live membership wellness and mentoring platform in a two-person team, owning most of the commits across frontend, backend, and infra. The public app is Next.js 16 (App Router, React 19, TypeScript); the CMS is a custom WordPress REST backend on PHP 8.3. The product handles registration and JWT auth, a community feed, LMS-style courses, podcasts, events, consultation booking, Stripe subscriptions, and Hungarian Billingo invoicing. Video uploads go from the browser straight to Bunny Stream over TUS; chat and notifications run on Pusher. Isolated Docker stacks cover test and production, with GitHub Actions, Caddy, Vitest, Playwright, and PHPUnit in the pipeline.
Browser (nuviaworld.hu)
│ App Router, SSR, JWT cookie
▼
Next.js ── REST/JWT ──► WordPress (headless)
│ │
├─ Bunny Stream (video) ├─ custom plugins (domain + REST)
├─ Pusher (realtime) ├─ Stripe webhook
└─ Stripe Checkout └─ Billingo (async invoice queue)
Test and prod: isolated Docker stacks on one VPS.
Deploy swaps app code only — never DB, uploads, or third-party plugins.Deep dive
Membership product, not a CMS theme
The frontend is the product. WordPress supplies domain REST, admin UI, and webhooks — not a classic theme plus jQuery stack. JWT plus capability checks protect App Router routes; roles are member, instructor, and admin. Modules include the paid membership funnel, a cursor-paginated community feed, LMS courses with quizzes and progress, VIP-gated podcast and blog content, events with capacity and live rooms, instructor-calendar consultations, group chat and DMs, instructor stats/referral/payouts with encrypted bank details, a ledger-based points engine, and a public landing that showcases live courses from the API.
Paid funnel: Stripe plus Billingo
Checkout, subscriptions, plan changes, cancellation, coupons, and webhooks run through Stripe. Hungarian invoices are not issued on the Stripe webhook thread: the webhook only enqueues work, and a cron talks to Billingo with a unique vendor_id so retries never duplicate a bill. Members can download invoice PDFs. Bank transfer sits beside card as an extra path.
Video never goes through WordPress
A Next.js Server Action body limit makes WordPress-proxied uploads a dead end. The browser talks TUS straight to Bunny Stream (chunked, parallel, retry, resume); WordPress only issues tokens and tracks encode status. Instructors get a media library without hitting CMS upload caps. Live rooms use Daily.co or MediaMTX depending on the event or consultation.
Community, events, consultations
The feed supports text, image, video, and polls, plus comments, likes, shares, and moderation. Group channels and 1:1 DMs (including voice notes) plus in-app notifications run on Pusher. Events handle signup, capacity, invites, and live broadcast. Consultations close the loop from instructor calendar through approve/reject to a live room.
The REST hot path
Several plugins ran schema migration on every request. A version guard plus Redis object cache cut per-request database work on hot public GETs by about 16× — measured under load, not estimated. Test and production share a VPS but run as isolated Compose stacks (separate volumes and databases). GitHub Actions publishes images to GHCR; Caddy terminates TLS. A deploy replaces application code only.
Tests without a live CMS
Playwright talks to a mock WordPress REST API, including a11y checks. PHPUnit uses Brain Monkey to stub WordPress APIs so custom plugins can be tested without booting CMS. Lint, typecheck, unit, and E2E run on every pull request.