Skip to content

About

Resources

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Latest commit

 

History

553 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TabletopTime

Ditch the group chat chaos. A scheduling tool for tabletop gamers, hosted for the community or self-hosted on your own server.

CI Docker image License: CC BY-NC-SA 4.0 Support on Ko-fi

Live on tabletoptime.us right now: Events active Voting open Players active Games locked in

What it is

TabletopTime helps your gaming group find the best time to meet. A host proposes time slots, everyone votes without making an account, and the host finalizes a slot. The hosted version runs at tabletoptime.us. The same code ships as a Docker image for home servers (Synology, Unraid, Raspberry Pi) and integrates with Telegram and Discord for real-time coordination.

Key Features

  • Host: Create, edit, and delete time slots dynamically. Manage quorum rules (min players) and capacity limits (max players). Remove accepted participants to trigger waitlist auto-promotion.
  • Vote & Suggest: No login required. Simple "Yes", "If Needed", or "No" voting. Attendees can also suggest new time slots if none work.
  • Waitlist: Automatic waitlist management with First-Come-First-Serve promotion when spots open up. "If Needed" players are seated automatically only when the table is below its minimum, "Yes" voters whenever a seat is open; a waitlisted player can switch to "Yes" to claim an open seat.
  • Finalize: Select a host/location and generate calendar invites (.ics / Google Calendar). Once finalized, slot management is locked.
  • Telegram / Discord Bot:
    • Pins a live-updating dashboard in your group chat or channel.
    • Posts slot changes, availability updates, the finalized result, and cancellations.
    • Sends voting and session reminders when the organizer turns them on.
    • DMs the organizer a login link if they lose the manage page, and DMs players about waitlist promotions and final results (each person can turn bot DMs off).
  • My Events & Cross-Device Sync: No account, but sign in through the Telegram bot or with Discord and your voting history follows you anywhere. The profile page shows a per-event sync badge (linked, or "This Device Only"), and you can link or unlink any event you've voted on straight from that badge.
  • Privacy & Security:
    • Zero Tracking: We do not use Google Analytics, Facebook Pixels, or any third-party trackers.
    • Hashed admin tokens: Event admin tokens are hashed (SHA-256) before storage. A token from an older release that is still stored in plaintext is rewritten as its hash the first time it is used.
    • Signed identity cookies: Platform identities (Telegram, Discord) are stored in signed cookies, and a bare platform ID is never trusted as a login.
    • Self-Hostable: Your data stays on your server (SQLite). We capture as little information as possible.
    • Persistence: We use cookies only to restore your session, never to track you.

Run it

The easiest way to run TabletopTime is the pre-built Docker image.

mkdir -p data && sudo chown 1000:1000 data

docker run -d \
  -p 3000:3000 \
  -v ./data:/app/data \
  -e DATABASE_URL="file:/app/data/scheduler.db" \
  --name tabletop-time \
  ghcr.io/mels0n/tabletop_scheduler:latest

Open http://localhost:3000 to start creating events.

Upgrading is automatic. Run docker pull ghcr.io/mels0n/tabletop_scheduler:latest and restart the container; on start it brings your database up to date from whatever release you were on, keeping your data.

Upgrading

Check these settings when you move to a new release with a bot connected:

  • Set NEXT_PUBLIC_BASE_URL. Every link a Telegram or Discord bot sends (logins, events, reminders) is built from it. Without it the app still starts, but it logs an error and bot links fail until you set it. Use an address your players can open; a LAN address works behind NAT.
  • Polling is the self-host default. With a Telegram token and no TELEGRAM_MODE, a self-hosted install polls Telegram, which needs no public URL.
  • Set TELEGRAM_MODE=webhook only with a public URL. If your install used webhook mode before, keep it by setting TELEGRAM_MODE=webhook; NEXT_PUBLIC_BASE_URL must then be a public HTTPS address Telegram can reach. If the log says another consumer owns the bot, a webhook is still registered: set TELEGRAM_MODE=webhook or delete the webhook.

Bind mounts and UID 1000. The container runs as the node user (UID 1000). A bind-mounted ./data directory must be writable by UID 1000, or the database cannot be created and the container exits at startup. A named Docker volume needs no extra step.

Secrets. If SESSION_SECRET or CRON_SECRET is unset, the container generates a random value on first start and keeps it in /app/data (.session-secret, .cron-secret), so they survive restarts as long as the data volume does. Set them explicitly if you run more than one instance against the same data.

How the schema is applied (db push contract). The Docker image uses SQLite and applies the schema on every start with prisma db push. There is no migration history for self-hosters: the schema in the image is the schema you get. Releases only ever add to the schema, and CI checks that every past release upgrades without losing data. If startup ever stops with a data-loss warning (a database edited by hand, for example), it has deleted nothing: back up /app/data/scheduler.db, then start once with PRISMA_ACCEPT_DATA_LOSS=1 to accept the change, and remove the variable afterwards.

Configuration

All configuration is through environment variables, validated once at boot. A missing or invalid value stops the server with a message that lists every problem. See EnvVariables.md for the full reference.

The variables most installs touch:

Variable When you need it
DATABASE_URL Always. SQLite path for Docker (file:/app/data/scheduler.db), pooled Postgres URL when hosted.
DIRECT_URL Hosted only. Direct Postgres URL for migrations.
NEXT_PUBLIC_BASE_URL Whenever a bot token is set. Every link the bots send is built from it.
SESSION_SECRET Production. Docker generates one if unset.
CRON_SECRET Hosted or Vercel production. Docker generates one if unset.
TELEGRAM_BOT_TOKEN To enable the Telegram bot.
TELEGRAM_MODE Optional. polling, webhook or off; derived when unset.
DISCORD_BOT_TOKEN, DISCORD_APP_ID, DISCORD_CLIENT_SECRET To enable the Discord bot.
LOG_LEVEL Optional. debug, info (default), warn or error.

Retention windows, vote announcement cooldown, private webhook sources, Ko-fi, recovery flags and every default are in EnvVariables.md.

Documentation

Develop

Requirements: Node 22 and npm.

git clone https://github.com/mels0n/tabletop_scheduler.git
cd tabletop_scheduler
npm ci
cp .env.example .env        # DATABASE_URL="file:./dev.db" is enough to start
npx prisma db push          # creates prisma/dev.db from prisma/schema.prisma
npm run dev

Blog posts

Posts live in content/blog/*.md. A build publishes only the posts that are live on the build date: draft: true never publishes, and a post dated after the build date waits for a later build. npm run dev, npm run build, npm run typecheck, npm run lint, npm test and npm run depcruise first run scripts/generate-published-posts.mjs, which writes shared/data/published-posts.generated.ts (gitignored). The site reads posts only from that module, through shared/lib/blog.ts.

SHOW_SCHEDULED=1 npm run dev                  # also show future-dated posts
SHOW_DRAFTS=1 SHOW_SCHEDULED=1 npm run dev    # also show drafts
BUILD_DATE=2026-12-01 npm run build           # build as if it were that date (UTC, YYYY-MM-DD)

The preview flags apply only to npm run dev; production builds, CI and any run with NODE_ENV=production ignore them. Restart the dev server after editing a post or changing a flag, because the module is generated once at startup. npm run blog:queue lists live, scheduled and draft posts.

Checks to run before opening a pull request:

npm run typecheck
npm run lint
npm test
npm run depcruise
npm run build

The app has two Prisma targets from one codebase: prisma/schema.prisma (SQLite, self-host and local development) and prisma/hosted/schema.prisma (Postgres, hosted). A schema change edits both files and adds a migration under prisma/hosted/migrations/. See CONTRIBUTING.md and HostedMaintenance.md.

Deploy

The hosted version runs on Vercel with a Supabase Postgres database.

  • Build and migrations. vercel.json points the build at scripts/vercel-build.sh, which generates the Prisma client from the hosted schema, runs prisma migrate deploy and pending data migrations on production builds only, then builds the app. A failed migration fails the deploy. Preview builds never migrate.
  • CI gates. Pull requests and pushes run typecheck, lint, tests, a production build, the dependency-direction check, the self-host upgrade check, and a check that the hosted migration history matches prisma/hosted/schema.prisma. The Docker image is published only after those pass. main is protected, so every change lands through a pull request that passes these checks.
  • Fortnightly rebuild. A GitHub Actions workflow calls a Vercel deploy hook every other Thursday so scheduled blog posts go live. It pushes no commits.
  • Required settings. DATABASE_URL (pooled), DIRECT_URL, NEXT_PUBLIC_IS_HOSTED=true, NEXT_PUBLIC_BASE_URL, SESSION_SECRET, and CRON_SECRET.
  • Scheduled jobs. Event cleanup runs daily from Vercel Cron. Reminders and the outbound webhook queue run from Supabase pg_cron, with a GitHub Actions reminder run every two hours as a backstop; the one-time setup is in HostedMaintenance.md.

Support

The hosted version is free to use. If TabletopTime saves your group some chaos, you can help with the hosting bill on Ko-fi.

License

CC-BY-NC-SA 4.0 (see LICENSE).

Hosted by UntapWeb

About

Resources

Contributing

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages