尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Speeldit – Music Voting Web App (MVP)
Full-stack web app for music voting in bars/restaurants using QR codes. Customers scan to vote and request songs; venue owners manage the queue and settings.
• Frontend: React (Vite), Tailwind CSS, React Router, Axios, date-fns, qrcode.react
• Backend: Node.js, Express, SQLite (better-sqlite3, WAL mode) on a persistent disk, JWT auth, CORS
• Deploy: Frontend → Vercel/Netlify; Backend → Render/Railway
Server runs at http://localhost:3000.
Health checks (for uptime monitors / load balancers):
• GET /health
• GET /api/health
JSON: { ok, service, ts }檢視完整 README ↗Full-stack web app for music voting in bars/restaurants using QR codes. Customers scan to vote and request songs; venue owners manage the queue and settings.
speeldit/
├── client/ # React frontend (Vite)
├── e2e/ # Playwright end-to-end specs
├── server/
│ ├── app.js # Express app factory (rate limits, routes, logging) — imported by tests
│ ├── instrument.js # Optional Sentry init (only if SENTRY_DSN)
│ ├── server.js # HTTP server, Socket.IO, queue auto-advance interval
│ ├── routes/ # queue.js mounts queueVote, queuePayment, queueAutofill helpers
│ ├── utils/yoco.js # Shared Yoco checkout verify + webhook HMAC
│ └── data/ # SQLite database file (speeldit.db) + nightly backups
├── playwright.config.js
└── README.md
cd server
npm install
npm start
Server runs at http://localhost:3000.
Health checks (for uptime monitors / load balancers):
GET /healthGET /api/health{ ok, service, ts }Production behaviour:
trust proxy – Set NODE_ENV=production and, if the API sits behind a reverse proxy (Render, Railway, nginx), set TRUST_PROXY_HOPS (default 1) so client IP and rate limits are correct.
Structured request logs – Each finished request logs one JSON line to stdout: method, path, status, ms (health routes are skipped to reduce noise). Point your host’s log drain at this stream.
CORS policy – In production (NODE_ENV=production), the server requires an explicit origin allowlist. Set CORS_ORIGINS (comma-separated) and/or PUBLIC_URL so the server knows which frontends may call the API and connect via Socket.IO. Values are trimmed and trailing slashes are stripped; whitespace-only values are treated as empty. If neither variable resolves to at least one real origin, the server refuses to start with a clear error. In development, localhost:5173 is allowed automatically.
To verify your production config locally:
NODE_ENV=production CORS_ORIGINS= PUBLIC_URL= node -e "require('./app')"
# → throws FATAL: No CORS origins configured for production …
Security headers – helmet is applied to all responses, adding X-Content-Type-Options, Strict-Transport-Security, X-Frame-Options, and other hardening headers. Cross-origin resource/embedder policies are relaxed so the SPA + Socket.IO clients on a different origin keep working.
Graceful shutdown – The process handles SIGTERM and SIGINT: it stops accepting new connections, closes Socket.IO so clients disconnect cleanly, then exits. A 12-second timeout forces exit if shutdown stalls. This matters on PaaS hosts (Render, Railway, Kubernetes) that send SIGTERM before killing the container — without a handler the process would be hard-killed mid-request.
Rate limits – express-rate-limit on the API:
/api/auth/*: RATE_LIMIT_AUTH_MAX attempts per 15 minutes per IP (default 40)./api/* routes (except auth paths): RATE_LIMIT_API_MAX requests per minute per IP (default 500). Raise this if many customer devices share one public IP (e.g. venue Wi‑Fi NAT).Optional env (full list):
| Variable | Purpose |
|---|---|
PORT | Listen port (default 3000) |
NODE_ENV | Set to production for trust proxy + stricter defaults |
CORS_ORIGINS | Comma-separated allowed origins for CORS (e.g. https://yourapp.vercel.app). Required in production (along with or instead of PUBLIC_URL). Values are trimmed; whitespace-only is rejected. |
JWT_SECRET | Required in production – signing venue JWTs |
TRUST_PROXY_HOPS | Trust X-Forwarded-For hops (default 1) |
RATE_LIMIT_AUTH_MAX | Auth route cap per 15 min / IP (default 40) |
RATE_LIMIT_API_MAX | General API cap per minute / IP (default 500) |
APPLE_MUSIC_DEVELOPER_TOKEN | Pre-generated Apple Music token (optional if using key file) |
PUBLIC_URL | Frontend URL for redirects (e.g. https://yourapp.vercel.app) |
PATRON_PAYMENT_PROVIDER | yoco (default, South Africa) or stripe (international) — which checkout vendor handles pay-to-play |
PAYMENT_CURRENCY | Currency for patron song-request checkouts (default ZAR; Stripe supports USD, EUR, GBP, …) |
YOCO_SECRET_KEY | Yoco API secret for pay-to-play (Bearer token to verify checkouts) |
YOCO_WEBHOOK_SECRET | Optional whsec_… signing secret from Yoco — when set, incoming webhooks must pass HMAC verification (recommended in production) |
STRIPE_SECRET_KEY | Stripe API secret (sk_…) — required for either Stripe provider |
STRIPE_WEBHOOK_SECRET | Signing secret (whsec_…) of the Stripe endpoint pointing at /api/webhooks/payment |
cd client
npm install
npm run dev
App runs at http://localhost:5173. The Vite config proxies /api to the backend.
Frontend env:
| Variable | Purpose |
|---|---|
VITE_PUBLIC_URL | Canonical public site URL (optional; falls back to window.location.origin) |
VITE_API_URL | Production: full API base including /api, e.g. https://api.yoursite.com/api |
VITE_SENTRY_DSN | Optional – browser Sentry DSN (omit in dev to disable) |
VITE_SENTRY_ENVIRONMENT | Optional – Sentry environment label (defaults to Vite MODE) |
VITE_SENTRY_TRACES_SAMPLE_RATE | Optional – 0–1; if 0, tracing integration is disabled |
# Server (Jest)
cd server && npm test
# Client (Vitest + Testing Library)
cd client && npm test
# From repo root (unit tests: server + client)
npm test
# End-to-end (Playwright – starts API + Vite, needs Chromium once: npx playwright install chromium)
npm run test:e2e
GitHub Actions runs server tests, client tests, client build, and Playwright E2E (Chromium + system deps) on push/PR to main/master.
Sentry (optional): With SENTRY_DSN / VITE_SENTRY_DSN set, the API registers setupExpressErrorHandler and the SPA initializes @sentry/react with an error boundary. Without DSNs, Sentry code paths are skipped.
Still out of scope for the repo: horizontal scale (the SQLite + per-venue async mutex pattern assumes a single Node instance), full React Query data layer, and deeper E2E (register → login → playlist) — add those as separate milestones if you need them.
The platform owner (you) signs in on the same Venue login page (/venue/login). If credentials match server env, you are sent to /owner with a full-screen dashboard: total and monthly revenue, your platform cut vs venue share (from VENUE_EARNINGS_PERCENT), per-venue monthly breakdown, all registered venues, recent payments, analytics event volume (24h), and live Socket.IO connection count (approximate “active” clients).
Required server environment (never commit real passwords to git):
| Variable | Purpose |
|---|---|
OWNER_EMAIL | Your login email (single platform owner) |
OWNER_PASSWORD_HASH | Bcrypt hash of your password — not plain text |
If OWNER_EMAIL is set on the API but OWNER_PASSWORD_HASH is missing, login with that email returns 503 (not a venue login). If both are set, that email never logs in as a venue—only the owner dashboard—so an old venue row with the same email cannot shadow owner login.
Generate the hash locally:
cd server
npm run hash-owner-password -- "your-password-here"
Paste the printed hash into OWNER_PASSWORD_HASH on your host (e.g. Render). Restart the API.
Registration is blocked for the same email as OWNER_EMAIL so it stays reserved for you.
/v/YOUR_CODEPOST /api/auth/register, POST /api/auth/loginGET /api/queue/:venueCode, POST /api/queue/:venueCode/request, POST /api/queue/:venueCode/create-payment, GET /api/queue/:venueCode/request-status, POST /api/queue/:venueCode/vote, POST /api/queue/:venueCode/skip, DELETE /api/queue/:venueCode/song/:songIdGET /api/token – MusicKit JWT (no auth)GET /api/search?q=...&venueCode=... (venueCode optional)GET /api/music/search?q=...&venueCode=... (legacy)GET /api/venue/:venueCode, PUT /api/venue/:venueCode/settings, GET /api/venue/:venueCode/earnings (auth required)GET /api/admin/venue-earnings?year=2025&month=2 (requires X-Admin-Key: <ADMIN_SECRET> header)GET /api/owner/overview (requires Authorization: Bearer <token> from owner login — JWT with role: 'owner')Live storage is SQLite at DATA_DIR/speeldit.db (WAL mode, single file). On
Render the disk is mounted at /var/data. The legacy filename votebeats.db
is auto-renamed to speeldit.db on first boot. Schema is created on demand
by server/utils/sqlite.js; key tables:
venues – venue info, owner credentials, settings JSON, playlists JSONqueues – now playing + upcoming songs per venue (one row per song)votes – per-venue / per-song / per-device vote statepayments – completed Yoco patron payments (for venue earnings + payouts)pending_payments – checkout IDs awaiting webhook confirmation (encrypted at rest with PAYMENT_ENCRYPTION_KEY)subscriptions – Paystack venue subscription state per venuepayouts – monthly venue payout records (status: pending / paid / failed)analytics, audit_log, auth_tokens, request_throttles, player_volumeSongs auto-advance when their duration ends — the server ticks every 1s for active queues and runs a slower 30s sweep across all venues to start scheduled-playlist slots even when the queue has been fully empty. Next song is chosen by highest votes.
cd client && npm run buildclient folder (or connect Git and set root to client)VITE_API_URL=https://your-backend.onrender.com/apiservernode server.jsJWT_SECRET, PUBLIC_URL (frontend URL), optionally APPLE_MUSIC_DEVELOPER_TOKEN, YOCO_SECRET_KEYThe server stores live data in DATA_DIR/speeldit.db (SQLite). Two scripts handle hot backup + restore:
# Hot backup — safe while the server is running
node server/scripts/backup-db.js
# List + restore — STOP THE SERVER first
node server/scripts/restore-db.js # lists available backups
node server/scripts/restore-db.js --latest # restores newest backup
node server/scripts/restore-db.js speeldit-2026-...db
Backups land in DATA_DIR/backups/ and the script keeps the 14 most recent (override with BACKUP_RETENTION=N).
Recommended cron (Render persistent disk, 03:00 UTC nightly):
0 3 * * * cd /opt/render/project/src/server && node scripts/backup-db.js
Full restore drill and operational notes: see BACKUPS.md.
For real search and full playback:
server/.env:
APPLE_TEAM_ID=[YOUR_TEAM_ID]
APPLE_KEY_ID=[YOUR_KEY_ID]
APPLE_MUSIC_KEY_PATH=[PATH_TO_P8_FILE]
Example: APPLE_MUSIC_KEY_PATH=./AuthKey_XXXXXXXX.p8 (relative to server folder)/api/token (ES256, 180-day expiry)/venue/player/YOUR_VENUE_CODE on the device that plays music, click "Authorize Apple Music" once, then leave it open. It polls the queue every 5s and auto-plays requested songs.Legacy: You can still set APPLE_MUSIC_DEVELOPER_TOKEN if you prefer a pre-generated token. Without either, mock catalog is used for search (playback requires real credentials).
Venue owners can enable “Require payment to suggest a song” in Settings. Customers then pay (R5–R50 equivalent) via a hosted checkout before their request is added to the queue. Two vendors are supported — pick one with PATRON_PAYMENT_PROVIDER:
Yoco (South Africa, default):
YOCO_SECRET_KEY (use sk_test_... for testing) and PUBLIC_URL on the server.https://your-backend.com/api/webhooks/yoco in the Yoco dashboard (see webhooks guide).Stripe (international):
PATRON_PAYMENT_PROVIDER=stripe, STRIPE_SECRET_KEY, and optionally PAYMENT_CURRENCY (e.g. USD).https://your-backend.com/api/webhooks/payment with events checkout.session.completed and checkout.session.async_payment_succeeded; paste its signing secret into STRIPE_WEBHOOK_SECRET.Monthly venue billing with a free trial. Pick a vendor with SUBSCRIPTION_PROVIDER:
PayFast (South Africa, recommended): set SUBSCRIPTION_PROVIDER=payfast, PAYFAST_MERCHANT_ID, PAYFAST_MERCHANT_KEY, PAYFAST_PASSPHRASE (must match the passphrase set on the PayFast account) and PUBLIC_API_URL (public https base of the API — PayFast posts its ITN webhook to {PUBLIC_API_URL}/api/webhooks/subscription, no dashboard webhook config needed). The free trial works via a 0.00 card-tokenization checkout; the first real charge lands on the trial-end date. ITNs are verified four ways (MD5 signature, PayFast source IP, server-to-server /validate postback, amount match) before anything activates. PAYFAST_SANDBOX=true targets sandbox.payfast.co.za for testing — never set it in production. PayFast has no hosted "manage subscription" page; venues cancel in-app (server-side cancel goes through PayFast's API).
Paystack (legacy default): set PAYSTACK_SECRET_KEY, PAYSTACK_PLAN_CODE (R599/month plan), and PAYSTACK_WEBHOOK_SECRET; webhook URL is https://your-backend.com/api/webhooks/paystack.
Stripe (international):
SUBSCRIPTION_PROVIDER=stripe, STRIPE_SECRET_KEY, and STRIPE_PRICE_ID.https://your-backend.com/api/webhooks/subscription with events customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.paid, and invoice.payment_failed; paste its signing secret into STRIPE_SUBSCRIPTION_WEBHOOK_SECRET.MIT
STRIPE_PRICE_IDStripe recurring Price id (price_…) for venue subscriptions |
STRIPE_SUBSCRIPTION_WEBHOOK_SECRET | Signing secret of the Stripe endpoint pointing at /api/webhooks/subscription (falls back to STRIPE_WEBHOOK_SECRET) |
SUBSCRIPTION_PROVIDER | paystack (default), payfast (South Africa, recommended) or stripe (international) — venue subscription billing vendor |
PAYFAST_MERCHANT_ID / PAYFAST_MERCHANT_KEY / PAYFAST_PASSPHRASE | PayFast merchant credentials (passphrase must match the account setting) |
PAYFAST_SANDBOX | true targets the PayFast sandbox and relaxes the ITN source-IP check — never in production |
PUBLIC_API_URL | Public https base of this API; PayFast ITNs are posted to {PUBLIC_API_URL}/api/webhooks/subscription |
SUBSCRIPTION_GRACE_DAYS | Days past the paid-through date before an active sub stops counting as paid (default 5) |
VENUE_EARNINGS_PERCENT | Venue revenue share % of Yoco song-request payments (default 70) |
PAYSTACK_SECRET_KEY | Paystack secret key (sk_test_... or sk_live_...) for venue subscriptions |
PAYSTACK_PUBLIC_KEY | Paystack public key, used client-side in the inline popup |
PAYSTACK_PLAN_CODE | Plan code from Paystack dashboard (R599 monthly) |
PAYSTACK_WEBHOOK_SECRET | Webhook signing secret for verifying Paystack webhook events |
PAYSTACK_SUBSCRIPTION_AMOUNT_ZAR | Monthly subscription price in rand (default 599) |
PAYSTACK_TRIAL_DAYS | Free-trial length in days (default 14) |
ADMIN_SECRET | Admin API key (header X-Admin-Key) |
RESEND_API_KEY | Resend key for transactional email (verification, password reset, billing notices). Without it, no email is sent and new accounts are auto-verified at registration — set it in production so email verification is actually enforced. |
RESEND_FROM_EMAIL | From-address for transactional email (default [email protected]) |
APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_MUSIC_KEY_PATH | MusicKit token generation (see below) |
SENTRY_DSN | Optional – Sentry DSN for API error reporting |
SENTRY_ENVIRONMENT | Optional – label in Sentry (defaults to NODE_ENV) |
SENTRY_TRACES_SAMPLE_RATE | Optional – 0–1 performance trace sampling (default 0 = off) |