Measure public support for any constitutional or civic change.
FTS is an open-source web application for running a public, anonymous consensus tracker. Participants register their support with a single click — no account, no email. A gauge shows progress toward a configurable ratification threshold, with per-region breakdowns. The canonical deployment is jordan92.com, which tracks public support for removing the slavery exception from the 13th Amendment.
The 13th Amendment to the US Constitution, ratified in 1865, abolished slavery — except for one carve-out buried in the text:
"Neither slavery nor involuntary servitude, except as a punishment for crime whereof the party shall have been duly convicted, shall exist within the United States, or any place subject to their jurisdiction."
Those 13 highlighted words still permit forced prison labor today. jordan92.com exists to count how many Americans want them gone.
FTS is the framework powering that site. It can be deployed for any civic issue where you want a clear, growing public count tied to an actual ratification or approval threshold.
Participants register anonymously — no account, no email, just an anonymous device ID and an optional state selection. The gauge shows the percentage of estimated US smartphone users who have joined. State-level tracking is central because ratification happens state by state.
- Backend: Node.js + Express, better-sqlite3, Helmet, express-rate-limit
- Frontend: Vanilla JS (ES modules), inline CSS, SVG gauge, PWA-capable
- Database: SQLite (single file, WAL mode)
- Identity: UUID stored in localStorage — anonymous, no PII collected
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/join |
Register a participant (device_id, optional state) — rate-limited |
GET |
/api/stats |
National + per-state counts and percentages |
GET |
/api/status/:device_id |
Check whether a device has already joined |
Prerequisites: Node.js 18+
git clone <repo-url>
cd FTS
npm install
cp .env.example .env
cp public/js/site.config.example.js public/js/site.config.js
npm run devOpen http://localhost:3000.
The dev server uses node --watch so it restarts on file changes. The SQLite database is created automatically at ./fts.db on first run.
FTS uses two config files that are never committed to git — you create them from their examples.
cp .env.example .envEdit .env:
PORT=3000
NODE_ENV=production
DB_PATH=/var/data/fts/fts.db # use a persistent path outside the app directory
SITE_URL=https://your-domain.example.com
TURNSTILE_WORKER_URL=https://your-turnstile-siteverify-worker.workers.devSITE_URL is injected into the og:url and og:image meta tags at serve time — set it to your canonical domain so social shares link back to your deployment.
cp public/js/site.config.example.js public/js/site.config.jsEdit public/js/site.config.js. Every field is documented in the example file. Key fields:
| Field | Description |
|---|---|
siteUrl |
Your canonical domain — used in the share button |
mission.title / .tagline |
The headline and subhead shown on every page |
turnstile.siteKey |
Your Cloudflare Turnstile site key (see below) |
amendment.currentHtml |
The text being changed, with <mark> around the target clause |
amendment.proposedText |
The text as it would read after the change |
ratification.threshold |
The approval percentage you're counting toward (e.g. 0.75 for 75%) |
creator.* |
Your name, bio, and contact info shown on the About page |
about.siteNameSection |
Explains your domain/site name |
about.whyThisSection |
Motivates the cause |
Replace the files in public/icons/ with your own branding:
icon-192.pngandicon-512.png— PWA iconsog.png— social share preview (1200×630px recommended)
Edit the three lines that identify your deployment:
"name": "Your App Name",
"short_name": "YAN",
"description": "Your short description."The public/js/pages/why.js file contains a long-form essay specific to the Jordan92/13th Amendment deployment. Replace it with your own "Why This Matters" page, or remove the route from public/js/app.js if you don't need one.
npm install --omit=devnpm startUse pm2 or a systemd unit to keep the server alive across reboots.
pm2 example:
npm install -g pm2
pm2 start src/server.js --name fts
pm2 save
pm2 startupsystemd example (/etc/systemd/system/fts.service):
[Unit]
Description=FTS Civic Consensus Tracker
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/fts
EnvironmentFile=/opt/fts/.env
ExecStart=/usr/bin/node src/server.js
Restart=on-failure
[Install]
WantedBy=multi-user.targetRun FTS behind nginx, Apache, or Caddy to handle TLS and forward traffic.
nginx snippet:
server {
listen 443 ssl;
server_name your-domain.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Apache VirtualHost snippet (requires mod_proxy, mod_proxy_http, mod_headers):
<VirtualHost *:443>
ServerName your-domain.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "https"
SSLEngine On
SSLCertificateFile /etc/ssl/certs/your-domain.example.com.pem
SSLCertificateKeyFile /etc/ssl/private/your-domain.example.com.key
</VirtualHost>ProxyPreserveHost On passes the original Host header through. RequestHeader set X-Forwarded-Proto is needed because Apache doesn't set it automatically the way nginx does; without it, Express would think all requests are plain HTTP.
Caddy snippet (Caddyfile):
your-domain.example.com {
reverse_proxy localhost:3000
}
FTS uses a single SQLite file. This is intentional — the data is simple, write volume is low, and SQLite in WAL mode handles concurrent reads gracefully. Horizontal scaling (multiple processes sharing the DB) is not supported without switching to a network database. For a civic petition tracker this is unlikely to be needed.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port to listen on |
NODE_ENV |
development |
Set to production in prod |
DB_PATH |
./fts.db |
Path to the SQLite database file |
SITE_URL |
http://localhost:3000 |
Canonical URL injected into OG meta tags |
TURNSTILE_WORKER_URL |
(none) | URL of the deployed Cloudflare Turnstile siteverify Worker (see below). If unset, /api/join skips token verification — fine for local dev, must be set in production. |
The join flow is protected by Cloudflare Turnstile, verified server-side via a dedicated Cloudflare Worker (see docs/adr/002-turnstile-bot-protection.md for why a Worker instead of direct siteverify).
Your Turnstile site key goes in public/js/site.config.js under turnstile.siteKey. The site key is public-facing (it's embedded in browser JS), so it doesn't need to be in .env. The secret key is held inside the Cloudflare Worker and never touches this codebase.
Set your Worker's URL as TURNSTILE_WORKER_URL in .env.