Verified polls that can't be faked, censored, or quietly rewritten — running on your own machine, owned by the people who use them.
ProofPoll is a free, open-source desktop app for polls and votes where the results actually mean something. It runs on Linux, macOS, and Windows. Two things set it apart from any normal poll:
- One identity, one vote. Voters link a verified Flowsta identity, so every vote resolves to a single identity — even across all your devices — and duplicates are dropped. No emails to harvest, no burner logins; votes are deduplicated cryptographically per identity.
- No one can rewrite or delete it. Polls and votes live on a peer-to-peer network (Holochain), replicated and validated across everyone who runs the app. There's no admin who can change a result or take a poll down — not even us. Polls can be community-flagged as spam or misleading, but nothing is ever silently deleted.
It even does something most people think is impossible: private data on a public network. Add a private rationale to a vote, or draft a poll before publishing it, and ProofPoll encrypts it on your machine before it ever leaves — peers store the ciphertext but can't read a byte of it.
ProofPoll is built on Flowsta Vault, which keeps your identity and keys on your own machine. The first time you sign in, Vault links your identity; after that, ProofPoll runs on its own.
⬇ Download the latest release — Linux · macOS (Intel + Apple Silicon) · Windows. Windows builds are code-signed (verified publisher: FLOWSTA).
Built to be forked. ProofPoll is also a complete, working template for any desktop Holochain app — swap polls for reviews, proposals, a task tracker, a social feed. The genuinely hard parts (conductor lifecycle, identity linking, DNA migration, encrypted private data, running an always-on node) are solved and documented. See the Forking Guide.
- Windows builds are now code-signed. Installers carry Flowsta's SSL.com OV certificate, so Windows shows FLOWSTA as the verified publisher instead of a SmartScreen "unknown publisher" warning.
- Security: sanitized voter profile-picture image sources on the poll page (closes a DOM-based XSS finding).
- Integrity: every release now ships a
SHA256SUMS.txtso you can verify your downloads. - Docs: clarified that cross-device recovery is identity-aware recognition (re-link your identity, data syncs from the network), not a backup/replay.
- First public release — verified one-person-one-vote polling, community flagging, client-side-encrypted private data, cross-device identity recognition, and the full fork-ready template.
ProofPoll is the live reference implementation of every Flowsta-on-Holochain integration pattern. The authoritative cross-app docs (and the up-to-date version of every link below) live at docs.flowsta.com. Useful starting points whether you're a human or an AI assistant:
- Building Holochain Apps with Flowsta — the full integration guide this README implements.
- For Holochain Developers — the three integration options (OAuth-only, agent linking + Vault, Tauri Vault auth) and what you get with each.
- @flowsta/holochain SDK reference — every function ProofPoll calls into, including identity linking, cross-device recognition, and the canonical-shape backup pipeline.
- Vault IPC reference — the localhost API; the canonical backup payload shape is documented there.
This README focuses on the fork mechanics — what to rename, what to keep, where the seams are. For the why and the recommended patterns, follow the links above.
- Frontend: Qwik, TypeScript, Tailwind CSS
- Backend: Tauri v2 (Rust), Holochain 0.6.1
- DNA: Rust (hdi 0.7.0, hdk 0.6.0 — non-breaking on the 0.6.1 conductor)
- Identity: Flowsta agent linking via
flowsta-agent-linkingcrate - Encryption: lair xsalsa20poly1305 via
crypto_box_xsalsa_by_sign_pub_key
# Prerequisites
# - Rust + wasm32-unknown-unknown target
# - holochain + lair-keystore binaries (v0.6.1) — drop into src-tauri/binaries/
# named `holochain-<target-triple>` and `lair-keystore-<target-triple>`.
# CI does this automatically from the official Holochain GitHub release;
# for local dev, either fetch them yourself or rebuild from source.
# - hc CLI: cargo install holochain_cli --version 0.6.0
# (0.6.0 hc CLI produces bundles the 0.6.1 conductor reads — no recompile
# needed for the 0.6.0 → 0.6.1 non-breaking upgrade.)
# - Node.js 18+
# - flowsta-agent-linking repo cloned as a SIBLING directory
# (../flowsta-agent-linking/ next to this repo, not inside it):
# git clone https://github.com/WeAreFlowsta/flowsta-agent-linking ../flowsta-agent-linking
# The DNA build scripts path-depend on it; cloning ProofPoll alone won't build.
# Build all DNA versions
bash build-all.sh
# Install frontend dependencies
npm install
# Run in dev mode
cargo tauri dev| Version | Network Seed | Features |
|---|---|---|
| v1.0 | proofpoll-network-v1.0 |
Polls, votes, agent linking |
| v1.1 | proofpoll-network-v1.1 |
+ Community flagging, migration support |
| v1.2 | proofpoll-network-v1.2 |
+ Public/anonymous poll types, voter profiles |
| v1.3 | proofpoll-network-v1.3 |
+ Encrypted private data (vote rationale, draft polls) |
All versions are installed side-by-side during migration. All new reads and writes go to v1.3.
ProofPoll demonstrates how to store private data on a public DHT. Entries are encrypted client-side using lair's xsalsa20poly1305 crypto_box before being committed to the DHT. The data is replicated across peers for resilience, but only the author can decrypt it.
- Encrypt — Tauri encrypts plaintext using the agent's Ed25519 signing key (lair converts to x25519 internally)
- Store — the encrypted blob is committed as a public
EncryptedEntryon the DHT - Gossip — peers replicate the ciphertext like any other entry — they can see it exists but cannot read it
- Decrypt — only the author's lair-managed private key can decrypt
cipher: [187, 202, 33, 175, 31, 134, ...] (opaque bytes)
nonce: [244, 219, 96, 104, 85, 138, ...] (random, unique)
hint: "private" (no metadata about content type)
No information about whether the entry is a vote rationale, draft poll, or anything else.
- Vote rationale — after voting, add a private note about why you voted that way. Encrypted, only visible to you. Stored on the DHT linked to your vote via
VoteToRationale. - Draft polls — save polls privately before publishing. Encrypted on the DHT, listed on the Drafts page. Publish when ready (creates a real poll, deletes the draft).
| File | Purpose |
|---|---|
src-tauri/src/crypto.rs |
encrypt_to_self / decrypt_from_self via lair crypto_box |
dna/v1.3/zomes/polls/integrity/src/lib.rs |
EncryptedEntry type, VoteToRationale + AgentDrafts link types |
dna/v1.3/zomes/polls/coordinator/src/lib.rs |
create_encrypted_entry, get_vote_rationale, get_my_drafts, delete_encrypted_entry |
src-tauri/src/commands.rs |
6 Tauri commands: save/get rationale, save/get/publish/delete drafts |
src/routes/poll/[id]/index.tsx |
Vote rationale UI (private note textarea) |
src/routes/drafts/index.tsx |
Drafts page (list, publish, delete) |
src/routes/create/index.tsx |
"Save as Draft" button |
The encryption pattern is generic — EncryptedEntry stores any encrypted blob. To add your own private data types:
- Encrypt your data with
crate::crypto::encrypt_to_self()in a Tauri command - Call
create_encrypted_entrywith a link type that fits your use case - Add a new link type in the integrity zome if needed (e.g.
ItemToPrivateNote) - Decrypt with
crate::crypto::decrypt_from_self()when reading
The entry_type_hint field is always "private" — no metadata is leaked. Routing is done by link type, not by the hint.
Polls can be flagged by signed-in users for: Spam, Misleading, Off Topic, or Inappropriate.
- Censorship-resistant: Flags are a UI-layer opinion. The poll and all votes remain on the DHT forever. No data is deleted.
- Sybil-resistant: One flag per Flowsta identity per poll (same deduplication as votes).
- Configurable threshold: Polls with >=
FLAG_HIDE_THRESHOLDflags (default 3) are hidden from the default view. Users can toggle "Show flagged" to see them. - Forking developers: Change
FLAG_HIDE_THRESHOLDin the coordinator zome to suit your community size.
This section is for developers (and AIs) who want to fork ProofPoll into a completely different app — a review platform, a task tracker, a social feed, anything. The architecture is app-agnostic; the poll/vote specifics are easy to swap out.
These identifiers must change or your app will conflict with ProofPoll:
| What | Where | Current Value | Change To |
|---|---|---|---|
| Bundle ID | src-tauri/tauri.conf.json |
com.proofpoll.app |
com.yourcompany.yourapp |
| Product name | src-tauri/tauri.conf.json |
ProofPoll |
YourApp |
| Rust crate name | src-tauri/Cargo.toml |
proofpoll |
yourapp |
| npm package name | package.json |
proofpoll |
yourapp |
| Bundled sidecars | src-tauri/tauri.conf.json (externalBin) |
binaries/proofpoll-holochain, binaries/proofpoll-lair-keystore |
binaries/yourapp-holochain, binaries/yourapp-lair-keystore |
| Sidecar resolver calls | src-tauri/src/conductor.rs + src-tauri/src/lair.rs |
sidecar_path("proofpoll-…") |
sidecar_path("yourapp-…") |
| Windows console markers | src-tauri/src/process_ext.rs (SIDECAR_TITLE_MARKERS) |
proofpoll-holochain, proofpoll-lair-keystore |
your sidecar names (or console windows stay visible on Windows) |
| Bundled hApp resource | src-tauri/tauri.conf.json (resources) |
resources/proofpoll_v1_3_happ.happ |
your hApp filename (in step with HAPP_FILE_* in dna.rs) |
| App icons | src-tauri/icons/ |
ProofPoll artwork | your artwork (paths stay the same; regenerate from your source.svg) |
| CI binary download | .github/workflows/build-release.yml |
downloads to binaries/proofpoll-{holochain,lair-keystore}-<triple> |
binaries/yourapp-{holochain,lair-keystore}-<triple> |
| DNA names | dna/*/workdir/dna.yaml |
proofpoll_v1_* |
yourapp_v1_* |
| Network seeds | dna/*/workdir/dna.yaml |
proofpoll-network-v1.* |
yourapp-network-v1.* |
| hApp names | dna/*/workdir/happ.yaml |
proofpoll_v1_*_happ |
yourapp_v1_*_happ |
| hApp role | dna/*/workdir/happ.yaml |
proofpoll |
yourapp |
Then update these Rust constants:
| File | What to change |
|---|---|
src-tauri/src/dna.rs |
APP_ID_V1_* and HAPP_FILE_V1_* constants |
src-tauri/src/commands.rs |
APP_NAME constant (user-facing name in Vault dialogs, backups, exports) and ROLE_NAME constant |
src-tauri/src/migration.rs |
ROLE_NAME constant |
src-tauri/src/dna.rs + src-tauri/src/commands.rs |
"proofpoll" origin string in WebSocket connects (cosmetic, but keep it yours) |
src-tauri/src/lib.rs |
log file_name (proofpoll → your app's log name; cosmetic) |
src/lib/signin.ts |
proofpoll.signin.* storage keys |
Update build scripts (dna/*/build.sh, build-all.sh) — change hApp filenames.
Committed build artifacts: the .wasm/.dna/.happ files under dna/v1.3/workdir/ and src-tauri/resources/ are ProofPoll's canonical builds, committed on purpose - rebuilding a DNA changes its hash, and a changed hash is a different network, so ProofPoll itself must never rebuild them casually. As a forker the opposite applies: delete them and build your own (build-all.sh) so your app gets its own DNA hash and network.
Critical: The network_seed in dna.yaml determines which DHT your app joins. Two apps with the same network seed share a DHT. Always use a unique seed.
Why the sidecar prefix matters: Tauri installs externalBin contents next to the main executable, which on Linux means /usr/bin/. Shipping a sidecar called lair-keystore there collides with any other Tauri/Holochain app that ships the same — dpkg will refuse to install. Prefixing the bundled binaries with your app name keeps your .deb (and .msi) installable alongside any other Holochain Tauri app, including Flowsta Vault and unmodified ProofPoll.
ProofPoll's data model is polls and votes. Replace these with your own.
Integrity zome (latest version, currently dna/v1.3/zomes/polls/integrity/src/lib.rs):
// REPLACE these with your entry types:
pub struct Poll { ... } // → pub struct Review { ... }
pub struct Vote { ... } // → pub struct Rating { ... }
pub struct Flag { ... } // Keep or adapt for your content type
// KEEP these as-is (infrastructure):
pub struct MigratedPoll { ... } // Rename "Poll" to your type but keep the structure
pub struct EncryptedEntry { ... } // Generic — works for any private dataCoordinator zome — replace the zome functions. The patterns are reusable:
create_poll→create_review(same anchor + link pattern)cast_vote→submit_rating(same one-per-agent enforcement pattern)get_all_polls→get_all_reviews(same anchor query pattern)flag_poll→flag_review(same pattern, just rename)
Keep the migration functions and encrypted entry functions — they're generic.
src-tauri/src/commands.rs has mirror types and Tauri commands for each zome function.
Replace the poll/vote/flag Rust structs and commands with your own. The pattern is always:
- Define a response struct (serializable)
#[tauri::command]function that locks the AppWebsocket, encodes payload, callscall_zome, decodes result
Keep these as-is (infrastructure):
AppState,call_zome(),try_reenable_app(),friendly_error(),decode_entry()get_app_status- Identity link commands (
commit_identity_link,get_identity_link, etc.) - Encrypted entry commands (
save_vote_rationale,save_draft_poll, etc. — adapt names) - Migration status commands (
get_migration_status,abandon_pending_votes) - The two backup commands (
build_canonical_backup,decode_record_for_export) — each has onematcharm per entry type; add an arm for every new type you introduce. See Automatic Backups + Cross-Device Recognition below for the full pattern. get_export_datais deprecated and only kept for legacy callers — new forks should ignore it.
Register new commands in src-tauri/src/lib.rs → invoke_handler(tauri::generate_handler![...]).
src/lib/holochain.ts — Replace poll/vote/flag TypeScript types and invoke() wrappers with your own. Keep the identity, migration, and encrypted entry functions.
src/routes/ — Replace the pages:
index.tsx→ Your content list pagecreate/index.tsx→ Your content creation formpoll/[id]/index.tsx→ Your content detail page
Keep as-is:
layout.tsx— Conductor startup, header, migration banner (just rename "ProofPoll")identity/index.tsx— Flowsta identity linking pagedrafts/index.tsx— Encrypted drafts page (adapt for your draft type)src/lib/context.ts— Qwik signals for linked statesrc/lib/sanitize.ts— XSS prevention
src-tauri/src/migration.rs exports data from the previous version and re-creates it on the current version. The source client is clearly marked — change one line to point to your previous version's client field.
Replace the entry types and zome function names with your own. The orchestration pattern (export → create → register mapping → cast → retry loop) is identical for any data model.
The state file name is auto-generated from ACTIVE_APP_ID — no hardcoded strings to update.
ProofPoll uses Flowsta for decentralized identity verification. If you want to use Flowsta in your fork, keep these as-is and just change the client_id. If you want a different identity system (or none), remove them.
For the bigger picture of what Flowsta gives a Holochain app, read For Holochain Developers on docs.flowsta.com first. The short version: agent linking is the foundation, but the same SDK also lights up scope-gated user profile data (display name, username, avatar), encrypted Vault backups, cross-device recognition, document signing via Sign It, and CAL §4.2.1-compliant data export — for ~50 more lines of integration code.
- Register your app at dev.flowsta.com to get a
client_id - Update
.env:VITE_FLOWSTA_CLIENT_ID=your_client_id_here - Clone
flowsta-agent-linkingat../flowsta-agent-linking/(referenced by build scripts) - Keep the
agent_linking_integrityandagent_linkingzomes in yourdna.yaml - Update the
appNameparameter inlinkFlowstaIdentity()calls to your app name (shown in the Vault approval dialog)
Scopes control which Flowsta profile fields your app can access. They are configured per-app at dev.flowsta.com and are shown to the user in the Flowsta Vault approval dialog when they first sign in. The Vault enforces them — it only exposes data fields the user actually approved, regardless of what the app requests at runtime.
ProofPoll requests these scopes:
| Scope | What it provides | Why ProofPoll uses it |
|---|---|---|
openid |
Basic identity (implicit) | Required by all apps — not shown to the user |
did |
Decentralized identifier | Unique identity for sybil resistance |
public_key |
Holochain agent pub key | Links the Vault identity to the DHT entry |
holochain |
Holochain identity attestation | Required for agent_linking zome ceremony |
display_name |
The user's display name | Shown in the app header and voter chips |
username |
The user's @username | Displayed on the identity page |
profile_picture |
Avatar URL | Shown in the app header and voter chips |
The display_name, username, and profile_picture scopes are optional — ProofPoll requests them for a friendlier UI. If your fork has no use for profile data, remove them from your app's scope configuration at dev.flowsta.com.
Configuring scopes for your fork:
- Register your app at dev.flowsta.com and create a new application
- In the app settings, select the scopes your app needs
- Copy your
client_idinto.envasVITE_FLOWSTA_CLIENT_ID - The selected scopes are fetched fresh from the Flowsta API each time a user goes through the linking flow, so scope changes take effect immediately — no app rebuild needed
What the user sees: The Vault approval dialog lists every scope (except openid) in plain language before the user approves. The Vault will only serve those fields on GET /status at localhost:27777.
The Flowsta Vault is a separate desktop app that manages the user's identity. Your app communicates with it via HTTP on localhost:27777. The key design principle: the Vault only needs to be running for the initial identity linking ceremony. After that, your app works independently.
First launch (new user):
- User clicks "Sign in with Flowsta" → calls
linkFlowstaIdentity()from@flowsta/holochainSDK - The SDK sends
POST /link-identityto the Vault → Vault shows approval dialog - Vault signs an attestation with the user's key → returned to your app
- Your app commits the
IsSamePersonEntryon the DHT via theagent_linkingzome - Identity link data is saved locally (
identity-link.json) - Display name and profile picture are fetched from Vault and cached locally (
profile-cache.json)
Subsequent launches (Vault running or not):
- App loads
identity-link.json→ knows user was previously linked - App loads
profile-cache.json→ shows display name and avatar immediately - If Vault is running: refreshes profile and updates cache
- If Vault is closed/locked: cached data is used — everything works normally
- DHT entry is re-created in the background when Vault is available (for peer verification)
Key files for this flow:
src/routes/layout.tsx— Startup link detection, profile cache loading, Vault pollingsrc-tauri/src/commands.rs—get_identity_link,get_cached_profile,save_profile_cachecommandssrc/lib/holochain.ts— TypeScript wrappers for all identity + profile functions
ProofPoll backs up the user's authored data to Flowsta Vault's encrypted local storage every 60 minutes. The backup uses the canonical v1 payload shape — see @flowsta/holochain → Backups on docs.flowsta.com for the full pattern and the canonical payload reference for the on-the-wire schema. Because Vault recognises the shape, it:
- Renders per-entry-type counts on the Your Data page ("12 polls, 38 votes").
- Inlines the plain-English view of each record into the user's Cryptographic Autonomy License §4.2.1 data export — the user can take this file to any compatible Holochain app and use it independently.
Recovery is recognition, not restore. On a fresh install or a new machine there is no restore step. When the user signs in with Flowsta, the app resolves their full linked agent set — every agent key they've used across devices — with a 2-hop walk through the identity link graph (get_my_agent_set). Their polls and votes were never lost: they live on the DHT, authored by those agents, and the app recognises them as the user's own no matter which device created them, re-syncing from the network as the conductor warms up. The Vault backup is the user's portable CAL §4.2.1 export — not the recovery path.
Mechanics:
- Backups work even when the Vault is locked (after first unlock in the session).
- Each backup overwrites the "latest" label by default (single live backup; the 10-per-app capacity is there if needed).
- Only the current user's authored data is backed up (filtered by
action.author == agent_pub_key). - Recognition is read-only — the app reads entries authored by any agent in the user's set; it never re-writes or imports them, so there are no duplicate records or new action hashes.
Key files:
src/routes/layout.tsx— thestartAutoBackup()call.src-tauri/src/commands.rs— two backup Tauri commands at the bottom of the file:build_canonical_backup— builds the canonical payload from zome queries (replaces the legacyget_export_data, which is kept deprecated for backwards compat).decode_record_for_export— decodes an entry into plain JSON for the human-readable view.
get_my_agent_set(commands.rs) — resolves the user's cross-device agent set for recognition; used by the read paths insrc/lib/holochain.ts.
Keeping backups in sync with your data model: when you add a new entry type to your DNA, add one match arm in decode_record_for_export and one in build_canonical_backup's record-collection loop. The plumbing — encryption, storage, the Your Data UI, the CAL export — is provided by Flowsta Vault.
For forks: the two commands above are the entire backup surface area you maintain. Replace Poll / Vote with your own entry types. The appName parameter in layout.tsx controls how your app appears in the Vault's Your Data page.
| Value | Location | Purpose |
|---|---|---|
VITE_FLOWSTA_CLIENT_ID |
.env |
Identifies your app to Flowsta |
http://127.0.0.1:27777 |
layout.tsx, identity/index.tsx, commands.rs |
Flowsta Vault IPC server |
@flowsta/holochain |
package.json, layout.tsx, identity/index.tsx |
Flowsta SDK for identity linking |
flowsta-agent-linking |
build.sh, dna.yaml |
Reusable Rust crate for DHT identity attestations |
"ProofPoll" in linkFlowstaIdentity() |
layout.tsx, identity/index.tsx |
App name shown in Vault approval dialog |
Port 5174 |
vite.config.ts |
Dev port (avoids conflict with Vault on 5173) |
Port 4466 |
conductor.rs |
Admin WS port (avoids conflict with Vault on 4455) |
This app includes a complete migration system for upgrading between DNA versions. This section explains how it works and how to add your own versions.
Holochain DNA versions with different integrity zomes (new entry types, changed validation) get different DNA hashes, which means a new DHT. Old data lives on the old DHT. Each user runs their own conductor — there's no central server to orchestrate the upgrade.
When a user upgrades to a new version:
- Install new version alongside the old (both stay installed)
- Export user's authored content and actions from the old DHT
- Re-create content on the new DHT (entries get new action hashes)
- Publish migration mappings — a
MigratedPollentry linked from a deterministic migration anchor maps old hashes to new hashes - Re-cast actions (votes, etc.) where the target content's mapping exists
- Queue pending actions for content whose authors haven't upgraded yet
- Background retry — every 60 seconds, check if new mappings appeared and retry
Other users discover the mappings via get_links on the migration anchor. As more users upgrade, the new DHT fills up via gossip.
| Tier | When | Migration Needed? | Example |
|---|---|---|---|
| 1. Coordinator-only | Bug fixes, new queries, new link traversals | No — use admin.updateCoordinators() |
Fix a query bug |
| 2. Additive integrity | New entry types, new link types | Yes — new DNA hash, new DHT | Adding EncryptedEntry (v1.3) |
| 3. Breaking integrity | Changed validation, restructured entries | Yes — with data transformation | Restructuring Poll fields |
~70% of real-world upgrades are Tier 1 (no migration needed). Tier 2 is what ProofPoll demonstrates across v1.0→v1.3. Tier 3 follows the same pattern but adds a transformation step.
During migration, polls from older versions remain visible and functional:
get_all_pollsqueries ALL installed versions and deduplicates using migration mappings- Each poll carries a
dna_versionfield so votes and flags are routed to the correct cell - If a poll author hasn't migrated, their poll stays on the old DHT — votes cast on it go to the old cell
- Once the author migrates, the old copy is hidden and the new copy takes over
No votes are ever lost. Users on different versions can still interact with content on the version where it lives.
| File | Purpose |
|---|---|
src-tauri/src/migration.rs |
Migration orchestration (export, import, retry loop). Source client clearly marked for forkers |
src-tauri/src/dna.rs |
Multi-version install, AppWebsocket setup per version |
src-tauri/src/commands.rs |
get_all_polls multi-version merge with chained dedup |
dna/v1.3/zomes/polls/coordinator/src/lib.rs |
register_migrated_poll, get_migration_mapping zome functions |
dna/v1.3/zomes/polls/integrity/src/lib.rs |
MigratedPoll entry type, MigrationIndex link type |
- Create
dna/vX.Y/— copy the latest version, updatenetwork_seedindna.yaml - Add your integrity changes — new entry types, link types, validation
- Update coordinator — keep all migration + encrypted entry functions, add your new zome functions
- Update
src-tauri/src/dna.rs— addAPP_ID_VX_Y,HAPP_FILE_VX_Y, updateACTIVE_APP_ID, addapp_client_vX_YtoAppState - Update
src-tauri/src/migration.rs— change the source client field (one line, clearly marked with// FORKING) - Update
src-tauri/src/commands.rs— add your previous version to theolder_versionsarray inget_all_polls - Update
build-all.sh— add the new build step - Test — create data on the old version, upgrade, verify migration completes and all content is visible
The migration state file is auto-generated from ACTIVE_APP_ID — no hardcoded strings to update.
During a migration all DNA cells are active simultaneously. get_all_polls queries every installed version and deduplicates:
- Collect migration mappings from ALL versions into one set (chains across multi-hop migrations)
- Query each version — skip any poll whose hash appears in the migrated set
- Return merged list — each item carries
dna_versionso votes and flags are routed to the correct cell
This means content is never missing from the UI, even if only one user on the network has upgraded so far.
- First user on new version: Their own content migrates fine. References to others' content go to pending (retried every 60s).
- Content author never upgrades: Content stays on the old DHT and remains visible. Actions (votes) cast on it go to the old cell. Users can "abandon pending votes" to clean up.
- Crash during migration: State file is written after each entry. Restart picks up where it left off.
- Fresh install (no previous version): Installs latest directly. No migration needed.
These files work for any Holochain + Tauri app with zero or minimal changes:
| File | What It Does | Change Needed |
|---|---|---|
src-tauri/src/conductor.rs |
Starts lair-keystore + holochain conductor, waits for readiness, health monitoring | Change ports if running multiple apps |
src-tauri/src/lair.rs |
Lair keystore init, socket management, passphrase | None |
src-tauri/src/crypto.rs |
Encrypt/decrypt via lair's xsalsa20poly1305 crypto_box | None |
src-tauri/src/dna.rs |
Multi-version DNA install, AppWebsocket per version, signing credentials with CellDisabled recovery | Change app IDs and hApp filenames |
src-tauri/src/migration.rs |
Migration state machine, export/import/retry pattern. Auto-versioned state file | Change entry types, zome names, and source client field |
src/lib/context.ts |
Qwik signals for linked/display state | None |
src/lib/sanitize.ts |
XSS prevention for user content | None |
src/routes/identity/ |
Flowsta identity linking UI | None (if using Flowsta) |
Holochain apps need a bootstrap server for peer discovery, a signaling
server for NAT traversal, and an Iroh relay for connections that NAT
defeats. As of Holochain 0.6.1 all three are handled by the same binary
(kitsune2-bootstrap-srv ≥ v0.4.1).
The bootstrap / signal / relay URLs and an optional bootstrap auth
material are read at compile time from env vars by
src-tauri/src/conductor.rs — set them
before cargo tauri build (locally) or as GitHub Actions secrets
(in CI). Three deployment modes:
Don't set any env vars. The source defaults take effect:
| Var | Default | Notes |
|---|---|---|
PROOFPOLL_BOOTSTRAP_URL |
https://dev-test-bootstrap2.holochain.org |
Holochain's public dev bootstrap. No SLA. |
PROOFPOLL_SIGNAL_URL |
wss://dev-test-bootstrap2.holochain.org |
Same host. |
PROOFPOLL_RELAY_URL |
https://use1-1.relay.n0.iroh-canary.iroh.link./ |
Iroh's public canary relay. |
PROOFPOLL_AUTH_MATERIAL |
(unset) | No auth (open bootstrap). |
cargo tauri dev and casual experimentation work out of the box.
Run your own kitsune2-bootstrap-srv (see the official Holochain guide:
Running Network Infrastructure)
and set:
PROOFPOLL_BOOTSTRAP_URL=https://your-bootstrap.example.com \
PROOFPOLL_SIGNAL_URL=wss://your-bootstrap.example.com \
PROOFPOLL_RELAY_URL=https://your-bootstrap.example.com./ \
cargo tauri buildThe trailing-dot+slash on relay_url (./) is required canonical form.
Once Flowsta opens bootstrap-as-a-service, register your app at
https://dev.flowsta.com, get a client_id, then set:
PROOFPOLL_BOOTSTRAP_URL=https://bootstrap.flowsta.com \
PROOFPOLL_SIGNAL_URL=wss://bootstrap.flowsta.com \
PROOFPOLL_RELAY_URL=https://bootstrap.flowsta.com./ \
PROOFPOLL_AUTH_MATERIAL=<base64url of `{"client_id":"flowsta_app_..."}`> \
cargo tauri buildPROOFPOLL_AUTH_MATERIAL is opaque bytes sent verbatim to the
bootstrap's /authenticate endpoint. The kitsune2 client caches the
returned token and re-auths on 401 automatically. Without the material,
Flowsta's bootstrap returns 401 and peering fails.
The included .github/workflows/build-release.yml
reads PROOFPOLL_BOOTSTRAP_URL, PROOFPOLL_SIGNAL_URL,
PROOFPOLL_RELAY_URL, and PROOFPOLL_AUTH_MATERIAL from repository
secrets and exposes them to the build. If none are set (e.g. a fresh
fork), the release falls back to the development defaults above.
A DHT is only as durable as its peers. node/ deploys a small
always-on conductor (Docker, fits a GCE e2-micro) that keeps your app's
network alive when no desktop user happens to be online: setup.sh
provisions the box, docker-compose.yml runs the edge-node image, and
install-happ.sh + proofpoll-happ-config.json install the hApp with the
right network seed.
Two things to keep straight:
- Version pinning. The node config names a specific hApp file, app id,
and network seed. Keep them in step with
ACTIVE_APP_IDinsrc-tauri/src/dna.rswhenever you migrate DNA versions - a node pinned to an old version sits on the wrong (empty) network doing nothing. - Forks: rename the container, volume, hApp file, app id, and seed in
all four
node/files to your app's values (they are ProofPoll's).
You do not need any of this to build, run, or fork ProofPoll. The release workflow signs only when the relevant secrets are present; a fork with no signing secrets produces unsigned installers and the release notes say so. Unsigned bundles run fine — the OS just shows a first-launch warning (Windows SmartScreen / macOS Gatekeeper).
Signing is its own thing per platform, and none of the certificates,
private keys, or credentials live in this repo — they are all read from
GitHub Actions secrets at build time (see the env: block in the build
job). Cloning or forking the repo gives you the signing plumbing, never the
credentials; you cannot sign as anyone but yourself.
| Platform | What you need | Roughly |
|---|---|---|
| Linux | Nothing. .deb/.rpm/.AppImage are unsigned by convention. |
Free |
| macOS | An Apple Developer account ($99/yr) for a "Developer ID Application" certificate, plus notarization via an App Store Connect API key. Without it, users must right-click → Open once. | $99/yr |
| Windows | A code-signing certificate from a CA — OV (Organization Validation) or EV (Extended Validation). EV clears SmartScreen reputation immediately; OV warms up over time/downloads. Issued by services such as SSL.com, DigiCert, Sectigo, or Azure Trusted Signing. Modern certs are stored in an HSM/cloud signer, not a local .pfx. |
~$100–600/yr |
The official ProofPoll build signs Windows via SSL.com eSigner (cloud
HSM, driven by src-tauri/scripts/sign-windows.ps1
through Tauri's windows.signCommand) and macOS via a Developer ID cert +
notarization. A fork can use any provider — swap the sign step/script for
your CA's tool.
Add these as repository secrets (Settings → Secrets and variables → Actions). Any you leave unset simply disables that platform's signing step:
| Secret | Platform | Purpose |
|---|---|---|
APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD |
macOS | base64 of your Developer ID .p12 + its password |
APPLE_SIGNING_IDENTITY |
macOS | e.g. Developer ID Application: Your Name (TEAMID) |
APPLE_API_KEY / APPLE_API_KEY_PATH / APPLE_API_ISSUER |
macOS | App Store Connect API key for notarization |
ESIGNER_USERNAME / ESIGNER_PASSWORD / ESIGNER_CREDENTIAL_ID / ESIGNER_TOTP_SECRET |
Windows | SSL.com eSigner credentials (replace with your CA's equivalents if not using SSL.com) |
And one repository variable (not a secret — it's public anyway):
| Variable | Purpose |
|---|---|
WINDOWS_PUBLISHER |
The publisher name shown in your release notes when Windows signing is on (e.g. your company name). Left unset, signed builds just say "code-signed"; unset and unsigned, the notes say "unsigned". |
Do not advertise a build as signed by a publisher whose certificate you don't control. The workflow derives the release-notes signing line from the actual secrets/variable above precisely so a fork never inherits a false "verified publisher" claim.
ProofPoll/
├── dna/ # Holochain DNA source
│ ├── zomes/polls/ # v1.0 zomes
│ │ ├── integrity/src/ # Entry types, validation
│ │ └── coordinator/src/ # Zome functions (CRUD)
│ ├── workdir/ # v1.0 manifests (dna.yaml, happ.yaml)
│ ├── build.sh # v1.0 build script
│ ├── v1.1/ # v1.1 DNA (+ flags, migration)
│ ├── v1.2/ # v1.2 DNA (+ public/anonymous polls)
│ └── v1.3/ # v1.3 DNA (+ encrypted private data)
│ ├── zomes/polls/
│ │ ├── integrity/src/ # EncryptedEntry, VoteToRationale, AgentDrafts
│ │ └── coordinator/src/# Encrypted entry CRUD + existing functions
│ ├── workdir/ # v1.3 manifests
│ └── build.sh # v1.3 build script
├── src-tauri/ # Tauri v2 Rust backend
│ ├── Cargo.toml # Rust dependencies
│ ├── tauri.conf.json # App config (name, bundle ID, ports)
│ ├── resources/ # Built .happ bundles (v1.0 through v1.3)
│ └── src/
│ ├── commands.rs # Tauri commands (app + flags + encrypted entries + migration)
│ ├── conductor.rs # Conductor lifecycle management
│ ├── crypto.rs # Lair-based encryption (xsalsa20poly1305)
│ ├── dna.rs # Multi-version DNA install + WebSocket setup
│ ├── migration.rs # DNA migration orchestration
│ ├── lair.rs # Lair keystore management
│ ├── process_ext.rs # Windows console-window hygiene (sidecar markers)
│ ├── sidecar.rs # Sidecar path resolution
│ └── lib.rs # App setup, command registration, startup
├── src/ # Qwik TypeScript frontend
│ ├── lib/
│ │ ├── holochain.ts # Zome call wrappers + types
│ │ ├── context.ts # Qwik context signals
│ │ └── sanitize.ts # Input sanitization
│ └── routes/
│ ├── layout.tsx # Header, conductor status, migration banner
│ ├── index.tsx # Content list (+ flag filtering)
│ ├── poll/[id]/ # Content detail (+ flag + vote rationale)
│ ├── create/ # Content creation form (+ save as draft)
│ ├── drafts/ # Encrypted draft polls page
│ └── identity/ # Flowsta identity linking
├── node/ # Always-on node deployment (Docker; see Network Infrastructure)
│ ├── setup.sh # Provision a small VM (Docker + compose)
│ ├── docker-compose.yml # Edge-node conductor container
│ ├── install-happ.sh # Install the hApp into the running node
│ └── proofpoll-happ-config.json # App id + network seed the node joins (keep in step with dna.rs)
├── .env # VITE_FLOWSTA_CLIENT_ID
├── build-all.sh # Build all DNA versions
├── package.json # Node dependencies
└── vite.config.ts # Vite + Qwik config (dev port 5174)
MIT