DAOs, proposals and votes on the Canton Network, signed by keys only their members hold. Live on https://syncvotes.com (MainNet), https://test.syncvotes.com (TestNet) and https://dev.syncvotes.com (DevNet). How to use it, the voting rules and the trust model are in the docs at syncvotes.com/docs.
- Keys and parties. Each user is an external party hosted on the app's validator. The key comes from a 12-word phrase and never leaves the browser. The server prepares each transaction; the browser decodes it, checks it is what the page asked for, and signs.
- The model. A Daml package,
syncvotes, indaml/src/SyncVotes/. The app's own party, the provider, co-signs every contract, counts the ballots and carries out what passed. The ledger checks every ballot and every change it is handed. - Money. Everything costs network traffic. Users and DAOs pay in by sending Canton Coin to the
validator's party with a memo (
syncvotes:<dao id>orsyncvotes:<key fingerprint>); each transaction is charged its traffic, net of the rewards it earns back. SyncVotes is a featured app, and its activity markers and app rewards pay back part of that traffic. - Privacy. Whoever runs the validator can read every DAO it hosts, private ones and secret ballots included. A community that must keep its DAOs to itself runs SyncVotes on its own validator, as below.
-
A Splice validator on the network you want, onboarded, with coin to buy traffic, run by its own
start.shon a Linux server with Docker. -
A domain for the site, pointed at that server. Cloudflare in front is supported and advised.
-
On your machine: this repository, Docker, and a Docker context for the server:
docker context create syncvotes-mainnet --docker host=ssh://<user>@<server>
The image is built on the server; nothing but Docker and the validator lives there.
-
Settings. Copy
.env.exampleto<network>.env(mainnet.env,testnet.envordevnet.env) and fill it in; every setting is in the table below. Use long random values for every secret. The env files are ignored by git and never leave your machine. Compose insists onPROVIDER_PARTY, which step 3 prints: until then set it topending. -
Sign-in for the ledger. The app, its backend and the validator trust one Keycloak realm,
canton, which this compose project runs and serves under/authon your domain. Start it first:docker --context syncvotes-<network> compose --env-file <network>.env up -d keycloak caddy
Then point the validator's own
.envat it (AUTH_URL,AUTH_JWKS_URL,AUTH_WELLKNOWN_URL=https://<domain>/auth/realms/canton…, the audiences and client ids from your env file), pinPARTICIPANT_DB_NAMEthere, and restart the validator withstart.sh … -a. -
The app's party and ledger user. Once per validator:
VALIDATOR_CLIENT_SECRET=$(grep ^KC_VALIDATOR_SECRET= <network>.env | cut -d= -f2-) \ docker --context syncvotes-<network> compose --env-file <network>.env run --rm --no-deps \ -e VALIDATOR_CLIENT_SECRET app node scripts/setup-participant.mjs
It allocates the provider party
syncvotes-app-providerand gives the app's ledger userParticipantAdmin,CanReadAsAnyParty,CanExecuteAsAnyPartyandCanActAsthe provider, and never the right to act as a user. Put the party it prints inPROVIDER_PARTY, and the validator's own party inPAYEE_PARTY. -
Start.
pnpm deploy:<network>
That is
compose buildandup -dagainst the context, from a clean git tree only; the commit is baked in. At startup the app uploads its Daml package and reads the ledger.https://<domain>/versionthen answers with the commit and the package id. -
Check. The app log says
Daml package … is on the participant. Make sure the participant has also vetted it:POST /v2/package-vettingon the ledger API listssyncvoteswith the version you deployed. An upload can succeed and still not be vetted, for example next to an older, incompatible package of the same name. -
Who pays. For a community of your own,
BILLING_FACTOR=0makes every transaction free to members and lets the validator pay.INVITE_CODESkeeps sign-up to your people.
By default Caddy binds PUBLIC_IP:80/443 and gets its own certificates. Where another proxy
already holds those ports:
- nginx on the host:
CADDY_HTTP_BIND=127.0.0.1:8085,CADDY_SITE=http://<domain>,CADDY_TRUSTED_EXTRA=127.0.0.0/8, and an nginx site that terminates TLS and forwards to127.0.0.1:8085. Give itproxy_buffer_size 32k(andproxy_buffers 8 32k): the app's headers pass nginx's default 4 KB on some pages, and nginx answers 502. - traefik: the same binds, plus
TRAEFIK=true,PROXY_NETWORK=<traefik's network>,PROXY_NETWORK_EXTERNAL=true,CADDY_TRUSTED_EXTRA=private_ranges. Compose sets the router labels, including an allow-list of Cloudflare's ranges.
Behind Cloudflare, let only Cloudflare reach the server: CADDY_ALLOWED_PEERS where Caddy faces
the internet, the traefik allow-list above, or a geo on $realip_remote_addr in nginx. Caddy
reads the visitor's address from Cloudflare's headers and passes the app X-Client-Ip, which
rate limits use.
Caddy's config is inline in compose.yaml; after changing it, run
compose up -d --force-recreate caddy. Keycloak's admin console, its master realm and the
account console are not served publicly: reach them over an SSH tunnel to keycloak:8080, as
KC_ADMIN_NAME.
| Setting | What it is |
|---|---|
APP_DOMAIN |
The site's domain. |
PUBLIC_IP |
The address Caddy binds to when it has ports 80 and 443 itself. |
NETWORK |
MainNet, TestNet or DevNet: shown to users. |
NETWORKS |
The sites the network switcher offers: Name=https://url,…, this one included. |
SCAN_URL |
The network's public Scan: coin rules, rounds, prices, featured apps. |
VALIDATOR_NETWORK |
The validator's Docker network, which the app joins. Default splice-validator_default. |
WALLET_USER_NAME |
The validator operator's wallet login; equals the validator's PARTY_HINT. |
PROVIDER_PARTY |
The app's party, printed by setup-participant.mjs. |
PAYEE_PARTY |
Where users pay in: the validator's own party, whose wallet buys traffic. Empty means the provider. |
DEPOSITS_SINCE |
ISO time from which memo payments count. Set it when accounts start afresh. |
BILLING_FACTOR |
What users pay, as a multiple of the traffic's net cost: 1 covers it, below 1 the validator subsidises, 0 is free. |
BILLING_FLOOR |
The least charged, as a fraction of the network's traffic price, even when rewards cover everything. 0 is no floor. |
MARKERS |
true records featured-app activity markers, when the provider holds a featured app right. |
INVITE_CODES |
Comma-separated codes needed to sign up. Empty is open sign-up. |
LEDGER_API_AUDIENCE, VALIDATOR_AUDIENCE |
Token audiences of the participant's ledger API and the validator's API. |
WALLET_UI_URL, CNS_UI_URL |
Where the validator's own wallet and name-service UIs live, for their login clients. |
KC_ADMIN_NAME, KC_ADMIN_PASSWORD |
Keycloak's administrator. |
KC_DB_PASSWORD |
Keycloak's database. |
KC_APP_SECRET, KC_VALIDATOR_SECRET |
Client secrets of the app and of the validator backend. |
KC_APP_USER_ID, KC_VALIDATOR_USER_ID, KC_WALLET_USER_ID |
Fixed ids of the realm's users, which become ledger user names. |
KC_WALLET_USER_PASSWORD |
The validator operator's wallet login password. |
CADDY_HTTP_BIND, CADDY_HTTPS_BIND, CADDY_SITE, CADDY_TRUSTED_EXTRA |
Behind another proxy (see Proxies). |
TRAEFIK, PROXY_NETWORK, PROXY_NETWORK_EXTERNAL |
Behind traefik. |
CADDY_ALLOWED_PEERS |
Where Caddy faces the internet: the only peers it serves, e.g. private_ranges and Cloudflare's ranges. Empty is anyone. |
pnpm deploy:<network> again. Nothing goes down: Caddy holds requests until the new app answers.
The Daml package keeps its name, syncvotes, for good: renaming it would hide every existing DAO.
Each release is a Smart Contract Upgrade of the one before, checked by the compiler:
- Bump
versionindaml/daml.yaml. - Point
upgrades:at the previous DAR and keep that DAR indaml/upgrades/. - Update the
@daml.js/modelalias inpackage.jsonto the new version.
An upgrade may change a choice's body, add templates and choices, and add Optional fields at the
end. It may not remove or retype fields or choices, move a template to another module, or change
signatories and observers. After deploying, check the vetting on every network (step 5).
There is no local ledger; what runs locally is the type-checker, the linter and the build.
Building the DAR needs dpm, which is x86-64 only (Rosetta on Apple silicon).
pnpm daml:codegen # builds the DAR and writes the bindings to daml.js/
pnpm i
pnpm check
pnpm lint
pnpm exec vite buildRun codegen and pnpm i again after every change to the Daml side.