# First Discovery art-poll network review

**Decision date:** 2026-08-05
**Decision owner:** MakeJoy operator
**Capability:** `network` on explicit adult aggregate submission controls and
two saved-file handoffs; `offline` on the downloadable poll bundle.

## Approved boundary

The operator explicitly approved an adult-facilitated aggregate endpoint that
accepts only:

- the fixed round ID;
- the six fixed concept counts;
- the ballot total; and
- the skipped total.

The endpoint rejects missing fields, extra fields, unknown concept IDs,
individual ballots, identity, demographics, free text, child-authored content,
and inconsistent totals. One submitted file may contain 1–200 ballots. Every
non-skipped ballot must contribute one or two selections.

The hosted polls remain in-memory until an adult deliberately submits the
temporary tally. Page load, selection, and local ballot recording make no
request. Direct submission sends the same strictly validated aggregate as the
saved-file handoff and locks the local session after a green accepted receipt.
The downloadable bundle remains offline and strips all direct-submit controls.

## Stored data map

The application database contains one cumulative row for the round:

| Stored column | Purpose |
|---|---|
| `round_id` | Keeps totals attached to the published poll round |
| `ballot_count` | Cumulative number of facilitated ballots |
| `skipped_count` | Cumulative number of skips |
| six named count columns | Cumulative selections for each published concept |

There is no submission table and no timestamp, request ID, account, session,
cookie, IP address, user-agent value, referrer, demographic, free-text field,
individual ballot, or child-created content. The endpoint does not read
`request.cf`, `User-Agent`, `CF-Connecting-IP`, `X-Forwarded-For`, or similar
headers and does not write application logs.

Cloudflare necessarily processes ordinary connection data to deliver and
protect the website. That limitation is disclosed beside the submit control.
Worker observability is disabled. Do not enable request-body logging, Logpush,
or application observability for this endpoint. Operational review must use
only the cumulative D1 row.

## Deliberate limits

- There is no public live-results endpoint; running totals could bias later
  participants. Publish a complete snapshot after the round closes.
- With no identity, cookie, IP storage, timestamp, or submission ID, MakeJoy
  cannot automatically detect a facilitator submitting the same file twice.
  Each surface says to submit once and locks its in-memory session after success.
- Same-origin checks, a 2 KiB body limit, strict validation, and a 200-ballot
  batch cap reduce accidental and low-effort abuse. They are not proof that a
  public aggregate is representative.
- The Worker rate limiter uses one constant round key—not an IP, user agent,
  cookie, device, or person—and allows 12 attempts per 60 seconds in each
  Cloudflare location. It is a permissive, eventually consistent abuse brake,
  not an accounting system. A burst in one location may briefly delay other
  facilitators there; the downloaded aggregate remains safe for a later retry.

## Publication gate

Before accepting a real aggregate:

1. create a dedicated D1 database and apply the checked-in migration;
2. bind it as `POLL_DB` to the dedicated Pages preview;
3. keep the relevant `POLL_####_COLLECTION_OPEN=false` until every other gate
   passes;
4. serve the handoff over HTTPS on the same origin as the endpoint;
5. verify the constant-key `POLL_RATE_LIMITER` binding and disabled Worker
   observability;
6. submit a synthetic aggregate, query the one-row table, and erase the
   synthetic row before opening the real round;
7. verify the participant poll still completes with networking disabled;
8. verify the privacy disclosure remains immediately beside Submit; and
9. set only that round's `POLL_####_COLLECTION_OPEN=true` deliberately, then
   return it to `false` when the round closes.

This approval does not authorize accounts, individual ballot storage, comments,
uploads other than the aggregate JSON, telemetry, advertising, fundraising, or
any broader child-facing network request.

## Temporary direct-submission exemption — 2026-08-05

The operator authorized direct online polling until further notice. The scope
of the exemption is the transport step only: after an adult records one or more
responses into temporary cumulative counts, the hosted page may POST that exact
aggregate directly without requiring a download and re-upload.

The exemption does not authorize a response table, individual ballot records,
identity, accounts, cookies, demographics, child-authored content, free text,
timestamps, device identifiers, IP storage, user-agent storage, analytics, or a
public live-results endpoint. Cloudflare necessarily processes ordinary
connection data. MakeJoy stores only the same cumulative round rows already
approved. A one-ballot aggregate necessarily reveals its fixed-choice pattern
during the request, but the application immediately adds those values to the
cumulative row and retains no response or request record. The offline bundle
remains network-free.

## Parent direction extension — 2026-08-05

The operator additionally authorized minimum, parent-provided direction data.
This is implemented as a distinct adult-only round,
`fd-parent-direction-round-01`, so its totals cannot be confused with the
adult-facilitated art-preference round.

The parent endpoint accepts exactly one fixed round ID, one ballot total, three
question objects, three skipped totals, and eighteen fixed option totals. It
rejects individual ballots, identity, contact details, demographics, age,
child responses, free text, timestamps, submission IDs, IP fields, user-agent
fields, and every other extra field. For each question, the server proves that
the selection total fits the published choice limit and skip count. It also
requires at least one non-skipped question-response per ballot in aggregate.
That is a necessary plausibility check, not proof of individual combinations;
the in-browser poll enforces the per-ballot rule before immediately clearing
the choices, and the server deliberately never receives those combinations.

Accepted values are added directly to one row in
`first_discovery_parent_direction_totals`. The table has no identity,
connection metadata, timestamp, submission ID, demographic, free-text, or
individual-ballot column. The same provider-processing disclosure and disabled
application-observability rule apply. The exact public contract is recorded in
`parent-poll-aggregate-schema.json`; facilitation and decision limits are in
`PARENT-POLL-CHARTER.md`.

## Deployment verification — 2026-08-05

- Public poll directory: `https://poll.makejoylabs.org/`
- Public adult-facilitated art poll: `https://poll.makejoylabs.org/poll-0001`
- Public adults-only direction poll: `https://poll.makejoylabs.org/poll-0002`
- Provider fallback: `https://first-discovery-art-poll.v-matthew95.workers.dev/`
- Dedicated Worker: `first-discovery-art-poll`
- Dedicated D1 database: `first-discovery-art-poll` in WNAM
- Deployed binding report: `POLL_DB`, constant-key `POLL_RATE_LIMITER` at 12
  attempts per 60 seconds, static `ASSETS`, and independent Poll 0001 and Poll
  0002 collection switches
- Worker observability: disabled in checked-in deployment configuration
- Static HTTPS verification: CSP, no-store, no-referrer, no camera, microphone,
  geolocation, payment, USB, embedding, or cross-origin connection capability
- Fail-closed verification: a valid aggregate received HTTP 503 while the
  collection switch was false and the D1 table remained empty
- End-to-end verification: the same synthetic three-ballot aggregate was sent
  once through each public hostname; the single cumulative row correctly held
  six ballots, two skips, and doubled concept counts
- Cleanup: the exact synthetic round row was deleted and a follow-up query
  confirmed zero stored rows before community collection opened
- Final checked-in deployment configuration: collection switch open, with the
  same D1 and constant-key rate-limit bindings verified by Wrangler at deploy
- Parent direction pages verified at `/parent` and `/parent-submit`; parent
  endpoint verified at `/api/first-discovery-parent-poll`
- Parent schema migration applied without altering the art totals table
- Parent endpoint end-to-end verification accepted four synthetic ballots over
  the two public hostnames, produced the exact cumulative totals, rejected an
  extra `name` field with HTTP 400, and wrote no row for the rejected request
- Parent synthetic row deleted after verification; follow-up queries confirmed
  both parent and art totals tables contained zero community rows
- Deployment version containing the parent extension:
  `0fc0062a-0e65-4740-8aa4-57c457677008`
- Direct-submit controls verified end-to-end through both live browser pages.
  Each displayed a green accepted receipt and locked its local session.
- The direct art request produced exactly one synthetic `cut-paper` count; the
  direct parent request produced exactly the three selected synthetic counts.
  Both rows were checked, deleted with exact-value predicates, and follow-up
  queries confirmed zero rows in both tables before community polling resumed.

The duplicate synthetic submission was deliberate hostname verification and
also demonstrates the documented limitation: without a submission identifier,
the service correctly adds the same aggregate twice. No real ballot was part of
deployment testing.
