A production-oriented Discord bot and Roblox server bridge for remotely moderating a Roblox experience.
The project uses Discord slash commands, Discord Components V2, Roblox Open Cloud User Restrictions, Open Cloud Messaging Service, local JSON or PostgreSQL storage, signed server messages, role-based permissions, audit logs, and persistent temporary-ban reconciliation.
Official Discord and Roblox documentation was reviewed on June 15, 2026.
| Capability | Description |
|---|---|
| Permanent bans | Block a Roblox user from the entire experience |
| Temporary bans | Apply a restriction with a controlled expiration |
| Unbans | Remove an active Roblox restriction and update the local case |
| Live kicks | Send a best-effort kick request to active Roblox servers |
| Case management | Inspect, edit, cancel, filter, and paginate moderation cases |
| Player history | Review every locally recorded action for a Roblox user |
| Roblox profiles | Display public account information, avatar, and local ban status |
| System health | Inspect Discord latency, database health, Roblox activity, and request status |
| Audit logging | Send moderation actions and technical errors to separate Discord channels |
The system works even when the target player is offline. Persistent access control is handled by Roblox User Restrictions, while Messaging Service handles best-effort real-time notifications to active game servers.
| Area | Included |
|---|---|
| Runtime | Modern JavaScript ES modules, Node.js, no TypeScript |
| Discord | Slash commands, Components V2, ephemeral confirmations |
| Roblox | Open Cloud User Restrictions and Messaging Service |
| Database | Local JSON by default or PostgreSQL for shared deployments |
| Cases | Sequential case numbers such as RBX-000001 |
| Reliability | Mutation verification, idempotency keys, retries, and timeouts |
| Security | HMAC-SHA256, expiration checks, nonces, and replay protection |
| Permissions | Guild, role, administrator, and protected-user checks |
| Rate control | Per-user and global sensitive-action cooldowns |
| Logging | Separate moderation audit and technical error channels |
| Scheduling | Persistent temporary-ban reconciliation after restarts |
| Quality | Automated tests, ESLint, Prettier, and GitHub Actions CI |
| Deployment | Windows, Linux, and VPS compatible |
Roblox User Restrictions is the source of truth for whether a user may join. The configured database is the bot's operational record and audit history. Messaging Service is not treated as proof that a server received or executed a message.
| Requirement | Recommendation |
|---|---|
| Node.js | Node.js 24 LTS |
| npm | Version 10 or newer |
| Discord | Application, bot, guild, and moderation channels |
| Roblox | Published experience with Studio publishing access |
| Open Cloud | API key with the required experience permissions |
| Storage | Writable disk for JSON, or access to PostgreSQL |
PowerShell:
Copy-Item .env.example .env
npm installLinux or macOS:
cp .env.example .env
npm installOpen .env and fill every required value. Never commit this file.
npm run validate-startup
npm test
npm run lint
npm run format:checknpm run deploy-commandsCommands are deployed to DISCORD_GUILD_ID, which makes development updates appear quickly in the configured guild.
npm startDevelopment mode with automatic restarts:
npm run dev- Open the Discord Developer Portal.
- Select New Application.
- Enter a name and create the application.
- Open General Information and copy the Application ID.
- Set
DISCORD_CLIENT_IDto that value.
- Open the Bot page.
- Create the bot if Discord has not created one automatically.
- Reset and copy the bot token.
- Set
DISCORD_TOKENin.env. - Do not enable privileged intents. This project only requests the
Guildsintent.
- Open OAuth2 > URL Generator.
- Select the
botscope. - Select the
applications.commandsscope. - Grant:
- View Channels;
- Send Messages;
- Read Message History.
- Open the generated URL and invite the bot.
Enable Discord Developer Mode under User Settings > Advanced.
Copy:
- the guild ID into
DISCORD_GUILD_ID; - the moderation log channel ID into
DISCORD_LOG_CHANNEL_ID; - the technical error channel ID into
DISCORD_ERROR_CHANNEL_ID; - every moderation role ID into the appropriate role list.
Role lists accept comma-separated values:
DISCORD_BAN_ROLE_IDS=111111111111111111,222222222222222222| Role group | Allowed actions |
|---|---|
| Admin roles | Every command |
| Discord administrators | Every command when ALLOW_DISCORD_ADMINISTRATORS=true |
| Moderator roles | Kick and read-only commands |
| Ban roles | Ban and edit active bans |
| Kick roles | Kick |
| Unban roles | Unban and cancel active bans |
| View-case roles | Case, history, sanctions, profile, status, and ping |
Permissions are enforced in application code. Hiding a command in Discord is not considered a security boundary.
- Open Creator Dashboard > Credentials.
- Create an API key.
- Restrict it to the target experience.
- Restrict source IP addresses to your VPS when possible.
- Add only the permissions required by this project.
Required permissions:
| Product or scope | Permission |
|---|---|
| User Restrictions | universe.user-restriction:read |
| User Restrictions | universe.user-restriction:write |
| Messaging product | messaging-service |
| Messaging operation | universe-messaging-service:publish |
Set the key as ROBLOX_OPEN_CLOUD_API_KEY.
In Creator Dashboard:
- Open Creations.
- Find the experience.
- Open its menu.
- Select Copy Universe ID.
- Set
ROBLOX_UNIVERSE_ID.
Open the place page or Studio configuration and copy the numeric place ID. Set ROBLOX_PLACE_ID.
The current implementation applies universe-level restrictions. The Place ID is retained for diagnostics and future place-scoped support.
Follow roblox/INSTALLATION.md.
The API key must never be placed in Roblox Studio. Only the HMAC command secret is shared with the server bridge.
| Variable | Required | Purpose |
|---|---|---|
NODE_ENV |
Yes | development, test, or production |
LOG_LEVEL |
Yes | Structured log level |
| Variable | Default | Purpose |
|---|---|---|
DATABASE_PROVIDER |
json |
Select json or postgres |
DATABASE_PATH |
./data/database.json |
Local JSON file path |
DATABASE_URL |
Empty | PostgreSQL connection URL |
DATABASE_SCHEMA |
roblox_discord_admin |
PostgreSQL schema used by this installation |
DATABASE_SSL_MODE |
require |
PostgreSQL TLS mode: require or disable |
DATABASE_POOL_MAX |
10 |
Maximum PostgreSQL connection pool size |
| Mode | Best for | Shared between machines | Extra service |
|---|---|---|---|
| JSON | One bot process on one machine | No | No |
| PostgreSQL | Hosted or multi-instance systems | Yes | Yes |
JSON is the default. The bot creates data/database.json automatically on first startup.
DATABASE_PROVIDER=json
DATABASE_PATH=./data/database.jsonOnly one running bot process should write to a JSON database file. The in-process write queue prevents concurrent command writes from corrupting the file, but JSON is not a network database and must not be shared through a synchronized folder.
Use PostgreSQL when several bot instances need the same data, when the filesystem is temporary, or when the database is hosted separately.
DATABASE_PROVIDER=postgres
DATABASE_URL=postgresql://user:password@host:5432/database
DATABASE_SCHEMA=roblox_discord_admin
DATABASE_SSL_MODE=require
DATABASE_POOL_MAX=10The bot creates its schema, tables, indexes, and migrations automatically. Hosted providers such as Neon, Supabase, Railway, or any compatible PostgreSQL server can be used.
| Deployment goal | Schema configuration |
|---|---|
| Multiple instances sharing the same data | Use the same DATABASE_URL and DATABASE_SCHEMA |
| Separate bots on one PostgreSQL database | Give each installation a unique DATABASE_SCHEMA |
| Local PostgreSQL without TLS | Set DATABASE_SSL_MODE=disable only on a trusted network |
The project does not automatically import old SQLite data. Back up and migrate legacy data before replacing an existing installation.
| Variable | Required | Purpose |
|---|---|---|
DISCORD_TOKEN |
Yes | Bot token |
DISCORD_CLIENT_ID |
Yes | Discord application ID |
DISCORD_GUILD_ID |
Yes | Guild used for command deployment |
DISCORD_LOG_CHANNEL_ID |
Yes | Moderation audit channel |
DISCORD_ERROR_CHANNEL_ID |
Yes | Technical error channel |
ALLOW_DISCORD_ADMINISTRATORS |
Yes | Automatically trust Discord administrators |
DISCORD_ADMIN_ROLE_IDS |
No | Full-access roles |
DISCORD_MODERATOR_ROLE_IDS |
No | General moderator roles |
DISCORD_BAN_ROLE_IDS |
No | Ban roles |
DISCORD_KICK_ROLE_IDS |
No | Kick roles |
DISCORD_UNBAN_ROLE_IDS |
No | Unban roles |
DISCORD_VIEW_CASES_ROLE_IDS |
No | Read-only moderation roles |
| Variable | Required | Purpose |
|---|---|---|
ROBLOX_OPEN_CLOUD_API_KEY |
Yes | Open Cloud authentication |
ROBLOX_UNIVERSE_ID |
Yes | Target experience universe |
ROBLOX_PLACE_ID |
Yes | Target place metadata |
ROBLOX_MESSAGING_TOPIC |
Yes | Cross-server topic |
ROBLOX_COMMAND_SECRET |
Yes | HMAC secret, minimum 32 characters |
| Variable | Default | Purpose |
|---|---|---|
DEFAULT_BAN_REASON |
No reason provided |
Fallback visible ban reason |
DEFAULT_KICK_REASON |
No reason provided |
Fallback kick reason |
MAX_TEMP_BAN_DURATION |
365d |
Maximum temporary ban |
COMMAND_COOLDOWN_SECONDS |
3 |
Per-user command cooldown |
GLOBAL_SENSITIVE_COOLDOWN_MS |
750 |
Global sensitive-action cooldown |
CONFIRM_DANGEROUS_ACTIONS |
true |
Dangerous action confirmation setting |
CONFIRMATION_TIMEOUT_SECONDS |
60 |
Confirmation lifetime |
SCHEDULER_INTERVAL_SECONDS |
60 |
Expiration reconciliation interval |
ROBLOX_HTTP_TIMEOUT_MS |
10000 |
Roblox HTTP timeout |
ROBLOX_HTTP_MAX_RETRIES |
3 |
Maximum safe retries |
PROTECTED_ROBLOX_USER_IDS |
Empty | User IDs blocked from destructive actions |
PowerShell:
[Convert]::ToHexString(
[Security.Cryptography.RandomNumberGenerator]::GetBytes(32)
).ToLower()Linux or macOS:
openssl rand -hex 32Use the exact same value for:
ROBLOX_COMMAND_SECRETin.env;CommandSecretinroblox/AdminConfig.lua.
| Command | Purpose |
|---|---|
/ban user duration reason internal-note alt-detection private |
Create a temporary or permanent ban |
/unban user reason case |
Remove an active restriction |
/kick user reason private |
Publish a best-effort live kick |
/case number |
Display a moderation case |
/history user |
Display a player's case history |
/sanctions status type moderator player |
Filter and paginate cases |
/roblox-user user |
Display a Roblox public profile and local status |
/sanction-edit case duration reason internal-note alt-detection |
Edit an active ban |
/sanction-cancel case reason |
Cancel an active ban |
/status request-id |
Display system or request health |
/ping |
Display Discord latency |
| Input | Meaning |
|---|---|
30s |
30 seconds |
10m |
10 minutes |
2h |
2 hours |
7d |
7 days |
2w |
2 weeks |
1mo |
30 days |
1y |
365 days |
permanent |
Permanent ban |
perm |
Permanent ban |
After changing command names, descriptions, or options, run:
npm run deploy-commands- Resolve the input to a Roblox user ID.
- Create a pending database case.
- Apply the Roblox User Restriction with an idempotency key.
- Read the restriction back from Roblox.
- Mark the case active only after verification.
- Publish a signed
ban-notificationmessage. - Active servers kick the player if present.
- Send a Discord audit record.
If Messaging Service fails after Roblox accepts the restriction, the ban remains active and the case records a warning.
- Resolve the target account and active local case.
- Read the current Roblox restriction.
- Send
active=falsewith an idempotency key. - Read the restriction again.
- Revoke the local case only after Roblox confirms it is inactive.
- Publish an
unban-notification.
An already inactive restriction is handled idempotently and still receives an auditable unban case.
A kick publishes a signed message to active game servers. Each server checks whether it owns the target player before calling Player:Kick().
Messaging Service delivery is best effort. A successful HTTP response confirms publication, not that a server received the message or kicked the player.
Roblox enforces the configured restriction duration. The scheduler also scans expired active cases after startup and on a fixed interval. If Roblox still reports the restriction as active, the bot explicitly removes it before marking the local case expired.
- Secrets are loaded from
.env. .env, local JSON database files, backups, and logs are ignored by Git.- Configuration is validated before startup.
- Roblox requests have timeouts and bounded retries.
429responses honorRetry-After.- Mutations use idempotency keys.
- User IDs are the primary identity, never display names.
- PostgreSQL uses parameterized queries.
- Confirmations use opaque, expiring, single-use tokens.
- Button ownership is verified.
- Guild and role permissions are enforced server-side.
- Technical errors do not expose secrets publicly.
- Logs redact token, key, secret, authorization, and cookie fields.
- The Roblox bridge exposes no client moderation RemoteEvent.
- Game messages use HMAC-SHA256, expiration, nonces, and replay tracking.
The HMAC secret is present in published server code. It is not replicated to ordinary clients, but it is less protected than a secret stored only on your external server. Rotate it after team changes or suspected source access.
| Service | Method | Endpoint |
|---|---|---|
| User Restrictions | GET |
https://apis.roblox.com/cloud/v2/universes/{universe_id}/user-restrictions/{user_id} |
| User Restrictions | PATCH |
https://apis.roblox.com/cloud/v2/universes/{universe_id}/user-restrictions/{user_id} |
| Messaging Service | POST |
https://apis.roblox.com/cloud/v2/universes/{universe}:publishMessage |
| Username resolution | POST |
https://users.roblox.com/v1/usernames/users |
| User search | GET |
https://users.roblox.com/v1/users/search |
| User profile | GET |
https://users.roblox.com/v1/users/{userId} |
| Avatar thumbnail | GET |
https://thumbnails.roblox.com/v1/users/avatar-headshot |
User Restrictions is currently documented as Beta. Temporary restriction durations use protobuf duration strings such as 604800s. Permanent restrictions omit the duration field.
| Operation | Documented limit |
|---|---|
| Get restriction | 50 requests/second per universe |
| List restrictions | 50 requests/second |
| Update restrictions | 10 requests/second |
| Update the same user | 2 requests/minute |
| General API key owner | 150 requests/second |
| Resource | Documented limit |
|---|---|
| Topic length | 80 characters |
| Message size | 1,024 characters |
| Messages sent per game server | 600 + 240 * player count per minute |
| Messages received per topic | 40 + 80 * server count per minute |
| Messages received by the experience | 400 + 200 * server count per minute |
| Subscriptions per server | 20 + 8 * player count |
| Subscription requests | 240 per minute per server |
Limits may change. Recheck official documentation before high-volume deployment.
| Command | Purpose |
|---|---|
npm start |
Start the production bot |
npm run dev |
Start with automatic restarts |
npm run deploy-commands |
Deploy guild slash commands |
npm test |
Run the automated test suite |
npm run lint |
Run ESLint |
npm run format |
Format supported files |
npm run format:check |
Verify formatting without changing files |
npm run validate-startup |
Load configuration, database, services, and commands offline |
- Run every local validation command.
- Deploy commands to a test Discord guild.
- Use a private published Roblox experience.
- Test a kick on an online test account.
- Test a 2-minute temporary ban.
- Confirm the test account cannot rejoin.
- Confirm the account is allowed back after expiration.
- Test a permanent ban and unban.
- Test permission denial with a non-moderator Discord account.
- Verify internal notes do not appear to read-only roles.
- Verify audit logs and technical error logs use separate channels.
- Restart the bot during a temporary ban and verify reconciliation.
Never test destructive actions on production staff or owner accounts. Add protected Roblox IDs to PROTECTED_ROBLOX_USER_IDS.
Run the bot through a service manager or a dedicated process supervisor. At minimum:
npm ci --omit=dev
npm run deploy-commands
npm startStore .env outside any public web directory and restrict filesystem access to the service account.
Install production dependencies:
npm ci --omit=devExample service:
[Unit]
Description=Roblox Discord Admin
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/roblox-discord-admin
ExecStart=/usr/bin/node src/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
User=roblox-admin
Group=roblox-admin
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl enable --now roblox-discord-admin
sudo systemctl status roblox-discord-adminThe service account must be able to write to data/ and logs/.
| Provider | Recommended backup method |
|---|---|
| JSON | Stop the bot, copy data/database.json, then restart the bot |
| PostgreSQL | Use the hosting provider's backups or pg_dump with the configured database URL |
Example JSON backup:
Copy-Item data/database.json data/database.json.backupTest restoration regularly. A backup that has never been restored is not a verified backup.
- Create a replacement key with the same minimal permissions.
- Update
.env. - Restart the bot.
- Test
/statusand one safe request. - Revoke the old key.
- Generate a new secret.
- Update
.env. - Update
AdminConfig.lua. - Publish the Roblox experience.
- Restart the bot.
- Restart private test servers and test a kick.
HMAC rotation is not atomic across already-running Roblox servers. Plan it during a quiet maintenance window.
| Problem | Resolution |
|---|---|
Roblox returns 401 |
Verify ROBLOX_OPEN_CLOUD_API_KEY; the key is missing or invalid |
Roblox returns 403 |
Recheck the experience, source IP restrictions, and required permissions |
Roblox returns 404 |
Verify the Universe ID, user ID, resource scope, and API key access |
Roblox returns 429 |
Wait for Retry-After; the bot retries only bounded, safe operations |
| Commands do not appear | Run npm run deploy-commands and verify the client ID, guild ID, and applications.commands scope |
| Discord logs are missing | Verify both channel IDs and the View Channels, Send Messages, and Read Message History permissions |
| Kick publishes but the player stays online | Check whether the player is online, the topic matches, the bridge subscribed, and Messaging Service limits were not reached |
| Bans behave differently in Studio | Publish the experience and test in a private production server |
Read CONTRIBUTING.md before opening a pull request.
Security issues should follow SECURITY.md, not a public issue.
Released under the MIT License.
- Discord Components Overview
- Discord Component Reference
- Discord Application Commands
- Discord Interaction Responses
- discord.js documentation
- Roblox Open Cloud API reference
- Roblox Bans and Blocks
- Roblox Messaging usage guide
- Roblox API keys
- Roblox Players
- Roblox MessagingService
- Roblox HttpService
Created and maintained by Pingus.