zxc lobby management tool: Discord economy + inventory + crafting + marketplace + OBS Agent relay.
Pirate ship included. MMR not guaranteed. VNMCR-coded. 🦜
- What is Balkon?
- Ecosystem
- Features
- Discord OAuth2 setup
- Quick start
- Production deploy
- OBS Agent mode
- Project workflow
- Database workflow
- Manual test checklist
- Diploma showcase flow
Balkon is a TypeScript Discord bot built with discord.js, mysql2, slash commands and interactive Discord menus.
It started as a Discord bot diploma project, then evolved into a small ecosystem:
- 🎮 Discord community tools.
- 💰 Economy and balances.
- 🎒 Inventory and item instances.
- 🏪 Market and bot shop.
- ⚒️ Crafting recipes.
- 🎥 OBS Studio control through a standalone OBS Agent.
- 🧙 Streamer-oriented automation.
It was originally created as a thesis project and for a Discord server: join here. But now it’s a publicly available bot with a shared economy across servers
| Project | Role | Status |
|---|---|---|
balkon |
Main Discord bot, database logic, relay server, slash commands, interactive menu. | ✅ active |
balkon-obs-agent |
Standalone desktop app for streamers. Connects local OBS Studio to Balkon relay. | ✅ active |
balkon-website |
Planned web dashboard with Discord login, streamer/session management and admin UI. | 🧪 planned |
The repository now has two runtime applications:
balkon-bot: existing Discord bot process (Discord gateway +discord.jsactions).balkon-api: new REST API process for the future web dashboard.
Also planned for dashboard frontend:
balkon-website: separate Next.js repository (phenibut645/balkon-website).- For local convenience, it can live as nested folder
balkon-website/in this workspace. - Root
balkon.gitignoreexcludesbalkon-website/so website files are not tracked here. - Website uses existing Fastify API for OAuth/session/backend data and does not store Discord OAuth client secret.
Current rollout status: internal/staging only.
Flow:
- Website calls
balkon-apionly. - API uses shared services and database for standard data operations.
- For Discord-specific actions, API writes a command into
bot_commandsqueue. - Bot process (
BotCommandWorker) consumes commands and executes Discord API calls.
This does not create a custom Discord API and does not expose bot tokens to frontend clients.
- Install dependencies:
npm install - Run API in dev mode:
npm run dev:api - Build and run API from
dist:npm run buildnpm run start:api
Default API base path: /api
Implemented first endpoints:
GET /api/healthGET /api/versionGET /api/meGET /api/inventoryGET /api/marketGET /api/botshopGET /api/craft/recipesGET /api/admin/statsPOST /api/guilds/:guildId/members/:memberId/kick(enqueue only; execution happens in bot worker)
Discord OAuth2 Authorization Code Grant is implemented in balkon-api.
Important:
- OAuth2 is implemented in the API, not in the website frontend.
- Website must not store Discord client secret.
- Website login button should redirect user to
GET /api/auth/discord. - Website then calls API routes (for example
/api/me) with credentials included.
Development header auth is intentionally gated and disabled by default:
- Works only when
NODE_ENV !== "prod". - Works only when
API_DEV_AUTH_ENABLED=true. - If
API_DEV_AUTH_ENABLEDis missing, header auth stays disabled. - In
NODE_ENV=prod,x-dev-discord-idandx-dev-rolesare ignored.
Production behavior:
- Public production API requires real OAuth/session auth.
- Without a valid session cookie, protected routes return
401.
- Open Discord Developer Portal.
- Select the existing bot application.
- Go to OAuth2 -> General.
- Copy Client ID into
DISCORD_OAUTH_CLIENT_ID. - Copy Client Secret into
DISCORD_OAUTH_CLIENT_SECRET. - Add redirect URI:
http://localhost:3001/api/auth/discord/callbackfor local.https://your-api-domain.example/api/auth/discord/callbackfor production. - Set website redirect variables:
WEB_APP_URL,WEB_APP_AUTH_SUCCESS_URL,WEB_APP_AUTH_ERROR_URL. - Run migrations:
npm run db:migrate:dev - Start API:
npm run dev:api - Website login button should redirect to:
http://localhost:3001/api/auth/discord
GET /api/auth/discordredirects to Discord authorize URL.GET /api/auth/discord/callbackvalidates state, exchanges code, creates DB session, sets secure httpOnly cookie, and redirects to website.POST /api/auth/logoutrevokes/deletes server-side session and clears cookie.GET /api/meworks with real OAuth session cookie.GET /api/mealso works with dev headers only whenNODE_ENV !== prodandAPI_DEV_AUTH_ENABLED=true.- In production, dev headers do not authenticate.
For local development with explicit opt-in (API_DEV_AUTH_ENABLED=true):
x-dev-discord-id: <discord_user_id>x-dev-roles: bot_admin,bot_contributor,guild_founder
Never use these headers in production.
Discord user/admin
↓
Discord slash command or /botmenu
↓
Balkon main bot
↓
MySQL / OBS Relay / business logic
↓
Balkon OBS Agent on streamer PC
↓
Local OBS Studio WebSocket
- Slash commands through
discord.js. - Interactive
/menu//botmenu. - Role/member command permissions.
- Guild/member/channel/role persistence.
- Item templates.
- Concrete inventory items.
- Rarities and item types.
- Player inventory.
- ODM/LDM balances.
- Player market.
- Bot shop.
- Original owner tracking.
- Admin-managed craft recipes.
- Recipe ingredients.
- Craft result item generation.
- User-facing craft commands and menu flows.
- Streamer registration.
- Primary streamer support.
- Multiple streamers per Discord guild.
- OBS Agent credentials.
- OBS relay WebSocket server.
/botmenu → OBScontrols selected streamer's connected OBS Agent.- Scene listing and scene switching.
- Source visibility control.
- Text input update.
- Media source actions.
npm installPowerShell:
Copy-Item .env.dev.example .env.dev
Copy-Item .env.prod.example .env.prodFill Discord, MySQL and Twitch credentials in the copied files.
npm run buildUse this only when the database is empty:
npm run db:init:devnpm run db:migrate:devRunning it twice should skip already applied migrations.
npm run dev-deploynpm run devnpm run build
npm run db:migrate:prod
npm run prod-deploy
npm run startFor a completely fresh production database, run this once before migrations:
npm run db:init:prodgit pull
npm install
npm run db:migrate:prod
npm run build
npm run prod-deploy
pm2 restart all --update-env
pm2 logsRaw PM2 helpers:
pm2 start ecosystem.config.cjs --only balkon-bot
pm2 restart balkon-bot
pm2 logs balkon-bot
pm2 stop balkon-botRepository scripts:
npm run pm2:start
npm run pm2:restart
npm run pm2:logs
npm run pm2:stopFor production with remote streamers, Balkon uses a standalone OBS Agent desktop app.
The bot runs on a server. OBS Studio runs on the streamer's PC. Direct server → OBS connection is usually impossible because of NAT, routers and local networks.
So the correct flow is:
Discord /botmenu or /obs command
↓
Balkon bot on server
↓
OBS relay WebSocket
↓
Balkon OBS Agent on streamer PC
↓
Local OBS Studio ws://127.0.0.1:4455
-
Install the standalone
balkon-obs-agentdesktop app. -
Enable OBS WebSocket in OBS Studio:
OBS Studio → Tools → WebSocket Server Settings Enable WebSocket server Port: 4455 -
In Discord, generate credentials:
/streamer register nickname:<name> primary:true /streamer agent_pair nickname:<name> -
Put the generated values into the desktop app:
OBS_AGENT_RELAY_URL=wss://venomancer.aleksandermilisenko23.thkit.ee/ OBS_AGENT_ID=<from Discord> OBS_AGENT_TOKEN=<from Discord> OBS_WEBSOCKET_URL=ws://127.0.0.1:4455 OBS_WEBSOCKET_PASSWORD=
-
Click Connect.
-
Click Test OBS.
-
Check status:
/streamer agent_show nickname:<name>
The OBS panel is an OBS Agent control panel.
- It does not use server-side
OBS_WEBSOCKET_URL. - If one streamer is registered, it auto-selects that streamer.
- If multiple streamers are registered, it shows a streamer dropdown.
- The primary streamer is selected by default.
- Legacy direct
Config / Set config / Clear configbuttons are not shown in relay-agent mode.
Normal OBS menu flow:
/botmenu → OBS
→ select streamer
→ resolve streamer's OBS Agent
→ send command through ObsRelayService
→ OBS Agent executes command on local OBS
🕹️ Menu and button workflow
Interactive menu logic is centered around /menu and /botmenu.
The menu keeps per-user session state, including:
- current screen;
- selected inventory item;
- selected market listing;
- selected craft recipe;
- selected OBS streamer;
- selected OBS scene;
- selected OBS source;
- short-lived OBS status/scenes/source cache.
When changing screens:
- Update the session state.
- Persist the session.
- Re-render the current menu message.
- Use ephemeral replies where appropriate.
- Do not perform heavy operations unless the user explicitly opens that screen or clicks a refresh/action button.
When adding a new button:
-
Add a new
CommandDTO. -
Register it in the constructor:
this.buttons.set(this.someButton.toString(), this.someHandler);
-
Implement the handler.
-
Update session state only inside the handler.
-
Re-render through the existing render/update helpers.
-
Add locale strings for all supported languages.
-
Run:
npm run build
When adding a new select menu:
-
Add a new
CommandDTO. -
Register it:
this.stringSelectMenu.set(this.someSelect.toString(), this.someSelectHandler);
-
On selection:
- update the selected value in session state;
- clear dependent selections;
- clear related cache;
- re-render the current screen.
Dependency examples:
Changing OBS streamer
→ clear selected OBS scene
→ clear selected OBS source
→ clear OBS status/scenes/source cache
Changing OBS scene
→ clear selected OBS source
→ clear cached scene items
🎬 OBS menu workflow
The normal OBS panel must use OBS Agent relay mode.
Do not use direct OBS_WEBSOCKET_URL server-side configuration in the normal menu flow.
When opening the OBS panel:
-
Load streamers for the current Discord guild.
-
If no streamers exist, show:
No streamers registered. Use /streamer register first. -
If one streamer exists, select it automatically.
-
If multiple streamers exist, render a streamer select dropdown.
-
Default selection order:
- previously selected streamer if still valid;
- primary streamer;
- first streamer.
The OBS panel should display:
- selected streamer nickname;
- primary marker if applicable;
- OBS Agent id;
- agent online/offline status;
- control mode:
relay-agent; - OBS endpoint returned by the agent if available;
- current scene;
- selected scene;
- selected source.
All normal OBS menu actions must route through the selected streamer's OBS Agent.
Relay commands:
obs.getStatus
obs.listScenes
obs.listSceneItems
obs.switchScene
obs.setSourceVisibility
obs.setTextInputText
obs.triggerMediaInputAction
Do not call direct ObsService methods from the normal OBS menu panel.
Only fetch scene items after a scene is selected.
select scene
→ clear selected source
→ request obs.listSceneItems for selected scene
→ render source dropdown
The OBS panel must show clear errors for:
- missing Discord guild id;
- no registered streamers;
- selected streamer not found;
- selected streamer has no OBS Agent configured;
- selected OBS Agent is offline;
- OBS command timeout;
- scene not selected;
- source not selected;
- OBS command failure.
📡 OBS Agent relay workflow
The relay keeps active WebSocket connections to OBS Agents.
agent connects to relay
→ agent sends hello with agentId and agentToken
→ relay validates credentials
→ relay registers socket
→ relay sends hello_ack
bot creates requestId
→ bot sends command to agent
→ relay stores pending request
→ agent executes OBS command
→ agent returns command_result
→ relay resolves/rejects pending request
agent sends ping
→ relay responds pong
Heartbeat prevents idle WebSocket proxy timeouts. No heartbeat = proxy might go afk like 0/10 mid pudge. 🪝
The project uses MySQL.
Current database responsibilities include:
- Discord guild/member/role/channel data.
- Command permissions.
- Item templates, rarities, inventory, market, and bot shop.
- Craft recipes and ingredients.
- Streamers and guild-streamer bindings.
- OBS Agent bindings through bot settings.
- Service item OBS actions.
- Twitch notification channels.
For a fresh development database:
npm run db:init:devFor a fresh production database:
npm run db:init:proddb:init creates the baseline schema from sql/tables.sql.
Use it for fresh databases only.
For existing databases, use migrations:
npm run db:migrate:dev
npm run db:migrate:prodMigrations live in:
sql/migrations/
Every database structure change must be tracked by a new migration file.
Examples:
- creating a new table;
- adding a column;
- adding an index;
- changing constraints;
- moving data into a new structure;
- creating a new relation table.
When changing the database schema:
-
Update
sql/tables.sqlso fresh databases have the correct baseline schema. -
Add a new migration file:
sql/migrations/00X_description.sql -
Test locally:
npm run db:migrate:dev npm run build
-
Commit SQL and code changes together.
-
On production:
git pull npm run db:migrate:prod npm run build pm2 restart all --update-env
- Never edit old migrations after they were applied to a shared or production database.
- Never reuse migration numbers.
- Prefer additive migrations.
- Destructive migrations must be reviewed manually.
- Do not drop production data silently.
- Keep migrations idempotent where practical.
- Run migrations before restarting production bot when new code depends on new schema.
Currently OBS Agent binding is stored through bot settings.
Future improvement:
CREATE TABLE streamer_obs_agents (
id INT AUTO_INCREMENT PRIMARY KEY,
streamer_id INT NOT NULL,
agent_id VARCHAR(128) NOT NULL UNIQUE,
agent_token_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(128) NULL,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
last_seen_at TIMESTAMP NULL,
revoked_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (streamer_id) REFERENCES streamers(id) ON DELETE CASCADE
);This would allow:
- multiple agents per streamer;
- named agents, for example
Home PC,Laptop,Studio; - default agent selection;
- token revocation;
- last seen tracking;
- better auditing.
Do not add this table without a migration.
After changing OBS/menu/relay logic
[ ] npm run build passes
[ ] bot deploy succeeds
[ ] desktop OBS Agent connects to relay
[ ] OBS Agent connects to local OBS
[ ] /streamer agent_show shows agent online
[ ] /botmenu opens
[ ] OBS panel selects primary streamer by default
[ ] OBS panel shows streamer/agent status
[ ] OBS panel does not ask for OBS_WEBSOCKET_URL
[ ] scenes refresh works
[ ] scene switching works
[ ] source list loads only after scene selection
[ ] source visibility toggle works
[ ] text update works if source is text input
[ ] media action works if source is media input
[ ] agent Recent Events shows incoming commands
After changing database schema
[ ] new migration file exists
[ ] sql/tables.sql updated if baseline changed
[ ] npm run db:migrate:dev passes
[ ] npm run db:migrate:dev second run skips applied migrations
[ ] npm run build passes
[ ] prod migration applied before restart
After changing desktop agent protocol
[ ] main bot relay updated
[ ] desktop agent updated
[ ] main bot deployed first
[ ] agent release created second
[ ] old agent behavior considered
[ ] auto-update assets uploaded
The desktop OBS Agent is released separately in balkon-obs-agent.
For a patch release:
cd balkon-obs-agent
npm version patch --no-git-tag-version
npm run dist
git add .
git commit -m "chore: release v0.1.X"
git pushCreate a GitHub Release:
Tag: v0.1.X
Title: Balkon OBS Agent v0.1.X
Upload:
Balkon-OBS-Agent-Setup-0.1.X.exe
Balkon-OBS-Agent-Setup-0.1.X.exe.blockmap
latest.yml
Every new desktop app version must have:
- updated
package.jsonversion; - matching GitHub tag;
- matching installer filename;
- matching
latest.yml.
Recommended demo route:
- Run
npm run dev. - Register slash commands with
npm run dev-deploy. - Create one rarity with
/raritycreate. - Create an item template with
/itemcreate, including animage_url. - Inspect it with
/iteminfoor/itemcatalog. - Give the item to a test user with
/itemgive. - Show
/inventoryand/itemview. - Put one item on the market with
/market selland buy it from another user with/market buy. - Add one fixed bot listing with
/botshop add, then demonstrate/botshop buyand/botshop sell. - Open
/menuand show balance, inventory, market, bot shop and admin shortcuts. - Create one craft recipe and show
/craftconsuming materials into a crafted result item. - Start
balkon-obs-agent, connect it to OBS, then show/botmenu → OBSscene control.