docs(poll): document governed participation boundary

This commit is contained in:
2026-07-21 21:17:04 +02:00
parent 156486fcee
commit 2e1ed6e0d8

View File

@@ -50,6 +50,72 @@ and authenticated API routes for:
- signed participation links for reduced/no-Access participation
- context and workflow metadata for modules such as Scheduling
## Governed signed participation
The v0.1.10 contract lets a consuming module bind an invitation to an exact
response gateway (`module_id`, `resource_type`, and `resource_id`) and snapshot
generic participation rules onto it. A bound invitation cannot use the legacy
`GET /poll/public/{token}` or `POST /poll/public/{token}/responses` routes: both
return the same generic not-found response used for invalid, expired, and
revoked links. There is deliberately no browser-facing Poll bypass when the
owning gateway is missing.
That boundary is Poll-wide. Setting `context_module` declares a module-owned
Poll; for a standalone Poll, its first governed invitation opts the whole Poll
into governed participation. From then on, direct authenticated response
writes, ordinary invitation creation, and every legacy public link fail closed,
including links created before governance was enabled. Governed invitations can
only be created through the in-process participation capability and their
gateway must match the Poll's declared module resource (or the standalone
Poll's first gateway). Polls that never opt into governance retain their
ordinary authenticated and signed-link behavior.
Module ownership also protects management boundaries. Generic Poll update,
lifecycle, option, and invitation-revocation paths cannot mutate an owned Poll,
and generic raw-response and invitation projections cannot bypass the owning
module's privacy policy. The in-process capability supplies the exact module,
resource type, and resource id again for every mutation; Poll compares that
owner while holding its row lock. Aggregate result summaries remain reusable.
Consumers resolve and submit bound invitations through the
`poll.participation_gateway` in-process capability. The policy covers:
- at most one non-`unavailable` selection
- whether `maybe` is accepted
- a maximum number of definitive participants per option
- response comments (trimmed and limited to 4,000 characters)
- email for anonymous participants
- an anonymous-password verification requirement
For availability polls, `available` reserves capacity while `maybe` does not.
Poll re-enforces these rules under its Poll-row lock, so concurrent final-place
claims serialize on PostgreSQL. A gateway-owned password never crosses the
capability boundary: the owning module stores the sole salted verifier,
throttles attempts before checking it, and supplies only the
`anonymous_password` verification attestation. Poll rejects secret-like
invitation/response metadata and stores no password column.
Authenticated module flows do not need to retain a public bearer token. They
can resolve and submit by exact tenant, Poll, governed invitation, gateway, and
respondent identifiers. Poll rejects mismatched identities and applies the same
Poll-row lock and policy checks used by the public-token path. A consumer can
lazily create that governed invitation, retain only its id, and discard the raw
token when no public link is needed.
An owning gateway can update a non-revoked invitation's expiry in place. A past
timestamp expires the existing link immediately; a later future timestamp or
`null` reactivates that same link without exposing or rotating its bearer token.
Revoked invitations remain revoked, and exact retries are idempotent.
The capability also supports durable invitation-scoped idempotency keys,
response prefill, option addition/removal, and invitation revocation. Exact
submission retries return the existing response; key reuse with different
content is rejected. Because a response is an editable resource, replay returns
its current representation rather than an immutable snapshot of the first
submission. Option removal and option changes invalidate only answers bound to
that option, and repeated removal/revocation is an idempotent replay even after
the Poll has moved out of an editable lifecycle state.
## Scheduling As A Poll-Backed Workflow
Scheduling should use Poll as the reusable response collection primitive, not