Hand-drawn with Procreate.
An interactive presentation system where your audience votes on what happens next. Think "Choose Your Own Adventure" meets live polling for tech talks and workshops.
You write your presentation as markdown files with decision points. When you reach a choice, your audience votes on their phones, and the presentation follows the path in real-time. All communication happens over WebSockets, so votes appear instantly.
Because presentations should be fun! I sat through way too many boring presentations which could have a lot better given a tiny bit of adventure. Don't get me wrong, I like a good technical presentation that shares a lot of interesting details about the latest™ tech. But after a long day at KubeCon you don't want to sit through yet another slideshow.
This frustration and my love for TTRPGs made this... thing, happen.
This is in heavy alpha. While I tested it, I'm pretty sure it's not without its faults. Especially since I'm terrible at frontend development. The code there is pieced together from forums, books, and bits of code from various repos that do some websocket handling. It works quite well, and I'm reasonably sure it does not farm coins...
I used the bare minimum I could find, which is alpine.js.
Using Docker:
docker-compose up --buildThen open http://localhost:8080/presenter for your presentation screen and share http://localhost:8080/voter with your audience.
Or build from source:
make build
# Or: go build -o adventure .
./adventureThe frontend is embedded in the binary at compile time using Go's embed package, so you only need the binary and your content files for distribution.
Once done, run docker-compose down to shut it down.
Download from latest release:
Go to the GitHub release page, for example: https://github.com/user/adventure-voter/releases/tag/v0.0.1
Find your binary, download and extract it. From there, simply create a structure like this:
➜ tree
.
├── adventure
└── content
├── chapters
│ ├── 01-intro.md
│ ├── 02-certificate-choice.md
│ ├── 03a-cfssl-success.md
│ ├── 03b-openssl-fail.md
│ ├── 03c-self-signed-disaster.md
│ ├── 04-etcd-choice.md
│ ├── 05a-etcd-success.md
│ ├── 05b-etcd-warning.md
│ ├── 05c-etcd-disaster.md
│ ├── 06-apiserver-choice.md
│ ├── 07a-apiserver-success.md
│ ├── 07b-apiserver-insecure.md
│ ├── 07c-apiserver-broken.md
│ ├── 08-network-choice.md
│ ├── 09a-network-success.md
│ ├── 09b-network-mess.md
│ ├── 09c-network-broken.md
│ └── 10-final-success.md
└── story.yaml
3 directories, 20 files
And run the binary like this:
➜ ./adventure
2025/11/21 08:16:47 Adventure server starting...
2025/11/21 08:16:47 Content: /Users/user/goprojects/presentation/content/chapters
2025/11/21 08:16:47 Story: /Users/user/goprojects/presentation/content/story.yaml
2025/11/21 08:16:47 Static: /Users/user/goprojects/presentation/frontend
2025/11/21 08:16:47 Server: http://localhost:8080
2025/11/21 08:16:47 Voter: http://localhost:8080/voter
2025/11/21 08:16:47 Presenter: http://localhost:8080/presenter
2025/11/21 08:16:47 Presenter authentication: DISABLED
2025/11/21 08:16:47 Starting server on :8080
2025/11/21 08:16:47 Content directory: /Users/user/goprojects/presentation/content
2025/11/21 08:16:47 Static directory: /Users/user/goprojects/presentation/frontend
Navigate to http://localhost:8080 and you should be greeted with the choice of being a presenter or a voter.
Content lives in markdown files with YAML front-matter. Here's a basic story chapter:
---
id: intro
type: story
next: next-id
---
# Welcome to the Adventure
This is your presentation content. Use regular markdown syntax.Decision points let the audience vote:
---
id: first-choice
type: decision
timer: 60
choices:
- id: option-a
label: Try the risky approach
next: risk-path
- id: option-b
label: Play it safe
next: safe-path
---
# What Should We Do?
The audience will vote on the next step.Start in content/story.yaml:
# Adventure Voter Story Index
start: introAnd go from there by building up the chain through next sections in the markdown files.
Open the presenter view on your screen and start sharing the voter URL. As you navigate through your story, decision points will automatically trigger voting sessions. Results appear in real-time, and when voting closes, click continue to follow the winning path.
You can generate a QR code for the voter URL to make it easier for your audience to join.
The backend is a Go server handling WebSocket connections and vote aggregation. The frontend uses Alpine.js for reactive UI without heavy frameworks. Voters connect via WebSocket to submit votes, and the presenter view shows real-time results as they come in.
┌──────────────┐
│ Voters │ WebSocket connections
│ (phones) │
└──────┬───────┘
│
↓
┌──────────────┐
│ Go Backend │ Vote counting and state
└──────┬───────┘
│
↓
┌──────────────┐
│ Presenter │ Main display
│ (screen) │
└──────────────┘
The server is designed to run behind a reverse proxy like Nginx or Traefik. See for a configuration examples.
For quick deployment on a cloud server:
git clone <your-repo>
cd adventure-voter
docker-compose up -dThen configure your reverse proxy to handle TLS and forward requests to port 8080.
The server accepts several flags:
./adventure \
-addr=:8080 \
-content=content/chapters \
-story=content/story.yaml \
-presenter-secret=your-passwordConfiguration flags:
-addr: Server address (default::8080)-content: Path to chapter markdown files (default:content/chapters)-story: Path to story.yaml (default:content/story.yaml)-presenter-secret: Authentication password (optional; disables auth if empty)
The presenter secret is optional. If set, presenter control endpoints require authentication. This prevents audience members from advancing slides. Public endpoints (viewing chapters, voting) remain open.
The application includes optional presenter authentication and is designed for deployment behind a reverse proxy.
Key security features include thread-safe state management, optional Bearer token auth for presenter endpoints, and proper file path sanitization.
Other than that, the bare minimum has been done to achieve security, this isn't a mission-critical application. It is meant to be short-lived.
If WebSocket connections fail, check that your reverse proxy passes upgrade headers correctly and that port 8080 is accessible. Browser developer tools will show WebSocket connection status in the Network tab.
If votes aren't updating, verify the WebSocket connection is established and check the server logs for errors.
If markdown isn't rendering, validate your YAML front-matter syntax and ensure file paths in story.yaml match your actual files.




