Notes
Operational notes, security guidance, and design decisions.
Security
Never Commit Secrets
The following files contain sensitive data and must never be committed:
| File | Contains |
|---|---|
config.yml | Bot token, API keys, database credentials |
.env | Environment variable overrides |
patreon_cache.json | Cached Patreon pledge data |
mewwme.database.json | Full database contents (JSON driver) |
All of these are listed in .gitignore.
Environment Variable Strategy
The config.yml supports environment variable interpolation using ${VAR} syntax. This allows you to keep secrets in .env while keeping the config structure in version control:
# config.yml (safe to commit as template)
bot:
TOKEN: ${TOKEN}
features:
RestAPI:
SessionSecret: ${SESSION_SECRET}
DiscordClientId: ${DISCORD_CLIENT_ID}
DiscordClientSecret: ${DISCORD_CLIENT_SECRET}# .env (never commit)
TOKEN=your_actual_token
SESSION_SECRET=your_64_char_hex
DISCORD_CLIENT_ID=your_client_id
DISCORD_CLIENT_SECRET=your_client_secretSession Security
| Setting | Development | Production |
|---|---|---|
Cookie secure | false | true (requires HTTPS) |
Cookie httpOnly | true | true |
Cookie sameSite | lax | none (cross-site) |
Cookie maxAge | 7 days | 7 days |
| Cookie name | mewwme-dash.sid | mewwme-dash.sid |
Production mode is activated by setting NODE_ENV=production.
Generating a Session Secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"Use the output as your SESSION_SECRET value.
API Authentication
The internal API (/v1/*) uses a shared token for authentication:
Authorization: your_secret_api_tokenThis token is configured in features.RestAPI.auth. It is used by:
- The
DashboardPluginproxy (internal calls) - The
BotWsService(WebSocket bridge) - Any external services calling the API directly
IP Whitelist
The features.RestAPI.whitelist array restricts which domains can access the API. When empty, all domains are allowed. In production, set this to your frontend domain:
features:
RestAPI:
whitelist: ["meww.me", "api.meww.me"]Production Deployment Checklist
- Set
NODE_ENV=production- enables secure cookies and trust proxy. - Use HTTPS - place the API behind a reverse proxy (Nginx) with SSL/TLS.
- Use a strong
SESSION_SECRET- at least 64 hex characters. - Set the API whitelist - restrict to your domain(s).
- Use a process manager - PM2 or Docker for auto-restart.
- Configure database security - strong passwords, restricted network access, SSL if supported.
- Enable regular backups - for MySQL, use
MYSQLBACKUPin config; for others, set up external backups. - Rotate tokens periodically - regenerate bot token and API keys if compromised.
- Monitor logs - check
logs/directory for errors. - Set up DNS - configure A records for API and WebServer domains.
Design Decisions
Two Web Servers
The project runs two separate web servers:
| Server | Framework | Port | Reason |
|---|---|---|---|
| REST API | Fastify | 2555 | Performance, plugin system, WebSocket support, sessions |
| WebServer | Express | 2444 | Simple webhook handling, separate from the main API |
This separation allows the webhook server to be independently secured and scaled.
Mewslink vs Upstream Libraries
Mewslink is a custom Lavalink v4 client built specifically for this project rather than using upstream libraries like shoukaku or erela.js. This provides:
- Full control over the player lifecycle
- Custom data store per player
- Tight integration with the bot's event system
- Dashboard-specific features (e.g.,
dashboardEmit) - Music Trivia isolation via the
isMusicTriviaflag
Database Abstraction
The mewwme.quick.db library with QuickDatabasePlus wrapper provides:
- Uniform API across all four database drivers
- In-memory cache with configurable clear interval
- Schema-free - no migrations needed (tables are key-value stores)
- Hot-swappable - change the driver in config without code changes
The trade-off is that complex queries (joins, aggregations) are not supported. All data access is key-value based.
Session Storage
Sessions are stored using SessionLoginService, which wraps QuickDatabasePlus. This means sessions are persisted in the same database as all other bot data, using the configured driver.
Prisma Schema
A prisma/schema.prisma file is included in the project. It maps all database tables to Prisma models for PostgreSQL reference modeling. However, the bot currently uses mewwme.quick.db as its ORM, not Prisma.
The Prisma schema serves as:
- Documentation of the data model
- A reference for future migration to a type-safe ORM
- A tool for visualizing the database structure
Localization
Language files are stored in src/languages/<code>/ using YAML format. Each language directory contains:
commands.music.yaml- music command responsescommands.filter.yaml- filter command responsesevents.player.yaml- player event messagesevents.ticket.yaml- ticket system messagesinteraction.yaml- interaction responsestos.and.privacy.yaml- terms and privacy text
All 22 languages maintain key parity - every key that exists in the English files must exist in all other languages.
The Localization service loads the appropriate language file based on the guild's configured language (bot.LANGUAGE default, overridable per guild via /language).
Broadcast System Internals
The broadcast tip system uses a probability-cooldown mechanism to avoid spamming:
| Parameter | Value | Effect |
|---|---|---|
TRIGGER_PROBABILITY | 0.15 | 15% chance per targeted command |
COOLDOWN_MS | 86,400,000 | 24-hour cooldown after seeing a tip |
After seeing all available tips, the user's history resets and the cycle begins again.
Tips are defined in src/utilities/BroadcastConfig.ts and can include:
- Rich container mode (title, message, footer, thumbnail, buttons)
- Simple text mode
- Interactive command mentions (e.g.,
</play>renders as clickable) - Target-specific commands (only trigger on certain commands)
Timer Limitations
Node.js setTimeout stores delay values as a signed 32-bit integer, limiting the maximum delay to approximately 24.8 days (2,147,483,647 ms). The Music Trivia cleanup task works around this by:
- Calculating the delay to the next 25th of the month.
- Using
setTimeoutfor that specific duration. - Re-scheduling itself after each execution.
This ensures the timer value never exceeds the 32-bit limit.