Releases
June 15, 2026
The first release. Ranked and approval voting, public and private polls, two languages, three themes.
The initial release of Democratia. Everything below is new, so rather than a bare list this entry describes what shipped and the decisions worth knowing about.
Polls
- Create a poll with a title, description and optional participant instructions.
- Choose ranked preference or approval voting.
- Choose whether only the owner adds options or participants may propose their own.
- Set four scheduled moments — collection opens, collection closes, voting opens, voting closes — with a per-poll time zone. Timestamps are stored in UTC and displayed in the poll's zone.
- Optional participant limit, optional approval selection limit.
- Five participation rules: automatic result publication, ballot editing, participant option editing, submitter name visibility, and result anonymity.
- Every poll starts as a draft, visible only to its owner.
Voting
- Ranked ballots with three interchangeable controls: drag, up/down buttons, and a position dropdown per row. The dropdown is the real form field, so a ballot submits correctly without JavaScript.
- Approval ballots with a live selection counter and a server-enforced limit.
- Ballots are editable until voting closes, when the poll allows it.
- A receipt confirming the ballot was recorded and showing it back to its owner, containing no aggregate data of any kind.
Results
- Borda count for ranked polls: with
noptions, first place is worthn − 1points. - Deterministic tie-breaking — total score, then first places, then second places, and so on. Options that cannot be separated are reported as tied rather than separated arbitrarily.
- Neither submission time nor randomness is ever used as a tie-breaker.
- A ranking distribution table showing how many ballots placed each option at each position.
- A tie-break breakdown for the owner, naming the position that separated each near-tie.
- Results distinguish three counts that are easy to confuse: people invited, identified participants and ballots submitted.
- Streamed CSV export for the owner, written without adding a spreadsheet dependency.
Access
- Public polls behind a 32-character unguessable link — roughly 190 bits from a cryptographic random source. No account needed to take part.
- Private polls limited to invited registered accounts, re-checked on every request.
- Owners can regenerate a participant link, which invalidates the previous URL immediately.
- Public polls are never listed anywhere. "Public" means reachable by link, not advertised.
Participants
- One
Participantmodel serves both registered users and public guests, so every voting rule is written once. - Guests are identified by a 43-character random token in an encrypted, HTTP-only,
SameSite=Laxcookie. Only its SHA-256 hash is stored. - The limits of that are stated plainly in the interface and the documentation: it prevents accidental double submission, not a determined one.
Lifecycle
- Six phases: draft, collecting options, voting, closed, results published, cancelled.
- Automatic transitions driven by the schedule, plus eight manual owner actions.
- Every transition is authorized, validated against an allowed-transition table, transactional, idempotent, row-locked, audited and announced after commit.
- Phases are reconciled whenever a poll is accessed, so a late scheduler never leaves the interface in a phase the clock has already left.
- Voting cannot open with fewer than two options.
- Schedule timestamps only ever move earlier, so reconciling a late transition never rewrites history.
Notifications
- Four notifications: invited to a poll, voting has opened, voting closes soon, results published.
- Each arrives as an in-app record and a queued email, in the recipient's own language.
- Dispatch is claimed with an atomic conditional update, so overlapping scheduler runs cannot send twice.
- "Closes soon" reminders go only to people who have not voted.
Languages and themes
- English and European Portuguese, at exact key parity, enforced by a test.
- Locale resolved from the account, then the session, then a cookie, then the default.
- Light, dark and system themes, with system as the default.
- The theme is server-rendered into the document, so there is no flash of the wrong theme and no bootstrap script — nothing to allow in a Content Security Policy.
- Every colour comes from a semantic token, so there is one copy of every view.
Security
- Native authentication: registration, sign in, sign out, password reset, email verification, remember me.
- Email verification gates poll creation, management and registered voting.
- Rate limiting on seven entry points, with a translated message on sign-in failure.
- Result secrecy enforced three times over: the authorization rule refuses, the query refuses to compute, and no view renders hidden result markup. The owner is subject to the same rules as everyone else.
- Private polls report not found rather than forbidden, so their existence is never disclosed.
- Database constraints for every invariant that must never be wrong, including
RESTRICTforeign keys that make an option referenced by a ballot impossible to delete.
Documentation
- A sixteen-page documentation section and this changelog, both authored as markdown files and rendered by the same view, with a filterable sidebar and an on-page contents list.
- A README covering installation, configuration, security decisions, privacy and retention.
docs/architecture.mdrecording the reasoning behind each significant decision.
Notable fixes made during development
Three real bugs were found and fixed before release. They are recorded because each was silent, and a future reader may hit the same class of problem.
- Every Portuguese plural fell back to the singular. Laravel keys its plural rules by underscored
locale codes (
pt_PT) while Democratia uses the hyphenated form (pt-PT), so no rule matched and the selector returned index zero. A small message-selector adapter now bridges the two. - The stock pagination view crashed any paginated page. Laravel's built-in view resolves the bare
translation key
results, which collided with this application'sresultstranslation group and produced an array where a string was expected. The view is now published and themed with the application's own tokens, which was wanted anyway. - Error pages had no theme. The theme was shared by middleware, but error pages render outside the HTTP middleware stack, so a 404 raised during route resolution failed. A view composer now supplies it, which works everywhere.
Test coverage
427 tests, 1641 assertions: unit tests for phase derivation and both scorers with no database, feature tests over real HTTP requests, policy tests across the full authorization matrix, notification and command tests, and architecture tests enforcing the layer boundaries.
Known limitations
Stated in full in Security and privacy and the README. In short: guest identity is per browser rather than per person; results are recomputed rather than cached; account deletion is not implemented; there is no option moderation queue; ranked ballots require ranking every option.