Self-hosting: set up your central service
FloorVote runs on your own Cloudflare account. This page gets your central service running — the one shared worker that pulls in legislative data and feeds it to your teams. Once it's up, you add one or more tenants (a tenant is one team's instance).
It's a guided, one-time setup, and every step is a terminal command you copy and run. You don't need to be a developer, but you should be comfortable running commands in a terminal.
Accounts you'll need
Set up two accounts before you start:
- Cloudflare — where FloorVote runs. You'll need the Workers Paid plan ($5/month): the Free plan can't send email to your members, and FloorVote uses magic-link authentication, so email is the only way anyone can log in. The Free plan also covers only very light traffic — enough for a demo, not a working deployment.
- LegiScan — where the bill data comes from. Register for a LegiScan account here, confirm it, then follow their steps to generate an API key here. The free tier should be enough for many deployments, but heavier API users (what does that mean?) might need a paid LegiScan API subscription.
IMPORTANT
LegiScan provides the bill data through their API but is not involved with FloorVote. If you sign up for LegiScan, please don't contact them for help with FloorVote.
NOTE
FloorVote also contains some code for interfacing with the OpenStates API as an alternative to LegiScan, but this code path is not actively maintained and lacks features present in the LegiScan path.
Cloudflare API tokens
You'll use two kinds of Cloudflare API token. Knowing the difference up front makes the rest of this guide clearer:
- One broad deploy token, kept on your own machine. This is what
wrangler(Cloudflare's command-line tool, used throughout this guide) uses to create resources and deploy workers. It lives in your shell asCLOUDFLARE_API_TOKEN. - A few narrow runtime tokens, which the deployed worker itself uses for the handful of Cloudflare APIs it calls — for example, delivering bills to each team's queue. Each is stored as a worker secret, scoped to only what it needs. You create these at the steps that use them (the first is
CF_QUEUES_TOKEN, below).
WARNING
Save each token as you create it. Cloudflare shows a token's value only once. Paste each one into a password manager or a secure note the moment you create it — that's your record of your deploy token and each runtime token.
Create the deploy token
- In the Cloudflare dashboard, go to My Profile → API Tokens → Create Token.
- Start from the "Edit Cloudflare Workers" template. It already covers almost everything — you only need to Add three more permissions. Each is a row with three fields (group, resource, access):
- Account / D1 / Edit
- Account / Queues / Edit
- Zone / DNS / Edit — lets you put teams on custom domains later
- Create the token, copy it, and add it to your shell so
wrangleruses it automatically:
# in ~/.zshrc or ~/.bashrc — API tokens don't expire
export CLOUDFLARE_API_TOKEN="..."(Alternatively, wrangler login signs in through your browser, but that session expires periodically.)
Prerequisites
- A Cloudflare account on the Workers Paid plan, with the deploy token above exported in your shell.
- R2 storage enabled on that account — a one-time step: in the dashboard, go to R2 → Overview and complete the subscription checkout. It has a free tier, but you can't create a bucket until it's activated.
wranglerinstalled:npm install -g wrangler. Commands below are written as barewrangler, which uses that global install. You can instead prefix every command withnpxto use the version pinned in the repository'spackage.json— which is what the project's own scripts (new-instance.sh,deploy.sh) do. Either works; pick one and stay with it, since a global wrangler and a pinned one can differ enough in config handling to make a resource you created one way invisible to a script run the other.- The repository cloned, with dependencies installed (
npm installfrom the repo root, and once insidecentral/).
How it fits together
- Central service — one per operator. It makes all the legislative API calls and stores all the bill data, so provider traffic, caching, and quota all sit in one place however many teams you run. You set this up on this page.
- Tenant workers — one per team or topic. Each has its own users, votes, comments, and positions, and never calls LegiScan directly. You add these on the next page.
Set up the central service
1. Provision Cloudflare resources
Run from the repository root:
wrangler d1 create central-bills-ls --location enam
wrangler r2 bucket create central-bill-texts-ls
wrangler queues create central-legiscan-ingestorSave the database_id from the D1 output — you'll need it in the next step.
IMPORTANT
The -ls suffix is required, not stylistic. npm run deploy:legiscan applies migrations to a database named exactly central-bills-ls. The unsuffixed names belong to the OpenStates path, so the two providers can coexist on one account without colliding.
TIP
--location sets the D1 primary region, and it defaults to wherever you are when you run the command — not where your users are. Pick the hint nearest your audience (enam, wnam, weur, eeur, apac, oc). Getting this wrong adds a cross-ocean round trip to every query, and changing it later means recreating the database and reseeding.
2. Configure central/wrangler.toml
Copy central/wrangler.example.toml to central/wrangler.toml and fill in your values. The key settings for a LegiScan deployment:
[env.legiscan]
name = "floorvote-central-legiscan"
main = "src/index-legiscan.ts"
[env.legiscan.vars]
# optional — shown as a credit in each team's footer
OPERATOR_NAME = "Your Organization Name"
BILL_PROVIDER = "legiscan"
[[env.legiscan.d1_databases]]
binding = "DB"
database_name = "central-bills-ls"
database_id = "<YOUR_D1_DATABASE_ID>"
migrations_dir = "migrations-legiscan"
[[env.legiscan.r2_buckets]]
binding = "BILLS_BUCKET"
bucket_name = "central-bill-texts-ls"
[[env.legiscan.queues.producers]]
binding = "INGESTOR_QUEUE"
queue = "central-legiscan-ingestor"
[[env.legiscan.queues.consumers]]
queue = "central-legiscan-ingestor"
max_batch_size = 5
max_batch_timeout = 30
[env.legiscan.triggers]
crons = ["0 * * * *"]See central/wrangler.example.toml for the complete template, including optional bindings.
TIP
To show your organization's logo in the app, add a file named exactly web/public/operator-logo.svg and rebuild. If it's absent, no logo renders (and a harmless 404 for /operator-logo.svg in the browser network log is expected).
3. Set central secrets
From inside central/. First generate a strong ADMIN_SECRET — copy the output, you'll paste it at the prompt in a moment, and save it to your password manager:
openssl rand -base64 32Then set both secrets. Each command prompts for the value:
# Paste the openssl output above at this prompt — it guards the admin endpoints
wrangler secret put ADMIN_SECRET --env legiscan
wrangler secret put LEGISCAN_API_KEY --env legiscanNOTE
You haven't deployed yet, so the Worker doesn't exist — wrangler will ask whether to create one with this name and add the secret to it. Answer yes. That's the expected path when setting secrets before the first deploy.
Queue delivery
Central delivers bills to each team's queue. For that it needs a Cloudflare API token scoped Queues: Edit, plus central's account ID:
wrangler secret put CF_QUEUES_TOKEN --env legiscanAlso set CF_ACCOUNT_ID in [env.legiscan.vars] (it's not a secret — get it from wrangler whoami). Without both, a team can register cleanly yet never receive a single bill, with no error. You can reuse your broad token from above, or create a narrow Queues: Edit token at My Profile → API Tokens → Create Custom Token.
TIP
Central also offers an optional superadmin dashboard and observability panels. They're not needed to get running — see Operating your deployment.
4. Run migrations and deploy
cd central
npm ci
npm run deploy:legiscanNOTE
Use npm ci, not npm install. The repository ships a lockfile; npm ci installs exactly those versions, while npm install re-resolves against whatever has been published since and can fail on a peer-dependency conflict that has nothing to do with your setup.
The deploy script runs migrations for you. The deployed URL will be https://floorvote-central-legiscan.<your-subdomain>.workers.dev — note it, you'll use it as CENTRAL_API_URL when adding tenants.
5. Verify
curl https://floorvote-central-legiscan.<your-subdomain>.workers.dev/api/health
# → {"status":"ok","operator":"Your Organization Name"}Your central service is now running. From here it will sync live bill data on an hourly schedule.
Optional: preload historical bills now
If you already know which sessions your teams will track — say, only New Jersey, or all 50 states plus Congress — you can load that historical bill data into central now, so it's ready the moment you add a tenant. This is optional: you can skip it and bring bills in when you add a tenant instead (the tenant guide walks through seeding a session).
Load LegiScan's bulk JSON datasets — this uses zero API calls. Download the zip files from legiscan.com/gaits/datasets (one per state/session), extract them, then run the seeder for each session:
# LegiScan zips have a nested state/session/ directory inside. Unzip, then point
# --from-dir at the folder that contains the bill/ subdirectory:
unzip -q RI_2026-*.zip -d bulkseeds/RI_2026
find bulkseeds/RI_2026 -name "bill" -type d
# → bulkseeds/RI_2026/RI/2026-2026_Regular_Session/bill
npx tsx scripts/seed-legiscan.ts \
--from-dir bulkseeds/RI_2026/RI/2026-2026_Regular_Session \
--state RI \
--session-id 2253 \
--remoteThis loads the bills into central only. Linking them to a team and running AI summaries happens when you add the tenant.
CAUTION
Speed: about 1,000 bills per minute against the remote database, so a typical state session (500–3,000 bills) takes 1–5 minutes. There's a --from-api option that downloads the dataset for you, but it can run out of memory on very large states (16,000+ bills) — use the manual download with --from-dir for those.
Next steps
- Add your first tenant — a team's instance, with its own users and keywords. This is the next required step.
- Operating your deployment — optional central features (superadmin dashboard, observability) and day-2 tasks (adding states, upgrading, monitoring).
- Turnstile and Public demo site — optional add-ons.
IMPORTANT
Using LegiScan data: your API plan sets a monthly call budget, which central tracks against the legiscan_monthly_limit setting — see How much does it cost? for the tiers. Bill data is licensed CC BY 4.0, so any UI that displays it carries a data-source credit (FloorVote does this by default). Deeper LegiScan operational notes are in docs/internal/legiscan-notes.md in the repository.