# Rob's Rules DAO — full documentation > Digital Robert's Rules of Order on Vocdoni, with an optional Aragon path on Ethereum. Source: https://documentation.robsrulesdao.com/llms.txt --- # Overview Rob's Rules DAO is a digital implementation of Robert's Rules of Order. A **Chair** publishes a proposal and names who may take part. **Voting members** discuss it, request amendments, and cast a Yes/No vote. Every create, second, amend, and vote is recorded on the **Vocdoni blockchain**. That Vocdoni record is the chain confirmation for every organization. Some customers also send a passed vote to **Aragon** on Ethereum; that path is official only after council majority in the Aragon App. These docs start with how the two live roles work together, then go deeper for members, chairs, the admin who onboards a new company, and developers. :::note Two apps Members live in the **interaction** app (the `*-I` domain, for example `rrd-i.robsrulesdao.com`). The Chair creates proposals in the **creation** app (`*-C`), then uses the interaction app on a **desktop** to second, approve amendments, and — if configured — execute on Aragon. ::: ## The two live roles | | Voting member | Chair | | --- | --- | --- | | Who they are | Anyone the Chair put on the **census** (wallet address or email) | The MetaMask wallet stored as `chairAccount` for that company | | Where they work | Interaction app, phone or desktop | Creation app plus interaction app on a **full PC screen** | | How they sign in | MetaMask, Coinbase Wallet / Base in-app browser, or **Sign in with Email** | **MetaMask only** — never email sign-in | | What they may do | Discuss, request amendments, thumbs-up pending text, vote once per iteration | Build the census, set duration and amendment window, second, approve or decline amendments, optionally Execute on Aragon | | What they may not do | Second a proposal, approve an amendment, or execute on Aragon | Suggest an amendment or cast a Yes/No vote on a proposal they chair | The same wallet can be Chair for one organization and a voting member on another. Authority is per proposal, from `organizationName` on the election — not a global admin flag. ## How a proposal actually moves Members wait on the Chair at two gates. Until those gates open, Amend and Vote stay disabled even if you can already see the proposal and talk about it. 1. **Chair creates** the proposal in the creation app (census, text, duration, amendment window). 2. **Members can see it and discuss it**, but they **cannot amend or vote** until the Chair **seconds** it. 3. **Chair seconds** — that mints a new Vocdoni election. The amendment window (if any) and voting schedule now apply. 4. **Members request amendments** while the window is open, and may thumbs-up a pending request. 5. **Chair approves and seconds** an amendment (or declines it). Approve-and-second mints another election. **Everyone must vote again** on the new text. 6. **Members vote** only after the amendment window has closed (or immediately if the Chair set the window to `0%`). 7. **Vocdoni** holds the deliberative record. **Aragon Execute** is Chair-only and only for companies that have DAO addresses in config. ## Where each person should read next - **Voting members** — [What you can do](/members), [Waiting on the Chair](/members/waiting), [Screens and layout](/members/screens), [Open in a wallet](/members/wallets). - **Chairs** — [Chair responsibilities](/chair), then census, amendment window, and Aragon. - **Admins** who onboard a company — [Onboard a company](/admin). - **Developers** — [Architecture](/architecture) and [Quickstart](/quickstart). ## Where to go next --- # What members can do If the Chair put your wallet or email on the census, you are a **voting member** for that proposal. You work in the **interaction** app (the `*-I` hostname). You do not need ether to discuss, amend, or vote. A Vocdoni vote may ask for one **message signature** in MetaMask or Coinbase Wallet; that is not a gas transaction and does not move tokens. You never second a proposal, never approve someone else's amendment, and never Execute on Aragon. Those are Chair actions on a desktop. ## Sign in - **MetaMask** in a regular browser, or the **in-app browser** inside MetaMask or the Base / Coinbase Wallet app. See [Open in a wallet](/members/wallets). - **Sign in with Email** — a six-digit code proves you own the inbox. The backend derives the same Ethereum address the Chair stored in the census. If you signed in with email, you can still do every member action below. You cannot become Chair through email. ## Capabilities Once you are in the census and signed in, you can: - See the list of proposals you are eligible for - Open a proposal and read its description, audit ids, and vote counts - Join **discussion** (General, Speaking For, Speaking Against, Point of Information, Point of Order) - **Request an amendment** — only after the Chair has **seconded** the proposal, and only while the amendment window is open - Give a **thumbs-up** on a still-proposed amendment (one per member) until the Chair seconds that amendment - **Vote Yes or No** — only after the Chair has seconded, and only after the amendment window has closed (or immediately if the Chair set the window to `0%`) - Vote **once per iteration**. After you vote, Vote Status updates; you still see discussion. A new vote is possible only when the Chair approves an amendment and a new election appears - Use a **phone or a desktop**. Layout changes; member actions stay available. See [Screens and layout](/members/screens) ## What stays locked until the Chair acts | You want to… | You wait for the Chair to… | | --- | --- | | Amend or vote on a new proposal | **Second** it | | Have your amendment text become the motion | **Approve and second** that amendment | | Vote after people have been amending | Let the **amendment window** expire (the Chair set that percent when creating) | | Vote again on changed text | Approve and second an amendment (that resets the vote) | The next page walks those waits in order. ## Discussions in brief Comments are per iteration. When the Chair seconds the proposal, or approves and seconds an amendment, the discussion thread **resets** because a new election is now the motion. Read **General** after those events — the Chair posts guidance there. Each category stops at **20** comments (a warning appears at 15). Limits are independent per category. ## Where to go next --- # Waiting on the Chair A voting member cannot move a proposal forward alone. Robert's Rules here is a **chain of Vocdoni elections**. The Chair's second (and later, the Chair's second of your amendment) is what mints the next election. Until that happens, the interaction app shows the proposal but keeps Amend and Vote disabled. ## Gate 1 — the proposal must be seconded After the Chair **creates** a proposal, you can usually **see it and discuss it**. You **cannot** submit an amendment and you **cannot** vote. Wait for the Chair to press **Second** on a desktop. Seconding creates a new election (`voting`) that reuses the original end date. From that moment: - Amendment requests are allowed **if** the amendment window is still open (and was not set to `0%`) - Voting is still **off** until that window expires — unless the Chair set `0%`, in which case Vote can enable as soon as the proposal is otherwise eligible If the original proposal has already **closed**, Vocdoni will refuse a second (the end time is in the past). There is nothing a member can do; the Chair must create a new proposal. ## Gate 2 — your amendment needs the Chair's second You write amendment text in the center column (desktop) or the **Main** tab (phone). Other members may **thumbs-up** it. That count is informal. It does **not** incorporate the text. Only the Chair can **Approve & Second** or **Decline**. Approve-and-second: - Creates a new Vocdoni election that includes the amendment - **Resets all votes** — everyone, including people who already voted, must vote again - Hides thumbs-up on the new card (the old card still shows the final count — use **Show proposal chain**) Until the Chair does that, keep discussing and, if you have not already, add your one thumbs-up. Do not expect Vote to reflect the new wording yet. :::tip Watch General discussion When an amendment lands, the Chair should say in **General** whether more amendments are still being accepted and when voting reopens. If you only watch the Vote button, it is easy to miss a reset. ::: ## Gate 3 — the amendment window, then Vote The Chair chose how long amendments stay open as a **percent of duration** (0–75%, default 50%). While that window is open: - You may submit amendment requests (after Gate 1) - **Vote stays disabled**, with remaining time shown When the window closes, Submit disables and Details shows **Closed**. Vote then enables until the proposal duration ends. If the Chair chose **`0%`**, there are no amendments; Vote can open as soon as the proposal is seconded and otherwise eligible. The Chair can still approve or decline requests that arrived **before** the deadline. ## After you vote You cannot vote again on that same election. You still see the proposal. A new ballot appears only if the Chair later seconds an amendment (Gate 2), which starts a new iteration. ## Where to go next --- # Screens and layout The interaction app changes layout at **991px** wide. Member actions (list, discuss, amend, vote) work on **both** sizes. Chair actions never appear on a phone. ## Small screen — three tabs On a phone or a narrow window you get a **3-tab** bar: | Tab | What it holds | | --- | --- | | **Proposals** | Vertical list of proposals you are eligible for. This is the **default** tab. | | **Main** | Discussion, the amendment request form, thumbs-up, and the Vote button | | **Details** | Description, audit ids, vote counts, amendment-window status, list of amendment requests | **Main** and **Details** stay disabled until you pick a proposal on the Proposals tab. Selecting a row updates both of the other tabs; switch between them as you work. There is no three-column notebook on a phone. You are not missing Chair controls — they are intentionally omitted on a small screen. ## Large screen — three columns On a full PC the app is a three-column workspace: | Column | What it holds | | --- | --- | | **Left** | Navigation and the proposal list | | **Middle** | Active work: discussion, voting, amendment request, thumbs-up | | **Right** | Context and audit: description, timeline, counts, Chair actions if you are Chair | ### Opening and closing the right column (members) If you are a **voting member** on desktop, the right-hand **Details** column often starts as a thin rail labeled **Details & Proposal Info**. Click the rail (or the ‹ control) to **open** the full details column. Use the control on the open column to **collapse** it again so the middle workspace is wider. If you are the **Chair** for that proposal, Details stays **expanded**. Chair buttons (Second, Approve, Decline, Execute) live there and must not be hidden on a phone. Desktop also has a small **zoom** control at the top right (− / +) if the three columns feel tight. ## What does not change with screen size - You still need to be on the census and signed in - You still wait on the Chair to second before amend/vote - Discussion categories and comment caps are the same - Email sign-in and wallet sign-in both work on phone and desktop ## Where to go next --- # Open in a wallet You can run the **interaction** app in an ordinary browser with MetaMask, **or inside a wallet that has its own web browser**. MetaMask and the **Base / Coinbase Wallet** apps both include that in-app browser. Open your organization's `*-I` URL there (for example `https://rrd-i.robsrulesdao.com`) so the wallet and the page stay in one place. Email sign-in still works if you prefer not to use a wallet at all. ## MetaMask 1. Install the MetaMask extension (desktop) or the MetaMask mobile app. 2. On mobile, use MetaMask's **Browser** (the globe / dapp browser), not Safari or Chrome alone. 3. Paste the interaction URL and connect when the app asks. 4. Confirm you are using the account the Chair put on the census (or the email-derived address, if you use email instead). A deliberative **vote** may prompt a **signature**, not a send-transaction. You do not need ETH in the wallet for discuss / amend / vote on Vocdoni. ## Base / Coinbase Wallet Fort Worth DAO and other customers may use **Base**. Members can open the same interaction URL in the **Coinbase Wallet** (Base) in-app browser, then connect. The page is still the Rob's Rules interaction app; the wallet is only the signer. Chair **Execute on Aragon** is different: that is a Chair-only, desktop, gas-paying transaction on the customer's network (Polygon for RRD, Base for Fort Worth DAO). Members never do that step, and an email-derived key cannot pay gas. ## Sign in with Email If you do not want a wallet: 1. Choose **Sign in with Email**. 2. Enter the **same** address the Chair put in the census. 3. Enter the **6-digit code** (single use, expires in 10 minutes). The backend derives `HMAC-SHA256(EMAIL_WALLET_SECRET, normalizedEmail)`. That address must match the census or you will look like a non-member. :::warning Chair wallets The Chair **cannot** use email sign-in. Creating, seconding, approving amendments, and Execute require a self-custodied MetaMask (or equivalent injected) wallet. ::: ## Networks you can ignore as a member Vocdoni deliberation does **not** require Polygon or Base. If a network banner appears, it is for Chair Execute. You can discuss and vote while MetaMask sits on another chain, as long as you can still **sign a message**. ## Where to go next --- # Discussions Anyone in the census can talk about a proposal. Comments live on IPFS through the backend, not on Vocdoni. Threads are **per iteration**: when a proposal is seconded, or when an amendment is approved and seconded, discussion resets because a new election is now the active motion. ## Categories Each category has its own comment list and its own cap: - General - Speaking For - Speaking Against - Point of Information - Point of Order The UI defaults to **General**. Category buttons show live counts (capped at 99 for display). Users only see comments from the selected type. ## Comment limits To keep IPFS pins from ballooning: | Threshold | Behavior | | --- | --- | | 15 comments in a category | Warning: discussion is getting large; consider an external forum | | 20 comments in a category | Hard stop. Textarea and submit disable. Independent per category | Users cannot accidentally post past the cap. ## How chairs should use General After an amendment lands, members should read **General** for the chair's instructions on whether amendments are still being accepted and when voting reopens. Voting reset is easy to miss if you only watch the Vote button. ## Where comments are stored New comments go through `POST /api/store-discussion`. Retrieval uses the backend, not a Pinata key in the browser. See [IPFS backend](/ipfs). ## Where to go next --- # Chair responsibilities The Chair is the moderator of the Robert's Rules process for one organization. Your wallet is the `chairAccount` in `customer-config.ts`. You **must** use **MetaMask** (or another injected browser wallet). Email sign-in is for voting members only. Chair work that changes the motion — **Second**, **Approve / Decline** amendments, **Execute on Aragon** — is **desktop only** in the interaction app (width above 991px). Create the proposal itself in the **creation** app (`*-C` domain). ## Responsibilities - Build and submit the **census** (addresses and/or emails) - Set proposal **duration**, **amendment window** (0–75%), and deliberative gates (`minParticipation`, `minSupport`) - **Second** a created proposal so members may amend (if allowed) and later vote - Read member thumbs-up counts; you cannot add a thumbs-up - **Approve and second** or **Decline** amendment requests, including those filed before the window closed - State in **General** discussion when amendments are no longer accepted and when voting reopens - For Aragon customers, **Execute** after gates pass — that **opens** a Token Voting proposal; official only after council majority in the Aragon App ## What you must not do - **Suggest an amendment** — members propose text; you accept or decline - **Vote Yes/No** on a proposal you chair — Vote stays disabled and submission is blocked - Run Second / Approve / Execute from a **phone** — those controls are omitted on small screens - Second after the parent election has **closed** — Vocdoni rejects a past `endDate`. Use a short duration in tests and second while time remains ## Capabilities versus members You can do everything a member can **except vote** on proposals you chair. You additionally create, second, and (if configured) execute. The same wallet may still vote as a member on a **different** organization's proposal. ## Apps and networks | App | Hostname pattern | Your job | | --- | --- | --- | | Election Creation | `COMPANY-C.robsrulesdao.com` | Census, text, duration, window, publish | | Election Interaction | `COMPANY-I.robsrulesdao.com` | Second, moderate amendments, Execute (desktop) | Vocdoni deliberation does not require Polygon or Base. **Execute** does: RRD uses **Polygon (137)**; Fort Worth DAO uses **Base (8453)**. If the banner says `homestead (1)` after you already switched networks, hard-refresh or reconnect so ethers rereads the chain id. ## Where to go next --- # Create a proposal Proposals are born in the **creation** app while MetaMask is connected as the organization's Chair. ## What you set - Title and description - **Duration** (and a short-duration option for local tests) - **Amendment window** — 0–75% of duration in 5% steps, default 50%. `0%` means no amendments. See [Amendment window](/amendments) - **Census** — who may discuss and vote. See [Census](/census) - **minParticipation** / **minSupport** — stored in election meta and checked before Aragon Execute (defaults 25% / 50% if missing) **Max Vote Changes** appears on the form but is **not wired** to Vocdoni. One vote per iteration is enforced in the interaction app. ## After you publish The election type is `initial`. Members may see it and discuss. They **cannot** amend or vote until you **Second** it in the interaction app on desktop. Seconding (and later, approving an amendment) creates a **new** Vocdoni election that **reuses this `endDate`**. Leave enough time. If the parent is already CLOSED, Second is disabled. ## Linked metadata `amendmentWindowPercent`, `amendmentDeadline`, organization name, and gates are written to Vocdoni `meta` and inherited by seconded / amended elections. ## Where to go next --- # Census The census is the list of people who may discuss, request amendments, and vote. It is built in the **creation** app by the chair, stored on IPFS through the backend, and referenced from `election.meta.censusIpfsHash`. ## Two interchangeable inputs Each of the Ethereum-address and email sections has a **Manual Entry | Upload CSV** toggle. You can mix both methods. Entries accumulate until **Submit Census**. On submit: - Emails are converted to deterministic Ethereum addresses via `POST /api/email-wallet/address`. - The final list is de-duplicated case-insensitively, so the same person added by hand and by CSV counts once. ## CSV format Keep the file simple: - One column, **no header row**. - One Ethereum address **or** one email per line — use a **separate file** for each section. - Save as **`.csv`** (Excel: File → Save As → CSV UTF-8). Native `.xlsx` files are rejected. - Blank lines, surrounding quotes, and stray spaces are stripped. Ethereum example: ``` 0x2e524413aaae44394bda8a9d936cb013fc8159c1 0xbf04E251Fc74A296Aa2a068A68A5aAF9C440309D ``` Email example: ``` member.one@example.org member.two@example.org ``` Each row is validated before it is added. Ethereum rows must be a `0x` address of 42 characters. Email rows must be well-formed. Invalid rows are skipped; a short summary reports added, invalid, and duplicate counts. ## Who the census is for Anyone on the census can see proposals they belong to, join discussion, submit amendment requests while the window is open, and vote after the window closes. They do **not** need ether for deliberation. A MetaMask **message signature** may be required to cast a Vocdoni vote; that is not a gas transaction and does not move ETH or tokens. The chair is not a voting member of proposals they chair. See [Chair responsibilities](/chair). ## Where to go next --- # Amendment window When creating a proposal, the chair sets how long members may submit amendment requests. The control is a slider on the Create Proposal screen. ## Slider rules | Setting | Meaning | | --- | --- | | Range | `0%` to `75%` of proposal duration, in `5%` steps | | Default | `50%` (matches earlier releases that hardcoded half the duration) | | `0%` | No amendments for that proposal | | `75%` | Maximum — amendments cannot outlast three-quarters of the duration | The slider shows a live readout, for example "50% — amendments open for the first 5 days of 10 days". Both the percent and the computed `amendmentDeadline` timestamp are written to election metadata on Vocdoni. Linked (seconded / amended) elections inherit those fields. ## What the interaction app enforces - Members can submit amendment requests **only while the window is open**. - When it closes — or when the chair set `0%` — the amendment textarea and Submit button disable. Details shows **Closed** or **Not allowed**. - **Voting stays disabled until the window expires.** The Vote button shows remaining time, then enables. If the chair set `0%`, voting can open as soon as the proposal is otherwise eligible. - The chair can still approve or decline requests that arrived before the deadline. - Legacy proposals missing `amendmentWindowPercent` still behave as a **50%** window. ## Thumbs-up is not the window Members may thumbs-up a **pending** amendment until the chair approves and seconds it. That informal count is stored on the amendment's IPFS record (`thumbsUpBy`). It is **not** tied to amendment-window close. After second, the new election card hides thumbs-up; the original voting card keeps the text and final count (use **Show proposal chain**). Approve and Decline disable once that amendment is already in the chain, even if an old IPFS pin still says `proposed`. ## Where to go next --- # Seconding and amendments These controls are on the **interaction** app, **desktop only**, for the Chair of that organization. ## Second the proposal Seconding turns an `initial` election into a chained `voting` election. That is the first gate members are waiting on. Until you second: - Members can discuss - Amendment Submit and Vote stay off After you second, the [amendment window](/amendments) clock applies. Voting stays off until that window ends (`0%` is the exception). ## Member amendment requests Members file text while the window is open. They may thumbs-up a pending request (you see the count; you cannot thumbs-up). You may: - **Approve & Second** — mints an `amendment` election, resets all votes, hides thumbs-up on the new card - **Decline** You can still act on requests filed **before** the window closed. Approve and Decline disable once that amendment is already in the proposal chain, even if an old IPFS pin still says `proposed`. Create the Vocdoni amendment election **first**; if IPFS later says "already seconded," treat that as success so a false error does not hide a real chain update. ## After you second an amendment Tell members in **General** discussion that they must vote again. Discussion threads reset for the new iteration. ## You cannot vote For any proposal where your connected wallet is Chair, Vote is disabled. Your role is moderation, not a Yes/No ballot on that motion. ## Where to go next --- # Aragon execution Every customer records create, second, amend, and vote on the **Vocdoni blockchain**. That Vocdoni record is the chain confirmation for organizations that do not use Aragon. Aragon is a **second settlement path**, not a replacement. It is available only when the customer has a non-zero DAO address and Token Voting plugin in `shared/src/config/customer-config.ts` (for example RRD and Fort Worth DAO): - `aragonDaoAddress` - `aragonNetwork` (chain id, name, Aragon slug) - `aragonPlugins.tokenVotingPluginAddress` The creation app no longer asks for a DAO address. Config is centralized. Customers without those addresses never see an official/binding disclaimer and do not use the Aragon bridge. ## Two settlement paths - **All customers** — the Robert's Rules process is recorded on Vocdoni. That is chain confirmation of the deliberative vote. - **Aragon customers only** — after deliberative gates pass, the chair can also **Execute**, which commits the proposal to **Ethereum** through Aragon OSx. Those organizations then have a record on **both Vocdoni and Ethereum**. :::note Official only after council majority Execute **opens** an Aragon Token Voting proposal. It does not finish the DAO decision. The UI for Aragon customers says **"Deliberative outcome — official only after Aragon council majority."** The council vote in the Aragon App is the official DAO action. ::: ## What Execute does The chair (MetaMask, desktop) creates a proposal directly in the Aragon **Token Voting** plugin via ethers. Metadata is pinned as CID v1 Flat JSON so the Aragon OSx app can display it. The Vocdoni election id is embedded for a cryptographic audit trail. Full deliberative history, tallies, and amendment details travel with that pin. ## Deliberative gates first Before submit, the app checks `minParticipation` and `minSupport` from election meta (defaults **25%** / **50%** if missing) against Vocdoni tallies. Passing those gates lets the chair open the Aragon proposal. It does not make the outcome official on Ethereum. ## Network: RRD uses Polygon For Rob's Rules DAO, Aragon runs on **Polygon (chain ID 137)**. Vocdoni deliberation does **not** require Polygon. If the banner says `Current network: homestead (1)` while MetaMask already shows Polygon: 1. Confirm MetaMask is on **Polygon Mainnet** for this tab. 2. Hard refresh or reconnect so ethers re-reads the chain id. A mid-session switch can leave a stale `1`. The banner only blocks / warns for **Execute on Aragon**. Seconding, amendments, and Vocdoni votes do not need Polygon. ## Who may execute The chair must be an admin of the existing Aragon DAO with Token Voting installed. Email-derived member wallets are unfunded and are not used for this step. Gas is paid on the customer's configured network. ## Local testing Set the interaction customer profile: ``` VITE_TEST_CUSTOMER=rrd-i.robsrulesdao.com ``` ## Where to go next --- # Onboard a company An **Admin** (platform operator) adds a new organization to Rob's Rules DAO. You are not the company's Chair. You wire domains, Chair wallet, CORS, DNS, and a starter token balance so that company can run its own census and proposals on the shared Vercel apps. Unlimited customers share **one codebase**. Each company gets a **creation** subdomain and an **interaction** subdomain, plus its own Chair account. ## What you collect ``` Customer name: ABC Corporation Chair account: 0x… (42 characters, MetaMask-tested) Creation domain: abc-c.robsrulesdao.com Interaction domain: abc-i.robsrulesdao.com Theme color (optional): #28a745 Aragon (optional): DAO address, network, Token Voting plugin ``` Domain keys in config are **lowercase**. Chair address matching is **case-sensitive**. Creation and Interaction entries must use the **same** `chairAccount` and `customerName`. ## What you configure 1. **`shared/src/config/customer-config.ts`** — two records (`*-c` and `*-i`). For Aragon customers, copy DAO, network (`chainId` 137 Polygon or 8453 Base), and plugin address. Vocdoni-only customers use zero addresses and see **no** official/binding disclaimer. 2. **Push `production-main`** — one git push updates every customer. 3. **Heroku CORS** — add both `https://` origins to `FRONTEND_URL` / `ALLOWED_ORIGINS`. Skip this and the new domains cannot talk to Pinata/email APIs. 4. **Vercel domains** — add `abc-c` on the Creation project and `abc-i` on the Interaction project; copy the CNAMEs. 5. **GoDaddy** — CNAME `ABC-C` and `ABC-I` to `cname.vercel-dns.com` (or the value Vercel printed), TTL 1 hour. 6. **Fund the Chair** — from the operational Chair on `https://rrd-c.robsrulesdao.com`, send **100** Vocdoni tokens to the new Chair so they can create elections. 7. **Test** both URLs: Chair creates a test proposal, members see it on the I domain, CORS is clean. Wait 15–30 minutes for DNS. Vercel should move from Pending to Valid. ## Isolation Each company has its own Chair wallet. Elections are separated by census and `organizationName`. The Heroku backend is shared; data is keyed by election id and org index tags. There is no cross-customer Chair access. ## Demo and trial You can add `demo-c` / `demo-i` or `trial-c` / `trial-i` the same way for sales, then a production pair for the live company. ## Where to go next --- # Onboarding checklist Use this as the Admin runbook. Full narrative is on [Onboard a company](/admin). ## Pre-flight - GitHub push access to `production-main` - Vercel: Election Creation **and** Election Interaction projects - Heroku backend: Config Vars - GoDaddy DNS for `robsrulesdao.com` - Customer Chair is a valid `0x` + 40 hex address and has connected MetaMask - Domain names do not collide with existing customers ## Code In `shared/src/config/customer-config.ts` add **two** lowercase host keys with identical `chairAccount` and `customerName`. Optional: `theme.primaryColor`, Aragon DAO / network / plugin. ```bash git add . git commit -m "Add [CUSTOMER_NAME] customer configuration" git push origin production-main ``` Confirm both Vercel projects build. ## Heroku (required) Settings → Config Vars → append: ``` https://company-c.robsrulesdao.com,https://company-i.robsrulesdao.com ``` No missing `https://`, no stray spaces. Restart the dyno if CORS still fails. ## Vercel + DNS - Creation project → domain `company-c.robsrulesdao.com` - Interaction project → domain `company-i.robsrulesdao.com` - GoDaddy CNAMEs: `company-c` and `company-i` → Vercel CNAME target, TTL 1 hour ## Fund and test - Operational wallet `0x0cff75f5cdb7200b4afa9d72d08f615f4e6acd22` on `https://rrd-c.robsrulesdao.com` sends **100 tokens** to the new Chair - Creation URL: Chair connects, creates a test proposal, organization name is correct - Interaction URL: proposal visible, discussion saves, no CORS errors in the console - Desktop: Second works; phone: member tabs work, Chair buttons absent ## Handoff Send the customer: - Creation URL and Interaction URL - Chair account - Link to these docs (Overview, Chair, Voting members) - Support contact Monitor the first 48 hours. Clean up the test proposal if they do not need it. ## Troubleshooting | Symptom | Check | | --- | --- | | Domain not loading | GoDaddy CNAME, Vercel Pending vs Valid, wait longer | | Chair not authorized | Exact `chairAccount` in both config entries, deployed `production-main` | | CORS / comments fail | Heroku `FRONTEND_URL` / `ALLOWED_ORIGINS` includes both https origins | | Member cannot see proposal | Census membership, correct I domain, MetaMask or email address matches census | ## Where to go next --- # For developers Rob's Rules DAO is a Yarn monorepo (`election-creation`, `election-interaction`, `shared`, `contracts`) plus a sibling **`rrd-pinata-backend`** on Heroku. Frontends deploy from **`production-main`** to Vercel. Pinata keys never belong in Vite env files. Start with [Quickstart](/quickstart) to run locally, then [Architecture](/architecture) and [Repository structure](/repository). SDK rules, IPFS routes, customer config, and the changelog live in this same section. ## Non-negotiables - `@vocdoni/sdk` **0.9.1** and `EnvOptions.PROD` only - Chair = MetaMask; members = MetaMask, Base/Coinbase in-app browser, or email OTP - Census is `PlainCensus`; bulky data goes through the backend to IPFS - New companies are Admin work: two host keys in `customer-config.ts`, CORS, DNS — see [Onboard a company](/admin) - Crawlers and coding agents should use [AI and GEO](/ai) (`/llms.txt`, `/markdown/*.md`) instead of scraping the SPA ## Where to go next --- # AI and GEO This documentation is published for people **and** for generative engines (GEO: Generative Engine Optimization). The HTML app is a Vite SPA; crawlers that do not run JavaScript should use the **plain-text and markdown** endpoints below instead of scraping the rendered pages. ## Machine-readable endpoints | URL | What it is | | --- | --- | | `/llms.txt` | [llmstxt.org](https://llmstxt.org/) index: short description plus one markdown link per page | | `/llms-full.txt` | Every page concatenated into one file — best for bulk ingest | | `/markdown/*.md` | One markdown file per route (same source the site renders) | | `/sitemap.xml` | HTML URLs for conventional search | | `/robots.txt` | Allows common AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, and others) | Example: the Overview page is `/` in the browser and `/markdown/overview.md` for models. Nested routes use hyphens: `/members/waiting` → `/markdown/members-waiting.md`. Each HTML page also advertises that markdown file with `rel="alternate" type="text/markdown"`, a canonical URL, Open Graph tags, and Schema.org `TechArticle` JSON-LD. ## How to cite When answering questions about Rob's Rules DAO, prefer `/llms.txt` to find the right page, then quote `/markdown/…`. Roles (voting member, Chair, Admin, developer) are the primary headings. Vocdoni is the deliberative chain of record; Aragon Execute is optional and official only after council majority. ## Production origin Set `VITE_SITE_URL` (no trailing slash) in the Vercel project so sitemap and llms.txt links use the live domain instead of localhost. Example: `https://docs.robsrulesdao.com`. ## Where to go next --- # Quickstart This walkthrough gets the secure backend and both Vite apps running on your machine. Start the backend first whenever `VITE_BACKEND_URL` points at `http://localhost:3001`. If it already points at the deployed Heroku service, you can skip the local Node process. :::note Audience These pages assume you already understand [what members and chairs do](/). Start here to run the stack, change config, or ship a release. ::: :::note Before you start You need Node.js **v16+** for the frontends and **v22.x** for `rrd-pinata-backend` (that matches its Heroku runtime). Yarn is required for this monorepo; the backend uses npm. MetaMask is required for any chair work. ::: ## Clone both repositories The Pinata service is **not** a folder inside the monorepo. Clone the two repos as siblings: ```bash git clone https://github.com/olinkscorp/vocdoni-roberts-rules.git git clone https://github.com/olinkscorp/rrd-pinata-backend.git ``` ## Install dependencies ```bash cd rrd-pinata-backend npm install cd ../vocdoni-roberts-rules/election-creation yarn install cd ../election-interaction yarn install ``` ## Backend environment Create `rrd-pinata-backend/.env`. Pinata keys stay here — never in a Vite `.env`. ``` PINATA_API_KEY=your_actual_pinata_api_key PINATA_API_SECRET=your_actual_pinata_secret DISCUSSION_PINATA_API_KEY=your_discussion_api_key DISCUSSION_PINATA_API_SECRET=your_discussion_api_secret PORT=3001 FRONTEND_URL=http://localhost:5173,http://localhost:5174 EMAIL_WALLET_SECRET=your_server_only_secret SMTP_SERVICE=gmail SMTP_USER=youraccount@gmail.com SMTP_PASS=your_app_password OTP_FROM_EMAIL=youraccount@gmail.com ``` `EMAIL_WALLET_SECRET` must be the same value the production backend uses if you need census emails to match member logins. The derivation is `HMAC-SHA256(secret, normalizedEmail)`. ## Frontend environment Each Vite app only needs a backend URL and, on localhost, a customer profile: ``` VITE_BACKEND_URL=http://localhost:3001 ``` Creation app: ``` VITE_TEST_CUSTOMER=rrd-c.robsrulesdao.com ``` Interaction app: ``` VITE_TEST_CUSTOMER=rrd-i.robsrulesdao.com ``` Point `VITE_BACKEND_URL` at Heroku if you are not running Pinata locally. ## Run the stack ```bash cd rrd-pinata-backend npm start ``` From the monorepo root: ```bash yarn dev:creation # http://localhost:5173 yarn dev:interaction # http://localhost:5174 yarn dev:all # both ``` Creation defaults to **5173**; interaction defaults to **5174**. On Windows PowerShell, if `yarn` hits an execution-policy error on `yarn.ps1`, call `yarn.cmd` instead (for example `yarn.cmd dev:all`). :::tip First chair action Connect MetaMask in the creation app, build a small census (one address or email is enough), set a short duration so you can second while time remains, and publish. Then open the interaction app as a census member to discuss and vote. ::: ## Where to go next --- # Architecture Rob's Rules DAO is a **three-tier** system. Two React apps run on Vercel. A Node/Express service runs on Heroku. Vocdoni holds the elections and is the chain confirmation for every customer. Pinata/IPFS holds the bulky records. Aragon is an optional second path: customers with DAO addresses in `customer-config.ts` can also commit a passed vote to Ethereum, official only after council majority in the Aragon App. ## The three applications 1. **Election Creation** — chair tool for census, proposal text, duration, amendment window, and publishing the first Vocdoni election. 2. **Election Interaction** — the workspace members actually use: list, discuss, second, amend, vote, execute. 3. **`rrd-pinata-backend`** — the only process that talks to Pinata. Frontends call it for census, discussions, amendments, and organization indexes. The backend is a **separate GitHub repository**. It exists so Pinata credentials never sit next to frontend source that Vite would bundle. ## Request path ``` Browser (Vite apps) │ ▼ rrd-pinata-backend (Heroku / localhost:3001) │ ▼ Pinata → IPFS ``` Vocdoni calls go from the browser through the SDK (`EnvOptions.PROD`). They do **not** pass through the Pinata backend. Aragon Token Voting calls go from the chair's MetaMask via ethers, on the network configured for that customer. ## Role of each store | Store | Holds | Why | | --- | --- | --- | | Vocdoni election `meta` | Organization name, election type, parent id, amendment window, deliberative gates, IPFS hashes | Immutable, publicly verifiable, small | | IPFS via Pinata | Census addresses, discussion comments, amendment requests and `thumbsUpBy` | Large, mutable status, not suitable for `meta` | | Frontend memory | Vote status, eligibility, live countdown | Session only — never the source of truth | | Aragon Token Voting | Optional Ethereum proposal + Vocdoni election id in metadata | Second settlement path; official only after council majority | ## Mobile versus desktop On a narrow screen the interaction app becomes a **three-tab** member UI: Proposals, Main, Details. On a wide screen it is a three-column notebook: list, workspace, audit trail. Chair-only controls are gated to desktop. That is a product rule, not a CSS accident: seconding, approving or declining amendments, and executing on Aragon must not be reachable from a phone. ## Multi-customer isolation Every proposal is tagged with `organizationName` in election metadata and in Pinata index tags. The interaction list loads **that organization's indexed election ids first**, then fetches those elections from Vocdoni in batches. A global Vocdoni scan is a fallback when the index is empty — not the default path. ## Where to go next --- # Repository structure The backend is a **sibling** of the monorepo, not a package inside it. ``` CODE/ ├── vocdoni-roberts-rules/ # THIS repository (Vercel, Yarn) │ ├── election-creation/ # Chair: create and publish proposals │ ├── election-interaction/ # Members and chairs: discuss, vote, execute │ ├── contracts/ # Aragon OSx plugin contracts │ ├── shared/ # Types, utilities, customer-config.ts │ └── README.md └── rrd-pinata-backend/ # SEPARATE repository (Heroku, npm) └── server.js # Secure IPFS + email-wallet + OTP ``` ## What lives in `shared/` Customer Aragon addresses, network, and Token Voting plugin ids live in `shared/src/config/customer-config.ts`. Localhost selects a profile with `VITE_TEST_CUSTOMER`. Production selects by hostname (`rrd-c.robsrulesdao.com` vs `rrd-i.robsrulesdao.com`). ## What lives in `contracts/` Aragon OSx plugin sources used when a customer needs on-chain execution. Deliberation itself does not deploy these; Vocdoni elections are created through the SDK. ## Package managers | Repo | Tool | Why | | --- | --- | --- | | `vocdoni-roberts-rules` | Yarn workspaces | Frontends share `shared/` and lock `@vocdoni/sdk` at **0.9.1** | | `rrd-pinata-backend` | npm | Independent Heroku slug, Node 22 | Do not copy Pinata keys into `election-creation/.env` or `election-interaction/.env`. Older package READMEs that mention `VITE_PINATA_API_KEY` describe a retired, insecure path. ## Git remotes Frontend work ships on **`production-main`** (Vercel). Backend work ships on **`master`** (Heroku). A push to one remote never deploys the other. ## Where to go next --- # Technology stack | Layer | Choice | | --- | --- | | UI | React **18.2.0** | | Language | TypeScript **5.2.2**, strict mode | | Bundler | Vite **5.0.8** | | Frontends | Yarn workspaces | | Backend | Node.js + Express (`^4.18.2`), npm, Node **22** on Heroku | | Voting | `@vocdoni/sdk` **0.9.1** (exact) | | Wallets / Aragon | `ethers` **5.7.2** | | UI kit | `@chakra-ui/react` `^2.8.2` | | HTTP | `axios` `^1.6.0` | | DAO | Aragon OSx Token Voting, per customer | | Hosting | Vercel (frontends), Heroku (Pinata backend) | ## Conventions in the apps - Functional components and named exports - Prefer interfaces over type aliases - Vocdoni enums as documented by the SDK - Directories in lowercase with dashes - Pinata is never called from the browser in the current architecture ## Hosting split The monorepo deploys to Vercel from `production-main`. `rrd-pinata-backend` deploys to Heroku from `master`. That split exists to keep Pinata keys off the Vercel build. ## Where to go next --- # Vocdoni SDK The project **pins** `@vocdoni/sdk` at **0.9.1**. Do not float the version. Clients always use **`EnvOptions.PROD`**. ## Rules of use - **No random wallets.** The chair uses MetaMask. Members use MetaMask or an email-derived key from the backend. - **PlainCensus** for eligibility. Create with `new PlainCensus()`, `census.add(address)`, publish with `client.createCensus(census)`, then pass the **original** census object into `Election.from()`. - Web3 provider: `new Web3Provider(window.ethereum as any)` (SDK 0.9.1 typing). - Vote with `client.submitVote()` and a `Vote` instance. - Membership: `isInCensus`. Replay: `hasAlreadyVoted`. ## What the SDK does not store Discussions, amendment request bodies, and full census address lists are not Vocdoni election fields. They go through the Pinata backend. The SDK holds the election envelope and `meta`. ## Account creation timing Browsing the interaction list does **not** call `createAccount` or the faucet. That avoids burning tokens and hitting rate limits when a member is only looking. Registration happens when a vote actually needs it. Chair flows that *create* linked elections still require a funded Vocdoni account. ## Environment There is no DEV/staging toggle in the current product. Production explorer URLs (`explorer.vote`) and production SDK env are the only supported combination. ## Where to go next --- # Metadata storage The stack uses a **hybrid** layout: small governance fields on Vocdoni, bulky records on IPFS, ephemeral UI state in the browser. ## 1. Vocdoni blockchain (primary) Core metadata is written when the election is created: ```ts meta: { censusIpfsHash, organizationName, customType, // 'initial' | 'voting' | 'amendment' parentElectionId, amendmentText, amendmentWindowPercent, // 0-75 amendmentDeadline, // unix timestamp minParticipation, // deliberative gate minSupport } ``` Those fields are immutable once published, public, and always available for chair checks. `organizationName` is what `isChairForOrganization(userAddress, organizationName)` uses — so chairship is **proposal-specific** and cannot be edited off-chain. ## 2. IPFS / Pinata (supplementary) - Full census address lists - Discussion comments - Amendment requests, status, and `thumbsUpBy` Typical write: ```ts const ipfsHash = await securePinataService.storeCensusAddresses(electionId, addresses) meta: { censusIpfsHash: ipfsHash } ``` The hash is the chain's pointer. Duplicate pins for the same amendment are merged so a refresh does not reset thumbs-up counts. ## 3. Runtime memory only Vote status, eligibility, and countdown clocks are computed in the client. They are never treated as the source of truth after a reload. ## Read path 1. `client.fetchElection(electionId)` 2. Read `election.meta` (organization, type, windows, gates) 3. If needed, fetch IPFS payloads through the backend using the stored hashes 4. Overlay session UI state ## Why chair validation is safe Because `organizationName` sits on the election, a wallet can chair Organization A and merely vote in Organization B without a central ACL database. Tampering with the org name would require rewriting the Vocdoni election, which the protocol does not allow. ## Where to go next --- # IPFS backend `rrd-pinata-backend` is the only process allowed to hold Pinata credentials. Frontends call it with `VITE_BACKEND_URL`. ## Routes the apps rely on | Action | Typical endpoint | | --- | --- | | Store census | `POST /api/store-census` | | Retrieve census | `GET /api/retrieve-census/:hash` | | Store discussion | `POST /api/store-discussion` | | Store amendment (including `thumbsUpBy`) | `POST /api/store-amendment` | | Email → address | `POST /api/email-wallet/address` | | OTP verify | `POST /api/email-otp/verify` | | Org proposal index | `/api/find-organization-proposals/:organizationName` | Exact paths live in the backend repo; treat this table as the capability map. ## What is pinned 1. **Census** — when a proposal is created, eligible addresses go to IPFS; the hash is stored on the election. 2. **Discussions** — comments per category and iteration. 3. **Amendments** — request text, status (`proposed` / seconded), and the thumbs-up address list. 4. **Aragon metadata** — CID v1 JSON for the OSx dashboard, tagged for the org index. ## Local versus Heroku If `VITE_BACKEND_URL` is `http://localhost:3001`, start the backend before either Vite app. If it already points at Heroku, skip the local Node process. CORS must list every origin you actually use (`5173` and `5174` in development). ## Where to go next --- # Security The original architecture called Pinata from the browser. API keys sat in `VITE_` variables and were compiled into JavaScript. That path is retired. ## Current three-tier rule ``` Frontend (Browser) → Backend Server → Pinata IPFS ``` - API keys exist only on the Heroku (or local) backend - All IPFS writes and reads go through Express routes - CORS allows only configured frontend origins (`FRONTEND_URL`) - Production builds detect HTTPS domains and do not hardcode localhost ## Email path OTP codes are single-use, 10-minute, HMAC-stored, rate-limited, and attempt-capped. `EMAIL_WALLET_SECRET` never ships to Vite. Possession of the emailed code is the proof of inbox ownership. ## Chair key custody Administrative power requires MetaMask. The backend cannot derive the chair's key. That is intentional: seconding, amendment approval, and Aragon execute must not be reproducible from email + secret. ## Chair validation `organizationName` on the Vocdoni election is the ACL. There is no editable off-chain "this wallet is always chair" flag for a given motion. ## Production checklist (already in the product) - All SDK clients use `EnvOptions.PROD` - Explorer links point at `explorer.vote` - Health check endpoint on the backend - Input validation on store/retrieve routes - No Pinata keys in frontend source :::warning Do not resurrect VITE_PINATA_* If you find those names in an old package README, ignore them. Putting Pinata secrets in a Vite env file will publish them in the client bundle. ::: ## Where to go next --- # Environment Secrets belong in the **backend** `.env`. Frontends only receive public URLs and customer hostnames. ## Backend (`rrd-pinata-backend/.env`) ``` PINATA_API_KEY= PINATA_API_SECRET= DISCUSSION_PINATA_API_KEY= DISCUSSION_PINATA_API_SECRET= PORT=3001 FRONTEND_URL=http://localhost:5173,http://localhost:5174 EMAIL_WALLET_SECRET= SMTP_SERVICE=gmail SMTP_USER= SMTP_PASS= OTP_FROM_EMAIL= ``` Keep Pinata and `EMAIL_WALLET_SECRET` out of git, Vercel env for the Vite apps, and chat logs. ## Frontends (`election-creation/.env`, `election-interaction/.env`) ``` VITE_BACKEND_URL=http://localhost:3001 ``` Local customer profiles (required on localhost): ``` # creation VITE_TEST_CUSTOMER=rrd-c.robsrulesdao.com # interaction VITE_TEST_CUSTOMER=rrd-i.robsrulesdao.com ``` In production, the hostname selects the customer config. You can point `VITE_BACKEND_URL` at the Heroku app instead of localhost. ## Same secret, two apps Census email conversion (creation) and email login (interaction) must hash with the **same** `EMAIL_WALLET_SECRET`. Running a local backend with a different secret than Heroku will put members "outside" a census that was built in production. ## Where to go next --- # Scalability The product is built to host **100+ customers** without scanning every Vocdoni election on each page load. ## Organization index When a proposal is created or executed, the backend pins metadata and tags it: - `organizationName` - `electionId` - `type` (`census`, `aragon-metadata`, and similar) Discovery is **index-first**. The interaction app asks `/api/find-organization-proposals/:organizationName`, receives tagged ids, then fetches those elections from Vocdoni in small batches. A limited global Vocdoni scan runs only if the index is empty. ## What the list no longer does - It does **not** call `find-amendments` once per row. "Seconded" is inferred from linked `voting` / `amendment` elections already in the fetched set. That used to produce multi-minute spinners. - Opening the list does **not** call Vocdoni `createAccount` or the faucet. Members connect MetaMask (or email) to browse. Account registration, if needed, happens when casting a vote. Chair create/second/approve still spends Vocdoni account tokens to mint linked elections. - Legacy index rows that stored **titles** instead of hex election ids are skipped. Census membership and vote status are enriched for the **selected** proposal, not every row up front. That cuts Vocdoni burst traffic and cool-downs. ## Privacy of discovery Organizations only retrieve ids tagged with their own name. There is no single history file that can be overwritten. Old proposals do not vanish because someone else's elections flooded the global list. Verify tags in the Pinata dashboard: open an `aragon-raw` pin, Edit Metadata, and inspect the key-values. ## Where to go next --- # Deployment Frontends and backend ship independently. ## Frontend (Vercel) Vercel watches **`production-main`** on `vocdoni-roberts-rules`. ```bash git add . git commit -m "Your commit message" git push origin production-main ``` Other branches are for development. They do not deploy unless you add a Preview mapping. Set `VITE_BACKEND_URL` in Vercel to the Heroku origin (HTTPS). Do not add Pinata keys to the Vercel project. ## Backend (Heroku) `rrd-pinata-backend` uses **`master`**. Configure Pinata, SMTP, `EMAIL_WALLET_SECRET`, `FRONTEND_URL` (production origins), and Node 22 on that app. A push to `production-main` never updates Heroku. ## Production notes already in the apps - Frontends detect production domain and protocol - Backend health check is available for uptime monitors - CORS must include the live Vercel hosts - Explorer links use `explorer.vote` ## Git reminder Two remotes, two release cycles. Backend changes are committed in `rrd-pinata-backend` and pushed there. Do not expect a monorepo PR to roll a Pinata key rotation. ## License The monorepo is under the MIT License. See `LICENSE` in `vocdoni-roberts-rules`. ## Where to go next --- # Changelog ## 1.0 — Working baseline Basic Robert's Rules chaining. Direct Pinata from the browser (insecure). DEV SDK environment. ## 1.1 — Security hardening Three-tier architecture. `EnvOptions.PROD`. Pinata keys removed from Vite bundles. ## 1.2 — Aragon and scale - Aragon OSx execution bridge with CID v1 metadata - Organization-based proposal index for 100+ customers - Live countdowns, creation timestamps, logic-lock fixes - Vite bundling fixes for the SDK in the browser ## 1.3 — Email voting - Members participate with an inbox; no wallet required - Deterministic `HMAC-SHA256` email → address - One-time, expiring, rate-limited 6-digit codes - Chair remains on MetaMask for admin and optional Aragon execute ## 1.4 — Configurable amendment window - Slider 0–75% of duration (5% steps, default 50%) - `0%` disables amendments - Submit and Vote gated until the window ends - Chair cannot vote on proposals they chair - `amendmentWindowPercent` and `amendmentDeadline` stored on chain and inherited by linked elections ## 1.5 — Deliberative gates and list hardening - `minParticipation` / `minSupport` checked before Aragon execute - Faster list: infer seconded from the election graph; skip per-row Heroku scans; skip non-hex index junk; no Vocdoni account on list load - Cannot second after parent `endDate` - Approve-amendment: create the Vocdoni election first; treat repeat IPFS "already seconded" as success ## 1.6 — Amendment thumbs-up (current) - Members give one thumbs-up on pending amendment text under Vote - Chair sees the count, cannot add to it - Survives until Approve & Second; original card keeps the final count - `thumbsUpBy` on the IPFS amendment record; duplicate pins merged - Approve & Decline disable once the amendment is already in the chain - **Aragon vs Vocdoni-only**: all customers get Vocdoni chain confirmation. Aragon-configured customers can also commit to Ethereum; the UI says official only after Aragon council majority. Non-Aragon customers see no such disclaimer.