An internal tool for Xitter staff to make public forms and polls, like Google Forms. Staff sign in with their Xitter accounts; anyone with a form's link can answer it.
- Builder: title and description, then any number of questions: short answer, paragraph, multiple choice, checkboxes, dropdown, linear scale (0/1 to 2–10, with end labels) and date. You can mark questions required, reorder, duplicate and delete them. Changes save automatically.
- Sharing: every form has a public link (
/f/<id>) that works without JavaScript, on phones, and in light and dark mode. - Staff only: only the accounts listed in
XITPOLL_STAFFcan make and manage forms. Every staff member sees and can edit every form; each shows who made it. Anyone else who signs in is told the tool is for staff. - Sign in with Xitter: a form can require respondents to sign in, which records which account answered, and can limit each account to one response.
- Polls: optionally show respondents the results of the choice questions after they submit.
- Responses: a chart for each question, a table of individual responses, CSV export, closing and reopening a form, and deleting all responses.
- Looks: styled like Xitter itself, in light and dark mode, with X's six accent colors to choose from for each form.
No dependencies: Node 22.13+ and its built-in SQLite.
Xitter is an OAuth 2 provider (authorization code with mandatory PKCE, public
clients only, see src/oauth.ts in the Xitter repo). XitPoll:
- finds the instance's endpoints at
/.well-known/oauth-authorization-server, - sends the person to
/oauth/authorizewith thereadscope, a randomstate(also kept in a cookie, so a sign-in can only finish in the browser that started it) and an S256 code challenge, - trades the code at
/oauth/tokenwith the verifier, - calls
GET /api/v1/meonce to learn the account's id, handle, name and avatar, and then discards the token.
Accounts are identified by instance + Xitter user id, so @sam on two
different instances are two different people.
There are two ways to connect to Xitter.
- On your Xitter instance, open /account/apps/developer and register an
app with the redirect URL
http://localhost:3000/auth/callback(or your https URL +/auth/callbackin production). - Start XitPoll with the client ID it gives you:
XITTER_INSTANCE=https://your.xitter.instance \
XITTER_CLIENT_ID=1234567890 \
npm startXitPoll serves an OAuth client metadata document at /client-metadata.json
and uses its URL as the client_id, so people on any Xitter can sign in:
the sign-in page asks which instance they're on. Xitter only accepts this when
the document is served over https from a host without a port, so this mode
needs a real deployment:
PUBLIC_URL=https://polls.example.com npm startSet XITTER_INSTANCE as well to skip the instance question and allow only
that one instance. Instances people type in must be public https hosts; XitPoll
refuses private and loopback addresses.
List staff accounts in XITPOLL_STAFF, separated by commas or spaces:
XITPOLL_STAFF="alice, bob, carol@other.instance"A bare handle means that handle on XITTER_INSTANCE; write handle@host for
accounts on another instance. Xitter's API doesn't tell apps who its own staff
are, so this list is the source of truth.
The first time a listed handle signs in, XitPoll ties the entry to that Xitter
account. If the person later changes their handle and someone else takes the
old one, the newcomer is refused. To move an entry to a different account,
remove it from XITPOLL_STAFF, restart, then add it back. Removing someone
from the list takes effect when XitPoll restarts.
| Variable | Default | |
|---|---|---|
PORT |
3000 |
Port to listen on |
PUBLIC_URL |
http://localhost:$PORT |
The address people use. Builds the redirect URL and share links; https:// also turns on Secure cookies |
XITTER_INSTANCE |
(none) | Allow only this instance |
XITTER_CLIENT_ID |
(none) | A client ID registered on XITTER_INSTANCE |
XITPOLL_STAFF |
(none) | Staff accounts that can make and manage forms (see above) |
DATABASE_PATH |
data/xitpoll.db |
SQLite file |
APP_NAME |
XitPoll |
Shown in the header and on Xitter's consent screen |
Put XitPoll behind a reverse proxy that terminates TLS (Caddy, nginx) in production.
test/mock-xitter.js is a stand-in Xitter that follows the same OAuth rules
and approves every sign-in as @alice, who the demo makes staff. One command starts it together with
XitPoll (works in Windows Command Prompt and PowerShell too):
git clone https://github.com/jyeridley/xitpoll
cd xitpoll
npm run demoThen open http://localhost:3000. Demo data goes to data/demo.db.
Setting variables differs by shell. To start XitPoll with your own settings:
# macOS / Linux
XITTER_INSTANCE=https://your.instance XITTER_CLIENT_ID=123 XITPOLL_STAFF="alice, bob" npm start:: Windows Command Prompt
set XITTER_INSTANCE=https://your.instance
set XITTER_CLIENT_ID=123
set XITPOLL_STAFF=alice, bob
npm start# Windows PowerShell
$env:XITTER_INSTANCE="https://your.instance"; $env:XITTER_CLIENT_ID="123"; $env:XITPOLL_STAFF="alice, bob"; npm startnpm testThe end-to-end test signs in through the mock Xitter, builds a form, answers it anonymously and as signed-in accounts, and checks validation, ownership, CSRF and origin checks, one-response limits, CSV export and closing a form.
server.js HTTP routes, sessions, CSRF and origin checks
src/xitter.js Sign in with Xitter (discovery, PKCE, token exchange, /me)
src/forms.js Question types, sanitizing, answer validation, summaries, CSV
src/db.js SQLite schema and queries
src/views.js Server-rendered pages
public/builder.js The form editor
public/style.css Styles (light and dark)
test/ End-to-end test and the mock Xitter