A headless storefront for MSK Scripts — built with Next.js 15, React 19, TypeScript, Tailwind CSS and the Tebex Headless API.
Live: msk-scripts.de
| Framework | Next.js 15.5 (App Router) |
| Language | TypeScript 5.8 (strict mode) |
| UI | React 19.2 |
| Styling | Tailwind CSS 3.4 |
| Fonts | Inter (self-hosted via next/font) |
| State | Zustand 5 (persisted to localStorage) |
| Data Fetching | SWR 2 |
| Database | MariaDB / MySQL (via mysql2) |
| Payments | Tebex Headless API |
| Editor | CodeMirror (@uiw/react-codemirror) — Bot-Config editor |
| JSONC parsing | jsonc-parser |
| Icons | lucide-react |
| Cookies (client) | js-cookie |
| Auth | CFX.re (FiveM) + Discord OAuth via Tebex |
| Verify Flow | Discord OAuth + GitHub OAuth + signed session cookies |
| Server | Debian + Apache2 reverse proxy + systemd |
| Bot process manager | PM2 (pm2-musiker15.service) |
| CI/CD | GitHub Actions (auto-deploy on push to main) |
- 🛒 Full shopping cart with persistent state (survives page reload)
- 🔐 FiveM (CFX.re) authentication via Tebex
- 💬 Discord OAuth for role assignment after purchase
- 🎁 Gift packages with optional recipient Discord ID
- 🏷️ Coupon code support (apply & remove)
- 🔖 Custom badges, tags and descriptions per package
- 📦 Custom packages section (Discord Bots, GitHub, etc.)
- 📄 Markdown-based legal pages in English & German (editable without code)
- 🟢 Live Discord online member count
- 📰 News popup with optional coupon code display (configurable, shown on every page load)
- 📊 Public Ticket Bot statistics page (
/stats) — with allowlist viaSTATS_IGNORED_API_KEYS - 🎟️ Ticket Bot verify flow — Discord + GitHub OAuth, API key issuance, tier management
- 🗂️ Ticket transcript hosting with attachment support (MariaDB-backed)
- 🌍 Custom domain support per guild with DNS validation and Let's Encrypt SSL
- 💰 GitHub Sponsors webhook — auto-assigns tiers on sponsorship events
- 📊 Dashboard page for managing API keys, domains and transcripts
- 🤖 Hosted bot management for
is_hostedcustomers:- Config editor for
config.jsonc,snippets.jsonc,.envand the active locale file (locales/<lang>.json, withen.jsonfallback) - Bot control: start / stop / restart / update (git pull) via PM2
- Live log console with Server-Sent Events streaming PM2 error logs (
tail -F)
- Config editor for
- 🚪 Dashboard logout endpoint to switch between bots
- 🔒 Security headers, rate limiting, path traversal protection, signed session cookies
- 🌐 Apache2 reverse proxy with CSP, HSTS and security headers
- 🔧 Maintenance page included (
public/maintenance.html) - 🚀 Auto-deploy via GitHub Actions on push to
main
app/ Next.js App Router pages & API routes
├── api/auth/
│ ├── discord-verify/ Discord OAuth for the verify flow (scopes: identify, guilds)
│ │ └── callback/
│ └── github/ GitHub OAuth for sponsor tier detection
│ └── callback/
├── api/basket/ Tebex basket API proxy (private key stays server-side)
│ ├── auth.ts Shared auth helper for basket routes
│ ├── route.ts Create basket
│ └── [ident]/
│ ├── auth/ Auth provider URLs (CFX.re / Discord)
│ ├── coupons/ Apply & remove coupons
│ │ └── [code]/ Remove specific coupon
│ ├── packages/ Add & remove packages (+ remove/)
│ └── route.ts Fetch basket
├── api/bot-config/ Read & write hosted bot configs (config.jsonc, snippets.jsonc, .env, locales/<lang>.json)
├── api/bot-control/ Start / stop / restart / update bot via PM2
├── api/bot-logs/ Fetch last 100 lines of PM2 error log (one-shot)
├── api/bot-logs-stream/ Server-Sent Events — real-time PM2 log stream via tail -F
├── api/dashboard/
│ └── logout/ Clear dashboard session cookie (switch bot)
├── api/debug/ Debug route (returns 404 in production)
├── api/discord/ Discord online member count (cached 60s)
│ └── health/ Discord API health check
├── api/domain/ Custom domain set / remove / validate
├── api/packages/ Package list endpoint
├── api/stats/ Public Ticket Bot statistics
├── api/transcript/upload/ Ticket transcript upload (authenticated via API key)
├── api/verify/ Verify status / complete / check-guild / redirect-dashboard
├── api/webhook/
│ └── github-sponsors/ GitHub Sponsors webhook handler (HMAC-SHA256 verified)
├── account/ User account page
├── auth/discord/ Discord OAuth callback handler (purchase flow)
├── cart/ Cart page
├── categories/[id]/ Category pages (+ loading.tsx)
├── checkout/ Post-payment redirect handler
├── dashboard/ API key, domain & hosted-bot management
├── login/ Login page
├── packages/ Full package list page
│ └── [id]/ Package detail pages (+ loading.tsx)
├── stats/ Public Ticket Bot statistics page
├── verify/ Ticket Bot verify flow
└── terms/ Legal pages
├── imprint/ Imprint (EN + DE)
├── privacy/ Privacy Policy (EN + DE, GDPR compliant)
└── page.tsx Terms & Conditions (EN + DE)
components/
├── cart/ CartDrawer (slide-in)
├── home/ Hero, InfoSection, CTASection, Divider
├── layout/ Navbar, Footer
├── legal/ LegalContent (language switcher)
├── packages/ PackageCard, AddToCartButton, PackagePrice
├── BotConfigEditor.tsx Hosted-bot dashboard — config editor (incl. locale tab), bot control, live log console
├── SalePriceFetcher.tsx Client component — pre-fetches sale prices on mount
└── ui/ DiscordButton, NewsPopup
content/
├── custom-packages.ts Non-Tebex packages (Discord Bots, GitHub, etc.)
└── legal/ Editable Markdown files — no code needed
├── imprint.md / imprint-de.md
├── privacy.md / privacy-de.md (GDPR / DSGVO)
└── terms.md / terms-de.md
database/
└── schema.sql MariaDB schema — run once on a fresh database
lib/
├── auth.ts Auth URL helpers (basket auth providers)
├── config.ts All shop configuration (packages, badges, news popup, etc.)
├── dashboardSession.ts Signed dashboard session cookies (guildId)
├── db.ts mysql2 connection pool (singleton) + query/queryOne wrappers
├── i18n.ts Language helpers (EN / DE)
├── markdown.ts Markdown → HTML renderer (tables, lists, links, code)
├── rateLimit.ts In-memory rate limiter for API routes (per IP)
├── session.ts Signed verify session cookies (HMAC-SHA256) + OAuth state
├── statsIgnore.ts API keys excluded from /stats
├── tebex.ts Tebex API client (read-only direct, mutations via /api/basket)
├── tiers.ts Tier definitions (basic / premium / premium_plus) + limits
└── useCart.ts Cart hook (auth flow, basket management)
store/
├── cart.ts Zustand store (persisted to localStorage, key: "msk-cart")
└── salePrices.ts Sale price store (Zustand)
public/
├── logo.png Shop logo
├── favicon.ico
├── maintenance.html Maintenance page (serve via Apache when needed)
└── *.png Custom package banner images
scripts/
├── cleanup.js Housekeeping script (expired transcripts etc.) — daily cron
├── vhost-create.sh Apache2 vhost + Let's-Encrypt SSL setup for custom domains
└── vhost-delete.sh Remove Apache2 vhost for custom domains
Single source of truth for all limits. Tiers: basic · premium · premium_plus.
| Limit | basic | premium | premium_plus |
|---|---|---|---|
| Transcript max. | 10 MB | 100 MB | 250 MB |
| Attachments max. | — | 150 MB | 500 MB |
| Storage retention | 30 days | 60 days | 120 days |
| Custom domain | ✗ | ✓ | ✓ |
| Attachment downloads | ✗ | ✓ | ✓ |
| Uploads / hour | 30 | 60 | 300 |
getExpiresAt(tier) derives the expiry date from storageDays.
The shop uses MariaDB / MySQL for the Ticket Bot feature (API keys, transcripts, custom domains, GitHub Sponsors). The Tebex shop itself does not require a database.
# Create the database and run the schema
mysql -u root -p -e "CREATE DATABASE msk_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p msk_shop < database/schema.sqlTables created by database/schema.sql:
| Table | Purpose |
|---|---|
ticketbot_guilds |
Guild registrations, API keys, tier, custom domain status, is_hosted flag |
ticketbot_transcripts |
Ticket transcript metadata + expiry |
ticketbot_attachments |
File attachments for Premium transcripts |
ticketbot_rate_limits |
Per-API-key request rate limiting (hourly window) |
ticketbot_sponsors |
GitHub Sponsors mirror (written by webhook, read during verify) |
Migration for existing databases:
ALTER TABLE ticketbot_guilds ADD COLUMN is_hosted TINYINT(1) NOT NULL DEFAULT 0;
All shop configuration lives in lib/config.ts:
// Which Tebex packages appear on the homepage
export const FEATURED_PACKAGE_IDS = [5301828, 6446947, 6372865]
// Multiple badges per package
// Variants: 'esx' | 'qb' | 'standalone' | 'js' | 'ts' | 'lua' | 'py' | 'discord' | 'fivem'
export const PACKAGE_BADGES: Record<number, Badge[]> = {
5301828: [{ label: 'ESX', variant: 'esx' }, { label: 'Lua', variant: 'lua' }],
6446947: [{ label: 'ESX', variant: 'esx' }, { label: 'QBCore', variant: 'qb' }, { label: 'Lua', variant: 'lua' }],
}
// Short description shown on package cards
export const PACKAGE_DESCRIPTIONS: Record<number, string> = {
5301828: 'Realistic handcuffs with animations, props, drag and more.',
}
// Tags shown on package cards
export const PACKAGE_TAGS: Record<number, string[]> = {
5301828: ['msk_core', 'pma-voice'],
}
// News popup — shown on every full page load
export const NEWS_POPUP = {
enabled: true,
title: 'Discord Ticket Bot',
text: 'Get your API Key now and create a ticket system for your community!',
button: { label: 'Get API Key', href: '/verify' },
secondButton: { label: 'Dashboard', href: '/dashboard' },
coupon: null, // or e.g. 'NEWSHOP20' — renders a copyable coupon field
}
// Site metadata
export const SITE_CONFIG = {
name: 'MSK Scripts Shop',
tagline: 'High quality FiveM resources & Discord bots for your server',
discord: 'https://discord.gg/5hHSBRHvJE',
github: 'https://github.com/MSK-Scripts',
docs: 'https://docu.msk-scripts.de',
}Custom packages (non-Tebex) → content/custom-packages.ts
Legal pages → content/legal/*.md — plain Markdown, EN + DE versions
Pushing to main automatically deploys via GitHub Actions (.github/workflows/deploy.yml):
- Checkout + Node.js 20 setup
- Install dependencies (
npm ci) - Build (
npm run buildwithNEXT_PUBLIC_*+TEBEX_PRIVATE_KEYinjected at build time) - Transfer build output + runtime files to server via SCP
(
.next/,public/,content/,package.json,package-lock.json,next.config.js,msk-shop.service) - SSH script on server: stop service → wipe
node_modules→npm ci --omit=dev→ set permissions (chown musiker15:musiker15,chmod u+w .next/) →systemctl daemon-reload→systemctl restart msk-shop
Authentication: The workflow uses an SSH private key (
SSH_PRIVATE_KEY), not a password. Server-side secrets (DB, OAuth, webhook) live exclusively in/opt/msk-shop/.env.localand are not managed by the workflow.
| Secret | Value |
|---|---|
FTP_SERVER |
Server IP or hostname |
FTP_USERNAME |
SSH username |
FTP_PORT |
SSH port (e.g. 22) |
SSH_PRIVATE_KEY |
Private key for SSH authentication |
NEXT_PUBLIC_TEBEX_PUBLIC_TOKEN |
Tebex public token |
NEXT_PUBLIC_TEBEX_PROJECT_ID |
Tebex project ID |
NEXT_PUBLIC_BASE_URL |
https://www.msk-scripts.de |
TEBEX_PRIVATE_KEY |
Tebex private key |
Additional workflows: codeql.yml (code scanning), eslint.yml (lint), release.yml,
dependency-review.yml, secret-scan.yml.
- Node.js 20.x
- npm
- MariaDB or MySQL
- Apache2 with
mod_proxy,mod_ssl,mod_rewrite,mod_headers - Let's Encrypt SSL certificate (Certbot)
- Debian / Ubuntu with systemd
- PM2 (only required for hosted bot management)
# 1. Clone
cd /opt
git clone https://github.com/MSK-Scripts/msk-shop.git msk-shop
cd msk-shop
# 2. Environment variables
cp .env.example .env.local
nano .env.local
# 3. Database
mysql -u root -p -e "CREATE DATABASE msk_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p msk_shop < database/schema.sql
# 4. Install & build
npm ci
npm run build
# 5. Permissions (service runs as user "musiker15" on port 3005)
chown -R musiker15:musiker15 /opt/msk-shop
chmod -R u+w /opt/msk-shop/.next
# 6. systemd service
cp msk-shop.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable msk-shop
systemctl start msk-shop
# 7. Apache2
a2enmod proxy proxy_http rewrite ssl headers
# Copy Apache config — see msk-shop.conf and msk-shop_ssl.conf
systemctl reload apache2.env.local (see .env.example for all variables):
# Tebex
NEXT_PUBLIC_TEBEX_PUBLIC_TOKEN=your_public_token
NEXT_PUBLIC_TEBEX_PROJECT_ID=your_project_id
TEBEX_PRIVATE_KEY=your_private_key
NEXT_PUBLIC_BASE_URL=https://www.msk-scripts.de
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=msk_shop
# Session
SESSION_SECRET=<openssl rand -hex 32>
# Discord OAuth (verify flow — scopes: identify, guilds)
DISCORD_VERIFY_CLIENT_ID=your_client_id
DISCORD_VERIFY_CLIENT_SECRET=your_client_secret
# GitHub OAuth
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
# GitHub Sponsors webhook
GITHUB_SPONSORS_WEBHOOK_SECRET=your_webhook_secret
# Transcripts (served by Apache under /transcripts)
TRANSCRIPT_BASE_PATH=/var/www/html/transcripts
# DNS validation & SSL
SERVER_PUBLIC_IP=your.server.ip
ADMIN_EMAIL=info@msk-scripts.de
# Hosted bot management (is_hosted customers)
# Each guild has its own subfolder: {BOT_CONFIG_BASE_PATH}/{guild_id}/
BOT_CONFIG_BASE_PATH=/opt/customer_ticketbots
# Public stats — comma-separated API keys to exclude from /stats
STATS_IGNORED_API_KEYS=key1,key2,key3
⚠️ Never commit.env.local— it is listed in.gitignore.
cd /opt/msk-shop
git pull
npm ci
npm run build
chown -R musiker15:musiker15 /opt/msk-shop
chmod -R u+w /opt/msk-shop/.next
systemctl restart msk-shop# Next.js service logs
journalctl -u msk-shop -f
# Apache error log
tail -f /var/log/apache2/msk-shop-error.log
# Restart
systemctl restart msk-shop
systemctl reload apache2
# Fix permission errors (EACCES on .next/)
chown -R musiker15:musiker15 /opt/msk-shop
chmod -R u+w /opt/msk-shop/.next
systemctl restart msk-shop
# Test database connection
mysql -u your_db_user -p msk_shop -e "SHOW TABLES;"
# Hosted bot PM2 status
systemctl status pm2-musiker15
sudo -u musiker15 pm2 list- Private key (
TEBEX_PRIVATE_KEY) is never exposed to the client — all mutations go through Next.js API routes - Session cookies are signed with
SESSION_SECRET(HMAC-SHA256) and areHttpOnly+Secure - Rate limiting on basket creation and API key endpoints — in-memory per IP (
lib/rateLimit.ts), plus database-side per-API-key limiting inticketbot_rate_limits - Path traversal protection on markdown file reads (allowlist)
- URL validation — redirect URLs are always constructed server-side from
NEXT_PUBLIC_BASE_URL - CSP / Security Headers in
next.config.js: CSP, HSTS (2 years, preload),X-Content-Type-Options,X-Frame-Options: SAMEORIGIN,Referrer-Policy,Permissions-Policy - Debug route (
/api/debug) returns 404 in production - GitHub Sponsors webhook is verified via HMAC-SHA256 signature
- OAuth flows use a random
statetoken (CSRF protection)
Tailwind palette (tailwind.config.ts) — dark-themed with green accent:
| Token | Value | Token | Value |
|---|---|---|---|
bg |
#1b1b1d |
text |
#e3e3e3 |
surface |
#242526 |
muted |
#8d9096 |
surface2 |
#2a2b2e |
dim |
#5c6370 |
border |
#3d3d3f |
accent |
#5eb131 |
borderlt |
#2e2f31 |
accenthov |
#4e9827 |
danger |
#e05c4b |
discord |
#5865F2 |
Font: Inter (self-hosted, --font-inter).
Utility classes in app/globals.css (@layer components):
msk-btn-primary, msk-btn-ghost, msk-btn-discord, msk-card, msk-input, msk-badge, msk-label, msk-section-title.