Be the vibe. Scan the QR at your table or enter your venue code to vote on the music.
SocialJavaScript · CSSGitHub @BennoKuhn424OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
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 }Read the full 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) |