Production-ready Discord bot for the JARTON Studios middleman marketplace.
- Stack: TypeScript, discord.js v14, Prisma, MariaDB
- Deployment: VPS + systemd (no Docker)
- Cross-server privacy model: clients and freelancers communicate through a strict reply-only bridge between two guilds
- Slash-command architecture with modular folders (
src/features,src/commands,src/utils) - Honey/orange themed embeds with centralized style (
EMBED_COLOR, optionalLOGO_URL) /ticket- Opens guided Order flow (TOS -> department -> service -> modal -> ticket creation)
- Staff can post panel embed with
post_panel=true
- Ticket panel buttons
OrderSupport(modal + staff shadow ping)
- Cross-Discord bridge (Option 1, implemented)
- One bot in both guilds
- Linked channels (
ticket_links) - Reply-only forwarding on both sides
- Invalid non-reply messages are deleted (if permitted) and user is DM'd with instructions
- Message chain tracked in
message_bridge
- Quote lifecycle
/submit quotefrom freelancer-side linked channels- Client-side accept/deny buttons
- On accept: state updated, channels renamed from
quote-*toticket-*, freelancer access tightened
/bugreportmodal -> bug report channel/profile+/portfoliopersistence (view/set)/balance+/walletwallet readout- Stubbed command scaffolding for all requested commands with TODO marker and permission placeholders
- Audit logging for ticket/quote/job state changes (DB + service-records channel)
- Startup lockdown enforcement for panel/listing channels
- One bot application token added to both guilds.
- Each order creates:
- Client guild channel (client + staff)
- Freelancer guild channel (relevant freelancer roles + staff)
- Messages are mirrored only via reply-to-bot-forwarded-embed rules.
- Two bot apps/tokens, shared DB queue or internal API.
- Better permission isolation, more operational complexity.
- Single guild with hidden live chat is not truly possible in Discord text channels if both parties can see the channel.
- For strict separation, two guilds + bridge is required.
src/
commands/
definitions.ts
handler.ts
config/
env.ts
services.ts
db/
prisma.ts
features/
bridge/service.ts
jobs/quotes.ts
tickets/service.ts
types/context.ts
utils/
audit.ts
content.ts
embed.ts
permissions.ts
index.ts
register-commands.ts
prisma/
schema.prisma
config/
services.json
systemd/
jartonite.service
.env.example
README.md
- Node.js 20+
- MariaDB 10.6+
- systemd-based Linux VPS
- Discord bot with permissions:
- Manage Channels
- Manage Roles (if you expand role workflows)
- Manage Messages
- Send Messages
- Embed Links
- Attach Files
- Read Message History
- Add Reactions
- Privileged intents (Developer Portal -> Bot):
- Message Content Intent (required for cross-discord reply bridge)
- Server Members Intent (optional for this build; disabled by default)
- Clone project.
- Install dependencies:
npm i- Create MariaDB database and grants.
If maria is unavailable on Ubuntu 22.04, use mariadb:
mariadbSession context captured from your VPS:
- Host:
ip-23-137-105-200 - MariaDB monitor server version:
10.6.23-MariaDB-0ubuntu0.22.04.1
Commands used on your VPS:
CREATE DATABASE IF NOT EXISTS jartonite
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON jartonite.* TO 'mcuser'@'localhost';
FLUSH PRIVILEGES;
SHOW DATABASES;
USE jartonite;Because your grant uses mcuser@localhost, .env is configured with:
DATABASE_URL=mysql://mcuser:[email protected]:3306/jartoniteReplace CHANGE_ME with the real password for mcuser (or adjust auth/user as needed).
- Configure environment:
cp .env.example .env- Edit
.envand set all required IDs/tokens. - Edit
config/services.jsonfor your department/service map and freelancer role mapping. - Generate Prisma client + run migrations:
npm run prisma:generate
npm run prisma:migrate- Register slash commands (for configured guild IDs):
npm run register:commands- Build and run:
npm run build
npm run start- Copy template:
sudo cp systemd/jartonite.service /etc/systemd/system/jartonite.service- Edit service file
User,Group,WorkingDirectory, andEnvironmentFile. - Enable/start service:
sudo systemctl daemon-reload
sudo systemctl enable jartonite
sudo systemctl restart jartonite
sudo systemctl status jartonite- View logs:
journalctl -u jartonite -f- Use
/ticket post_panel:trueas staff to post the panel in channel1478952132546924655. - Running
/ticketwithoutpost_panelstarts the same order flow as panel button.
See .env.example. Core required variables:
DISCORD_TOKEN,DISCORD_CLIENT_IDDATABASE_URLCLIENT_GUILD_IDCLIENT_ORDER_CATEGORY_ID,CLIENT_SUPPORT_CATEGORY_IDTICKET_PANEL_CHANNEL_IDSERVICE_RECORDS_CHANNEL_IDBUG_REPORTS_CHANNEL_IDREVIEWS_CHANNEL_IDJOB_LISTINGS_CHANNEL_IDSTAFF_PING_ROLE_IDSTAFF_ROLE_IDS
Freelancer bridge variables (required for Option 1 full operation):
FREELANCER_GUILD_IDFREELANCER_ORDER_CATEGORY_IDFREELANCER_STAFF_ROLE_IDS(optional override)FREELANCER_ROLE_FALLBACK_IDS(optional)ENABLE_MESSAGE_CONTENT_INTENT(settruefor bridge)ENABLE_GUILD_MEMBERS_INTENT(optional, defaultfalse)EMBED_ICON_URL(optional; use your GIF/icon URL for embed header icon)
If freelancer bridge variables are missing, bot logs a clear warning and continues with client-side ticket creation only.
Troubleshooting:
Unknown interaction (10062):- Usually means multiple running instances are using the same bot token.
- Stop duplicate processes so only one bot instance is active.
Can't reach database server at 127.0.0.1:3306:- Database is unreachable from the bot process.
- Check MariaDB service status and verify
DATABASE_URL.
Fully implemented now:
/ticket/bugreport/profile/portfolio/balance/wallet/submit quote
Stubbed with TODO scaffolding:
/affiliate,/busy,/unbusy,/calculate,/denyticket,/undeny,/earnings,/report,/setother,/setpaypal,/settimezone,/setwise,/withdraw,/withdrawals,/claimroles,/toggleother,/apply,/embed,/reviews
message_bridgeenforces reply-only forwarding.- All ticket and quote lifecycle transitions are written to
audit_logsand service-records channel. config/services.jsonsupports missing department/service images (null) and fallback role behavior.TOS_URLfalls back to a placeholder if unset.