A new "Poll" post type with anonymous-by-default voting, sized to ship in a week and built to extend toward informal-election features as customers ask.
Polls is the smallest possible "structured input" feature we can ship: a new post type on the wall, anonymous-by-default, single or multi-choice, server-enforced close. Stacked across three PRs (schema, backend, UI). Everything builds clean. A single deploy branch (polls-v1-deploy) is published so the team can check out one branch and test the full feature locally.
Before any code deploys: three customer-validation calls (The Assignment) decide whether v1 stays small or v1.5 election-grade features pull in. The design doc carries a full v1.5 / v2 roadmap captured during 5 review passes.
A "Poll" post type that sits inside the existing Announcement system on the community wall. Admins create a poll the same way they create an image-or-text post today, with a new toggle at the top of the create form. Members see the poll in their feed and vote with one tap. Results render inline.
Two important per-poll settings the admin picks at create time:
| Setting | Options | What changes |
|---|---|---|
PollVoteVisibility |
Anonymous (default) · Public |
Whether vote attribution is visible. Anonymous polls promise no admin can see who voted what. Public polls show member names next to their vote. |
PollResultsVisibility |
AfterVoting (default) · AfterClose · Always |
When results become visible to the audience. Per-row counts hide cleanly when not allowed. |
PollSelectionMode |
Single · Multi |
Radios vs checkboxes. Multi requires a max-selections cap (2–10). |
PollClosesOn |
1h to 90d from publish | Server-enforced via WebJob. Votes after close are rejected at the API. |
The differentiated framing is that v1's defaults (lock-after-vote, anonymous, eligibility = audience) are safe enough for non-binding informal elections in associations and non-profits — the customer set Raklet actually serves. v1.5 adds the formal-election features (eligibility scoping, voter-roll snapshot, k-anonymity warnings, audit log) when customer demand validates that framing.
Members see the same card layout regardless of where they are in the flow. Card morphs between states without a page reload. The trust microcopy is conditional on the per-poll PollVoteVisibility setting.
The same card with PollVoteVisibility = Public. Notice the microcopy changes — Raklet is upfront with members that their name will appear next to their vote when they cast it.
The admin form lives in the existing create-post modal. A segmented Post / Poll toggle at the top swaps the editor in place. No new admin nav, no separate page.
/v2/.../polls/{id}/vote with Idempotency-Key header.PollClosesOn, WebJob flips status. Card shows "Poll closed" pill + final results.The original CEO ask was simple: "add a polls post type with a results-visibility setting and anonymity." Over the course of five review passes, the design accumulated election-grade features (eligibility scoping, k-anonymity warnings, audit log, member-created polls + moderation queue, the EntityLeakageGuard reflection assertion, 3-step migration trilogy, email-results rendering, etc.). At the end of that process the CEO observed that v1 had become roughly 3× the original ask without customer validation.
The plan was recut to minimum-v1. Everything the reviews surfaced is preserved as a v1.5 / v2 roadmap inside the design doc, but ships only when real customer demand surfaces it. This presentation reflects the post-recut state.
Each review's "defensible-in-isolation" addition compounds into "over-engineered-in-aggregate." Customer validation belongs before the maximalist review chain, not after. Saved as a project feedback memory so future feature plans don't repeat this.
AnnouncementType discriminator on AnnouncementPollOption, PollVote, IdempotencyKeyAnnouncementSocialAccess claim gates the endpointsMost deferrals are election-grade features (eligibility scoping, voter-roll snapshot, audit log, k-anonymity warning) that the design reviews surfaced as worthwhile once customers validate the governance use case. v1 ships safe enough for informal elections (lock-after-vote, server-enforced close, anonymity-by-policy with the integration test) without yet committing the architecture to formal governance.
A second group of deferrals is UX polish (publish-confirmation modal, drag-and-drop options editor, email-results rendering) — defensible additions that don't block the core flow. They join the roadmap.
The third group is platform infrastructure (EntityLeakageGuard reflection assertion, EligibilityRollSnapshot generalized for Events) that only earns its complexity once Raklet has multiple consumers. IdempotencyKey is the one platform primitive we kept in v1, because naming it correctly is a one-way decision — refactoring after Events RSVP and Payments adopt it is expensive, doing it right at first commit is cheap.
Approach A from the design doc: a discriminator on the existing Announcement table. Survived four reviews and one Codex challenge.
The hot path is PollService.SubmitVoteAsync. Read order: idempotency-key replay check, poll-state validation, audience + eligibility check, lock-after-vote check, transactional insert. The DB unique constraint catches concurrent racers; the service maps the constraint violation back to "you already voted" with the caller's current vote.
Blue path = vote submission (the hot path). Grey = standard reads + create. Dashed boxes = SQL tables. IdempotencyKey stores the per-request hash so a network-retried vote returns the cached response instead of double-counting.
PollVote.OrganisationMembershipId exists in the database (we need it to enforce lock-after-vote and to surface the caller's own vote). The promise is that no V2 endpoint, no admin response, no export ever surfaces the link between a member and their vote choice on Anonymous polls. Pseudonymous-in-DB, anonymous at every wire.
Three layers of protection in v1:
GetResultsAsync only returns the calling member's own MyVoteOptionIds; other members' votes are never serialized.PollPayloadDto carries aggregates only. Per-option voter names are deferred to v1.5 for Public-mode polls; v1's Public-mode distinction is microcopy only.Raklet.UnitTests/Polls/PollAnonymityTest.cs has three sub-tests:
OrganisationMembershipId, MemberId, Member, or Voter to a poll-vote-adjacent DTO.[Ignore]'d placeholder for OwinTestServer wiring.On Anonymous polls, the card shows: "Raklet does not show individual votes to anyone, including admins." On Public polls it shows: "Your name will be shown next to your vote." The promise is conditional and accurate.
For an Anonymous poll where Sarah, Marcus, and Jenny voted, here's what lives in PollVote vs what any caller sees on GET /v2/.../social/announcements:
Id AnnId OrgMembershipId OptionId ───────────────────────────────────── guid-1 poll-A sarah-id opt-A guid-2 poll-A marcus-id opt-A guid-3 poll-A jenny-id opt-B
{
"id": "poll-A",
"poll": {
"totalVotes": 3,
"options": [
{ "id": "opt-A", "voteCount": 2 },
{ "id": "opt-B", "voteCount": 1 }
],
"myVote": { "optionIds": ["opt-A"] }
}
}
OrganisationMembershipId exists in the DB row (we need it to enforce lock-after-vote and to surface the caller's own vote at myVote.optionIds) but is never serialized adjacent to vote data on any V2 endpoint when the poll is Anonymous. PollAnonymityTest asserts this via reflection (DTO whitelist) + live JSON serialization grep.
Stacked review surfaces, in merge order:
Announcement, creates 3 new tables (PollOption, PollVote, IdempotencyKey) plus the supporting enums, EF6 Fluent config, and the migration files. Metadata-only ALTER on Announcement — no table rewrite.AnnouncementsList, DI registration, and the anonymity integration test. Stays DRAFT until local QA passes.git fetch origin
git checkout polls-v1-deploy
git pull origin polls-v1-deploy
# Restore packages once if your main checkout hasn't recently
.\.nuget\NuGet.exe restore Raklet.sln
# Apply migration (Visual Studio Package Manager Console, Models project)
Update-Database -ConfigurationTypeName Models.Migrations.Configuration
# Build + run via your normal local flow
# (build-fast.ps1 for Application; MSBuild per-project for Models,
# Services, Raklet.Api, Raklet.WebJobs, Raklet.UnitTests)
Then walk this script on gercek.raklet.org:
POST /v2/.../social/polls/{id}/close manually. Card flips to closed state with a "Poll closed" pill plus final results.GET /v2/.../social/announcements response on an Anonymous poll should not contain OrganisationMembershipId anywhere adjacent to vote or option data. Your own myVote.optionIds is the only member-link.Three customer-validation calls. Pick three association or non-profit customers. Ask each one a single question: "If we shipped polls in your wall, what's the first thing you'd run a poll on?" 30 minutes of customer success time, total.
The answers route the v1.5 roadmap:
30 minutes of calls prevents 2+ weeks of building the wrong default.
The full roadmap with rationale, effort estimates, and dependencies lives in docs/features/polls/design.md under "V1.5 / V2 Roadmap." Headline items in rough priority order if customers validate governance use:
| Item | Pulls in if customers ask for… |
|---|---|
Eligibility scoping (PollEligibility table + admin picker) | "Only voting members can vote" |
Voter-roll snapshot at publish (EligibilityRollSnapshot) | "X of Y eligible voted" denominator |
| K-anonymity warning UI for <10 electorates | Small-board / committee polls |
| Audit log (scrubbed of PollOptionId on vote events for timing-attack mitigation) | Governance / compliance trail |
| Settings-lock-at-publish (currently v1 locks at first vote) | Election-grade manipulation prevention |
| Notification-to-all-eligible (currently notifies creator only) | So notification existence doesn't reveal participation |
If casual polls dominate the customer calls, the UX polish items pull in first instead:
"Other (specify)" + PollOtherResponse table | "What should we bring to the potluck?" |
| Vote change (Mode B) with soft-delete + UPDLOCK | Casual polls where members want to revise |
| Publish-confirmation modal | Friction on sensitive polls |
| Email-results inline rendering | The strongest viral surface of the feature |
| Events ↔ Polls integration ("vote on the date") | Strongest "why Raklet" governance story |
What ran before any code was written. Useful context when reviewing the PRs — most decisions have a documented "why" behind them in the design doc.
/office-hours — design session/plan-eng-review — architecture pass/plan-design-review — UI/UX passPollVoteVisibility setting./plan-ceo-review → scope recutfeature/poll-ui / polls-v1-deploy.Project conventions from the Raklet team that the implementation already honours and v1.5 work should keep honouring:
build-fast.ps1 --all only rebuilds Application.csproj; call MSBuild for Models, Services, Raklet.Api, Raklet.WebJobs, Raklet.UnitTests individually.--no-verify. Pre-commit hooks exist for a reason.scripts/dev/pr-ready-check.ps1 before mark-ready / merge.rakletlocalfiles/customcodes/pr-evidence/<num>/ via the documented PowerShell snippet — never commit images to the repo.docs/agents/hot-tables.md before claiming a table is hot. (Announcement is not in the hot-tables registry.)