Pollmd: My Minimal Survey Service (for Newsletter) that Works with Markdown
Pollmd is my minimal (newsletter) survey with dynamic questions right from Markdown (links) with one time installation with DuckDB Quack Protocol.
I wanted to self hostit on my servers and use Markdown, that’s why I used the opportunity to try Quack Protocol from DuckDB and see how far I can get.
# Origin
I saw
Tally as a beautiful and simple poll, but 24$ per month for a couple of links, I saw it as a good opportunity to try DuckDB Quack.
# Code
Pollmd on GitHub.
# Test Surveys and Polls
# Test Survey
|
|
# Test Twitter
|
|
See the result at
init:

# The Query
|
|
# Creating a Poll with Fixed Answers
|
|
that gives you the landing page at 2026-06-16-landing-page to vote or the markdown to add to your newsletter:
|
|
See full command:
|
|
Including the results at Results: 2026-06-16-landing-page.
Landing page:

# Architecture and Flow
Single Go process. libduckdb 1.5.3 is compiled in via
duckdb-go-bindings/v2, and the Quack extension is INSTALL’d / LOAD’d
at startup. The same in-process DuckDB serves both sides — HTTP click
writes and Quack remote reads share one writer, which is what DuckDB
needs (single-writer constraint).
flowchart LR
subgraph readers["Newsletter readers"]
B["Browser"]
end
subgraph laptop["You (laptop)"]
M["make survey-create
make survey-result
make survey-reset"]
D["local duckdb CLI
+ quack extension"]
end
subgraph rly["Railway"]
Edge["HTTPS edge · auto-TLS"]
TCP["TCP Proxy · plaintext + token"]
subgraph cont["pollmd container · single Go process (CGO)"]
HTTP["net/http :8080
vote / landing / result / thanks / style"]
QSrv["Quack listener :9494
started by CALL quack_serve"]
DB[("libduckdb 1.5.3
single writer · in-process")]
Salt["32-byte salt in memory
rotates @ UTC midnight"]
end
V[("Persistent volume
votes.duckdb")]
end
B -->|"GET /{id}/{answer}"| Edge
Edge --> HTTP
HTTP -->|"RecordVote, TallyBySurvey, GetAllowedAnswers"| DB
HTTP -.->|"voter.Hash"| Salt
M -->|"survey-create writes via Quack"| TCP
D -->|"quack_query over HTTP"| TCP
TCP --> QSrv
QSrv -->|"reads/writes in same process"| DB
DB --> V
# One Vote, End to End
flowchart TD
Click["Browser clicks
https://q.ssp.sh/2026-06-04/awesome"]
Click --> Edge["Railway HTTPS edge"]
Edge --> H["Go handleSurvey()"]
H --> Method{"HTTP method?"}
Method -->|"HEAD (Safe Links prefetch)"| OK1["200 · no record"]
Method -->|"GET"| Slug{"slug regex
matches id + answer?"}
Slug -->|"no"| Err["400 Bad Request"]
Slug -->|"yes"| Bot{"User-Agent
looks like a bot?"}
Bot -->|"TwitterBot, etc."| OK2["200 · log bot-skip"]
Bot -->|"browser"| Reg{"survey_id
in surveys table?"}
Reg -->|"no (open mode)"| Hash["voter = sha256(ip + ua + daily_salt + survey_id)[:16]"]
Reg -->|"yes and answer allowed"| Hash
Reg -->|"yes and answer NOT allowed"| OK3["200 · log answer-reject"]
Hash --> Up[("INSERT INTO votes
ON CONFLICT (survey_id, voter)
DO UPDATE — last vote wins")]
Up --> Redir["302 → /thanks · log vote"]
# Changelog
Find all changes at GitHub CHANGELOG.md.
# Alternative Names Brainstorming
- pollmd
- pulsemd
- clickmd — describes the actual mechanic (one click, no form)
- votemd — more generic but clean
# Alternatives
- Tally is 24$ per month, I wanted one that cost me nothing except hosting
# Other Tiny Markdown Tools
I created Neomd with the same Markdown first philosopy in mind. Check it out, if you like Markdown, TUI and Neovim, to handle emails in the terminal with all keyshortcuts and a HEY screener and GTD workflow included.
Origin: Newsletter, Listmonk