# EpicNext-CMS v1.0.1 A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Drizzle ORM** and **React 19**. Designed to integrate seamlessly with Polaris / Arcturus Morningstar MySQL/MariaDB databases. Features a premium animated homepage (typewriter hero, floating orbs, scroll counters), a full admin panel, NextAuth authentication (argon2id hashing with legacy md5/bcrypt auto-upgrade), real-time RCON communication, Server-Sent Events for live radio data, smooth page transitions, and PM2 production deployment. --- ## System Requirements | Component | Version | Notes | | --------------- | -------------- | ---------------------------------------- | | Node.js | >= 22 | Required by Next.js 16 | | pnpm | >= 10.33.4 | Package manager (npm/yarn not supported) | | MySQL / MariaDB | 8.0+ / 10.6+ | Shared with the emulator | | Redis | 7.x+ | Optional — caching, rate limiting, SSE | | Java | 17+ | Required only if building the emulator | | Maven | 3.9+ | Required only if building the emulator | --- ## Quick Start ### 1. Clone & Install ```bash git clone https://gitlab.epicnabbo.nl/remco/EpicNext-Cms.git cd EpicNext-Cms pnpm install ``` ### 2. Database Setup The CMS shares a database with the Polaris/Arcturus emulator. Use an existing database or create a new one: ```sql CREATE DATABASE IF NOT EXISTS epicnext_cms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ``` The CMS reads emulator-owned tables (`users`, `items`, `rooms`, `bans`, etc.) directly. It never creates, alters, or drops them. The Drizzle schema in `src/db/schema.ts` is generated from the existing database structure and does not modify it. > **Note:** The CMS does **not** own the emulator schema — it maps to those tables via Drizzle. Never run `drizzle-kit push` / `migrate` against the shared DB. CMS-owned tables (`website_*`, `radio_*`, etc.) are created via idempotent SQL in `drizzle/migrations/` (`pnpm db:migrate`). ### 3. Configure Environment ```bash cp .env.example .env ``` Edit `.env` with at minimum: ```dotenv DATABASE_URL=mysql://user:password@127.0.0.1:3306/epicnext_cms AUTH_SECRET= HOTEL_NAME=YourHotel APP_URL=http://localhost:3000 ``` See `.env.example` for all optional variables (RCON, email, Redis, OAuth, PayPal, etc.). ### 4. ORM Setup & Type Generation #### Drizzle ORM (Primary Data Layer) Drizzle ORM is the runtime data layer. The connection is a singleton in `src/lib/db.ts`: ```ts import { db } from "@/lib/db"; import { users } from "@/db/schema"; import { eq } from "drizzle-orm/expressions"; const found = await db.select() .from(users) .where(eq(users.username, "hello")); ``` **Drizzle CLI** (`drizzle-kit`) is used for local development tasks — it is a devDependency and is never bundled in production. | Command | What it does | | ------- | ------------ | | `pnpm db:generate` | Draft SQL from Drizzle schema into `drizzle/drafts/` (review + copy into `drizzle/migrations/`) | | `pnpm db:studio` | Open Drizzle Studio (dev only) | | `pnpm db:introspect` | Reverse-engineer an existing DB into a Drizzle schema draft | | `pnpm db:schema:generate` | Regen committed `src/db/schema.ts` from previous schema names + live DB | > The CMS does **not** use `drizzle-kit push` or `drizzle-kit migrate` — the database is shared with the emulator. Apply CMS DDL only via `pnpm db:migrate`. Use `import { db } from "@/lib/db"` with table definitions from `src/db/schema.ts` for all database access. Types come from the committed Drizzle schema — no separate client code generation is required at build time. ### 5. Run CMS Migrations ```bash pnpm db:migrate ``` Creates all CMS-owned tables (`website_*`, `radio_*`, `acl_*`, `admin_audit_log`, etc.) via idempotent SQL files in `drizzle/migrations/`. Emulator tables are never touched. Check migration status: ```bash pnpm db:migrate:status ``` ### 6. Build & Start ```bash # Development (hot reload) pnpm dev # Production pnpm build && pnpm start ``` Open `http://localhost:3000` in your browser. ### 7. First Login 1. Register an account at `/register`, or log in with an existing emulator account. 2. Grant yourself admin access: `UPDATE users SET rank = 7 WHERE username = 'yourname';` 3. Visit `/admin` and configure your hotel via **Admin → CMS Settings**. --- ## Nginx Configuration The CMS is designed to run behind an nginx reverse proxy. Below is a reference configuration covering SSL termination, WebSocket upgrade, proxy caching, and the Habbo imager integration. ### Prerequisites - SSL certificates in `/etc/ssl/cert.pem` and `/etc/ssl/key.pem` (or use Let's Encrypt) - Next.js running on `127.0.0.1:3000` (default) or your configured port - Habbo imager (optional) running on `127.0.0.1:3030` ### Reference Configuration Create a file in `/etc/nginx/sites-available/epicnext` and symlink it to `sites-enabled`: ```nginx # ========================================== # GLOBAL SETTINGS # ========================================== server_tokens off; gzip on; gzip_vary on; gzip_proxied off; gzip_comp_level 6; gzip_min_length 256; gzip_types text/plain text/css text/javascript application/json application/javascript application/xml application/xml+rss image/svg+xml font/opentype font/ttf font/woff font/woff2; # ========================================== # REDIRECT HTTP → HTTPS # ========================================== server { listen 80; listen [::]:80; server_name yourdomain.com www.yourdomain.com; location /.well-known/acme-challenge/ { root /var/www/epicnext/public; } location / { return 301 https://$host$request_uri; } } # ========================================== # MAIN HTTPS SERVER # ========================================== server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name yourdomain.com www.yourdomain.com; root /var/www/epicnext/public; index index.html; # SSL Certificates ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # Security Headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; client_max_body_size 20m; client_body_timeout 30s; client_header_timeout 10s; keepalive_timeout 15s; send_timeout 10s; # Shared Proxy Settings proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffers 16 16k; proxy_buffer_size 32k; # ------------------------------------------ # Static Files # ------------------------------------------ location ^~ /nitro-client/ { alias /var/www/Nitro-V3/dist/; expires 7d; add_header Cache-Control "public"; access_log off; } location = /favicon.ico { expires 1y; access_log off; log_not_found off; try_files $uri =404; } location = /robots.txt { expires 1d; access_log off; log_not_found off; try_files $uri =404; } # ------------------------------------------ # Next.js Assets (immutable, long cache) # ------------------------------------------ location /_next/static/ { proxy_pass http://127.0.0.1:3000; add_header Cache-Control "public, max-age=31536000, immutable"; } location /_next/data/ { proxy_pass http://127.0.0.1:3000; add_header Cache-Control "public, max-age=0, must-revalidate"; } # ------------------------------------------ # API Routes (never cached) # ------------------------------------------ location /api/ { proxy_pass http://127.0.0.1:3000; add_header Cache-Control "no-cache, no-store, must-revalidate"; } # ------------------------------------------ # Habbo Imager (optional) # ------------------------------------------ # Proxies to a Docker container that renders Habbo avatars. # The imager caches renders to disk, so a long s-maxage is safe. location /imaging { proxy_pass http://127.0.0.1:3030; add_header Cache-Control "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400" always; } # ------------------------------------------ # WebSocket (Radio / SSE) # ------------------------------------------ location /ws { proxy_pass http://127.0.0.1:3030; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; } # ------------------------------------------ # Main Page Proxy (with HTML caching) # ------------------------------------------ # The CMS middleware sets: # Cache-Control: public, s-maxage=300, stale-while-revalidate=300 (anonymous) # Cache-Control: private, no-store (authenticated) # # nginx caches anonymous responses and serves them directly, bypassing # the Node.js process entirely. Authenticated responses are never cached. # # proxy_cache_valid: cache 200 responses for 60 seconds # proxy_ignore_headers Vary: Next.js emits many Vary headers (rsc, # next-router-*, Accept-Encoding) that would fragment the cache key. location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header CF-Connecting-IP $http_cf_connecting_ip; proxy_http_version 1.1; proxy_buffering on; proxy_cache html_cache; proxy_cache_valid 200 60s; proxy_cache_key "$host$request_uri"; proxy_ignore_headers Vary; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_background_update on; proxy_cache_revalidate on; add_header X-Cache-Status $upstream_cache_status always; } # ------------------------------------------ # Health Check # ------------------------------------------ location /health { access_log off; return 200 "OK"; add_header Content-Type text/plain; } # Block hidden files location ~ /(\.|vendor|storage/logs/|\.(sql|sqlite|sqlite3)$) { deny all; access_log off; log_not_found off; } } ``` ### HTML Caching The CMS uses an **origin-level proxy cache** for anonymous HTML pages. This means: - **Anonymous visitors** receive cached HTML directly from nginx (~1ms), skipping the Node.js process entirely. - **Authenticated visitors** always hit Node.js (personalized content). - The cache is **auto-invalidated** after 60 seconds and revalidates in the background. The proxy cache zone is defined in the `http` block (above any `server` block): ```nginx proxy_cache_path /var/cache/nginx/html_cache levels=1:2 keys_zone=html_cache:50m max_size=500m inactive=10m use_temp_path=off; ``` Verify caching works by checking the `X-Cache-Status` response header: ```bash # First request (MISS = fetched from Node.js, now cached) curl -sI https://yourdomain.com/ | grep X-Cache-Status # → X-Cache-Status: MISS # Second request (HIT = served from nginx cache) curl -sI https://yourdomain.com/ | grep X-Cache-Status # → X-Cache-Status: HIT ``` ### Key Points | Setting | Value | Why | | ------- | ----- | --- | | `proxy_http_version 1.1` | HTTP/1.1 to upstream | Required for keep-alive and chunked transfer | | `proxy_buffering on` | Buffer upstream response | Required for proxy_cache to work with chunked responses | | `proxy_ignore_headers Vary` | Ignore upstream Vary | Next.js emits dynamic Vary headers (rsc, next-router-*) that would fragment the cache | | `proxy_cache_valid 200 60s` | Cache 200s for 60s | Balances freshness with performance | | `proxy_cache_use_stale` | Serve stale on error | Keeps the site available during brief upstream outages | --- ## Production Deployment (PM2) ```bash pnpm build pm2 start pnpm --name "next" -- start pm2 save ``` Restart after updates: ```bash git pull pnpm install pnpm build pm2 restart next ``` The CMS runs behind an nginx reverse proxy on the default port 3000. Static assets (media uploads) are persisted via `/api/media/*` and survive rebuilds. --- ## Scripts | Command | Description | | ---------------------- | -------------------------------------------------- | | `pnpm dev` | Start development server (hot reload) | | `pnpm build` | Production build | | `pnpm start` | Start production server | | `pnpm typecheck` | Run TypeScript type checking | | `pnpm test` | Run all tests (Vitest) | | `pnpm db:migrate` | Apply pending SQL migrations | | `pnpm db:migrate:status` | Show migration status | | `pnpm db:schema:generate` | Regen `src/db/schema.ts` from prior schema + live DB | | `pnpm db:generate` | Draft SQL via drizzle-kit → `drizzle/drafts/` | | `pnpm db:studio` | Drizzle Studio (dev) | | `pnpm db:introspect` | drizzle-kit introspect (draft) | | `pnpm analyze` | Build + open bundle analyzer | | `pnpm jobs:worker` | Start background task worker (systemd / PM2) | | `pnpm biome:check` | Lint and format code | **Drizzle Kit notes:** `db:generate` / `db:introspect` write drafts only. Reviewed SQL must be copied into `drizzle/migrations/` as a new numbered file, then applied with `pnpm db:migrate`. Never run `drizzle-kit push` or `drizzle-kit migrate` against production. --- ## Performance Features | Feature | Description | | ------------------------------ | -------------------------------------------------------------- | | **React Compiler** | Automatic memoization — reduces unnecessary re-renders | | **View Transitions API** | Native browser transitions between page navigations | | **Lenis Smooth Scroll** | Fluid, customizable scrolling (respects `prefers-reduced-motion`) | | **Server-Sent Events** | Real-time radio now-playing & listeners via SSE (no polling) | | **Redis Caching** | Caches API responses (home, radio config) up to 30s in Redis | | **Bundle Analyzer** | Run `pnpm analyze` to visualize and optimize bundle sizes | | **RCON (TCP Socket)** | Live commands to the emulator (credits, badges, kick, ban) | | **Streaming & Suspense** | Next.js App Router streaming for fast page loads | | **Automatic Image Optimization** | `next/image` with Sharp for resizing and WebP/AVIF | | **CSS-based Animations** | `input-glow`, `btn-shine`, `Reveal` scroll animations, floating orbs | | **Compression** | Gzip compression enabled on all responses | | **Stale Times** | Optimized router cache (30s dynamic, 180s static) | | **PWA** | Service worker with skip-waiting, stale CSS chunk recovery | | **Biome** | Fast linting and formatting (replaces ESLint + Prettier) | --- ## Architecture ``` ├── drizzle/ │ ├── migrations/ # CMS SQL migrations (idempotent, tracked in cms_migrations) │ └── drafts/ # drizzle-kit generate output (never auto-applied) ├── scripts/ │ ├── apply-migrations.ts # SQL migration runner (apply + status) │ ├── jobs-worker.ts # Background task scheduler │ ├── merge-config.cjs # Utility: merge split config files │ └── generate-drizzle-schema.mjs # Regen src/db/schema.ts from schema + live DB ├── src/ │ ├── db/ │ │ ├── schema.ts # Drizzle ORM schema (committed — runtime data layer) │ │ └── relations.ts # Drizzle relations │ ├── app/ # Next.js App Router (pages & API routes) │ ├── actions/ # Server Actions │ ├── components/ # UI components │ ├── lib/ │ │ ├── auth/ # NextAuth, password hashing, 2FA, SSO tickets │ │ ├── services/ # RCON, email, currency, PayPal, alerts │ │ ├── db.ts # Drizzle connection singleton (runtime) │ │ ├── cached-db.ts # Redis-backed query cache helpers │ │ ├── redis.ts # Redis client (ioredis) │ │ ├── redis-cache.ts # Redis caching utility for API routes │ │ ├── cache.ts # In-memory cache fallback │ │ ├── motion.ts # Framer Motion animation variants │ │ └── use-event-source.ts # React hook for SSE subscriptions │ ├── messages/ # i18n translations (en, nl, de, fr, es, it, pt, da, no, sv) │ └── env.ts # Zod-validated environment schema ├── public/ │ ├── assets/ # Images, icons, fonts │ └── scripts/ # Client-side scripts (theme-init.js) ├── update-Nitrov3.sh # Emulator & Nitro updater utility ├── next.config.ts # Next.js configuration └── .env.example # Environment template with all options ``` ### Database Ownership | Component | Type | Migrations | | ------------------------------------------------- | ----------------------- | ----------------------------------- | | Emulator tables (`users`, `items`, `rooms`, etc.) | Existing Polaris schema | None — CMS reads/writes only | | CMS tables (`website_*`, `radio_*`, etc.) | CMS-owned | `drizzle/migrations/*.sql` | | Migration tracking | `cms_migrations` table | Auto-created by migration runner | ### ORM Architecture - **Runtime (Drizzle ORM)**: `@/lib/db` exposes a Drizzle singleton. Schema lives in `src/db/schema.ts`. - **Schema regeneration**: `pnpm db:schema:generate` reuses field/table names from the previous `src/db/schema.ts` and refreshes column types from the live DB. - **Drizzle Kit**: studio / generate / introspect for local tooling; CMS apply path remains `pnpm db:migrate`. Use `import { db } from "@/lib/db"` with queries built via `src/db/schema.ts`. --- ## Optional Integrations ### Emulator — RCON For live actions (credits, badges, rank changes, kick, ban, hotel alerts). The emulator must be running with RCON enabled at `RCON_HOST:RCON_PORT`. ### Client — Nitro The `/client` page loads the Nitro client. Configure the client URL via **Admin → CMS Settings** (`nitro_client_url`). See `update-Nitrov3.sh` for deployment. ### Radio Supports any streaming radio (e.g., Azuracast). Configure endpoints via CMS Settings: - `radio_now_playing_api_url` - `radio_listeners_api_url` The radio player uses SSE for real-time updates (10s interval, no polling). ### Background Jobs Run as a persistent process: ```bash pnpm jobs:worker ``` Scheduled tasks: - **Daily 03:00** — Emulator JAR backup (requires `EMULATOR_JAR_PATH` + `EMULATOR_BACKUP_DIR`) - **Daily 04:00** — Cleanup login logs (>30 days) and expired password reset tokens (>7 days) ### CAPTCHA Supports Cloudflare Turnstile and Google reCAPTCHA. Configure via CMS Settings. ### Theming 12 preset themes with fully customizable colors via **Admin → Theme** (`/admin/theme`). ### AI Content Moderation Optional OpenAI-powered moderation for comments and guestbook posts. Set `OPENAI_API_KEY`. --- ## Development ```bash pnpm dev # Start with hot reload pnpm typecheck # Type check all files pnpm test # Run test suite pnpm analyze # Build and analyze bundle sizes pnpm biome:check # Lint and format ``` ### Contributing 1. Ensure typecheck and tests pass: `pnpm typecheck && pnpm test` 2. Follow existing code conventions (Server Components where possible, minimal client boundaries) 3. Use the `src/lib/motion.ts` animation variants for consistent animations 4. SQL migrations in `drizzle/migrations/` must be idempotent 5. For new database code, use the Drizzle runtime directly (`import { db } from "@/lib/db"`) — see [ORM Setup](#4-orm-setup--type-generation) 6. Avoid `any` — use `eslint-disable` or `biome-ignore` comments only when unavoidable --- ## License CC BY-NC-SA 4.0. See `LICENSE` for details.