Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ttrpg-canvas

One tldraw canvas per room, one Postgres row per room's worth of state, and a Discord login screen standing between your players and both. That's the whole pitch. Everything below is just the part where I convince you it also has locks on the right doors.

Setup

  1. Postgres. Any instance works — for local dev, one line:
    docker run -d --name ttrpg-pg -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=ttrpg -p 5433:5432 postgres:17-alpine
  2. Create a Discord application at https://discord.com/developers/applications, and add the redirect http://localhost:4567/auth/discord/callback under OAuth2. Discord will not tell you if you got this wrong. It will just fail.
  3. Copy .env.example to .env and fill in:
    • DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET
    • ADMIN_IDS — comma-separated Discord user IDs. Admins are auto-approved and get the /admin page plus the "Make GM-only" button. Put yourself on this list. Obviously.
    • COOKIE_SECRET — any long random string. Yes, that one, from your password manager's "generate" button.
  4. pnpm dev → http://localhost:4567. That's it. That's the setup.

How it actually works

Everyone logs in with Discord, because building your own auth in 2026 is a choice, and not a good one. New users land on a waiting screen until an admin approves them at /admin — that page auto-refreshes, the waiting screen polls on its own, and nobody has to hit refresh like an animal.

Admins create rooms ({name, slug}) at /admin, each with its own per-user allowlist. Players see and connect to only the rooms they're on; admins walk into all of them, because admins made the rules. Canvases live at /r/<slug> and persist as snapshots in Postgres, debounced two seconds after every change — enough to not hammer the database, not enough for anyone to notice the lag.

GM-only shapes. As an admin, select any shape or frame and hit "Make GM-only." Non-admin clients hide it — and its children — through tldraw's getShapeVisibility. This is UI-level hiding. The data still syncs to every client sitting in that room; it's hidden, not secret, and if one of your players opens devtools that's a conversation between the two of you, not a bug report to me.

Cursors. Other people's cursors carry their Discord avatar and name, so you always know exactly whose token is about to get dragged into the lava by accident.

Dice. The 🎲 box in the top-right rolls Rollem syntax — 2d6+3, 4d6kh3, 2d20kh1 + 5 for initiative, d20!, and so on. Rolls happen server-side, which is the only part of this app that gets to be paranoid: the result lands as a text shape at your viewport center, and nobody's client-side Math.random ever gets a vote. The grammar itself is vendored (MIT) in server/rollem/vendor/; regenerate the parser with node server/rollem/build-parser.mjs if you ever touch it, which you won't.

Image and video uploads sit on disk in ./uploads (UPLOADS_DIR). Not S3. Not a CDN. A folder. It's a home game, not a hyperscaler.

Scripts

  • dev — dev server, frontend and backend, both on :4567
  • build / start — production build and run

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages