Skip to content

Results saving and marketing

Technical specification of the e-mail capture phase - the card that offers the result link by e-mail and an optional marketing consent - and of the send-and-forget endpoint behind it.

Docs: Results saving and marketing. | Design: Figma

The card empty, and with an address entered

Kind

Mixed

Scope

Two halves, specified together because the promise printed on the card is only true if the back-end keeps it.

  • Front-end - the e-mail capture phase of the questionnaire: one card with a heading, an e-mail field, an unticked consent checkbox, the privacy promise and one button that is "Pomiń" until the address is valid and "Wyślij i zobacz wyniki" after. The card only collects: it hands the address and the consent to results calculation and makes no request itself.
  • Back-end - one endpoint that sends one message with the result link and forgets the address, and one marketing list that holds addresses and nothing from the quiz. No such back-end exists today, so this half is the contract for whoever builds it. Until an endpoint is configured the phase is switched off and the questionnaire runs without it.

The request to the endpoint is made in the next phase, once the result has been stored, so a link is never mailed for a result that does not exist. The request and its answers are specified here; when it is made and what the taker sees if it fails are in the engaging loader.

What it does not cover, and where that lives:

  • The order of phases, and what back and reset do in general - the phases model. This spec only says what the two controls do while this card is on screen.
  • What a session keeps over a refresh, and that nothing is shared across devices - session and data.
  • The value the progress bar shows - progress and pacing.
  • Submitting the answers, making the link request and telling the taker when it fails - the engaging loader, the phase that follows this one.
  • The page the link opens - the results module. Today it is the production results page, see the results destination in the engaging loader.
  • Marketing campaigns - what is sent to the list, how often and with which tool. This spec fixes only what the list holds and how an address gets on and off it.
  • The privacy policy itself - a public document. The card links to it.
  • Accounts - accounts. Nothing here needs one or creates one.
  • A back-end task - the repo has no task convention for back-end work. The contract below is the hand-off.

Data

What the card holds

Input Data Meaning Rules
Address Text typed by the taker Where the link is sent Optional. Kept in the tab's memory only, see "Privacy and data handling"
Consent Ticked or not Agreement to marketing content from the foundation Unticked when the card opens. Never ticked by the app
Declared age The age chosen in demographics, if any Whether the card may be shown at all Only whether it is under 18 matters, see "Whether the phase is part of the session"
Endpoint address Configuration (VITE_RESULTS_EMAIL_URL) Where the request goes Absent today. Decides whether the phase exists

A valid address has exactly one @, at least one character before it, a domain after it that contains a dot with at least one character on each side of every dot, no whitespace anywhere, and at most 254 characters. Space around the typed text is removed before the check and before sending. This is a filter against obvious slips, not a proof: the back-end is the final judge.

Texts

Polish source strings. The first eight are drawn in the frames; the last two are not and are set here.

Element Text Source
Pill in the controls "Prawie koniec!" Frame
Heading "Zapisz swoje wyniki!" Frame
Body "Wyślemy na Twój e-mail link do wyników, dzięki czemu łatwo do nich wrócisz." Frame text reworded, see below
Field placeholder "twoj@mail.com" Frame
Consent "Wyrażam zgodę na przetwarzanie moich danych osobowych w celu przesyłania mi treści marketingowych przez Fundację Generacja Innowacja. Polityka prywatności." Frame. "Polityka prywatności." is a link
Promise "Dbamy o Twoją prywatność, Twoje dane osobowe (w tym adres e-mail) nigdy nie będą powiązane z danymi o Twoich poglądach." Frame
Button, no valid address "Pomiń" Frame
Button, valid address "Wyślij i zobacz wyniki" Frame
Name of the field for assistive technology "Adres e-mail" Set here
Hint under the field "Wpisz pełny adres e-mail, na przykład twoj@mail.com." Set here

The frame's body reads "Wyślemy na Twój e-mail link do wyników, dzięki czemu będziesz mógł do nich łatwo wrócić." The verb form "będziesz mógł" addresses a man. The source string says the same without a gendered form, by the rule the checkpoint cards follow.

The request

At most one request per session. Results calculation makes it right after the survey API has stored the result, and only when the taker gave an address on the card. It fills in the result identifier and the language; the card supplies the address and the consent.

Field Data Meaning Rules
email Text The address Required. Trimmed
resultId Identifier (UUID v4) The result the link opens Required
marketingConsent Yes or no Whether the box was ticked Required
consentWording Short identifier Which consent text the taker saw Sent only with consent. Changes whenever the consent text on the card changes
language pl or en The language of the message Required

It is a POST with a JSON body to the configured address. The address of the endpoint carries nothing personal: no query string and no identifier in the path, so that a log of addresses anywhere on the way holds neither the e-mail nor the result.

The endpoint gives one of four answers, each with an empty body or a fixed one: accepted (202), invalid (400), too many requests (429) and unavailable (503). The app treats anything else as unavailable.

The request carries no quiz, no answers, no demographics, no session data beyond the identifier, and no text for the message. The back-end builds the link from the identifier and its own configured results address, so a caller can never make it send a link of their choosing.

What the back-end keeps

Entity Data Kept Rules
The address, without consent - Nowhere Used for one message and dropped when the request ends
The address together with the result identifier - Nowhere, ever Not in a table, a queue, a cache, a log, a trace, a metric label or a backup
The result identifier - Nowhere Used to build the link and dropped
Marketing list entry, with consent Address, consent date, consent wording, language Until the address is removed The date is a calendar day with no time of day. Nothing else is stored
Request counter for rate limiting Network address of the caller and a count For the length of the limit window Held apart from the request body. Never written next to an e-mail

A marketing list entry deliberately has no time of day, no network address, no browser details, no quiz and no result identifier. A result records when it was created, so a consent stamped to the second could be matched to the result submitted seconds later; a day cannot.

The message

Part Content
Sender The foundation's own domain, authenticated so that mailbox providers accept it (SPF, DKIM, DMARC)
Subject "Twoje wyniki w myPolitics"
Body "Oto link do Twoich wyników:" and the link, then "Zachowaj tę wiadomość. Nie przechowujemy Twojego adresu razem z wynikami, więc nie możemy wysłać linku ponownie."
Added with consent "Twój adres trafił na listę, na którą Fundacja Generacja Innowacja wysyła informacje o nowych quizach. Możesz się z niej wypisać w każdej chwili:" and an unsubscribe link
Never in it The result, any answer, the name of the quiz, an open-tracking image, a rewritten or tracked link

The English message says the same. A request with a language the back-end does not know gets the Polish one.

Interface

Direction Name Shape Notes
In Declared age "Under 18", another age, or not declared From demographics
In Endpoint address A web address, or nothing From configuration, read once when the app starts
In Privacy policy address A link The app's privacy page
Out Whether the phase is part of this session Yes or no Asked by the questionnaire before entering the phase
Out Phase finished An event: skipped, or an address given - then with the address and whether the box was ticked Raised once. The questionnaire moves to results calculation
Out Send link The request above Made by results calculation, not by the card. Listed here because the contract is
In Answer of the endpoint Accepted, invalid, too many requests or unavailable Received by results calculation. See "The endpoint"

The address and the consent leave the card once, in memory, for results calculation to put into the request. They are handed to nothing else and written nowhere.

Behaviour

Whether the phase is part of the session

Case Behaviour
No endpoint address is configured The phase does not exist. Demographics lead straight to results calculation, and nothing on screen hints that a card is missing
An endpoint address is configured The phase is shown after demographics, whether demographics were filled in or skipped
The taker declared an age under 18 The phase is left out for this session, as if it were switched off
The taker skipped demographics without picking an age The phase is shown. Nobody is asked their age in order to leave an e-mail
The taker types an address, goes back, and declares an age under 18 The phase is left out when they come forward again. What was typed and ticked is dropped and no link is requested
The configuration changes while a session is open Nothing changes for that session. The address is read when the app starts

The foundation does not process personal data of people under 16. The line is drawn at 18 all the same: every age under 18 picked from the demographics list is treated as too young. It costs sixteen- and seventeen-year-olds the saved link.

The button

Case Behaviour
The field is empty "Pomiń", drawn as a text button
The field holds text that is not a valid address "Pomiń"
The field holds a valid address "Wyślij i zobacz wyniki", drawn as the filled button. "Pomiń" is not drawn
The address stops being valid while typing The button turns back into "Pomiń" at once
The consent box is ticked or unticked The button does not change. Consent never enables, disables or renames it
"Pomiń" pressed The phase finishes as skipped. Whatever is in the field or the box is dropped, and no link is requested later
"Wyślij i zobacz wyniki" pressed The phase finishes with the address and the consent handed over. The card makes no request and does not wait for one
Either button pressed twice quickly The phase finishes once
The taker wants to skip with a valid address in the field They clear the field, and "Pomiń" returns

There is one button in every state, so a taker is never asked to choose between two ways forward. The card makes no request, so it cannot fail and never holds anyone back.

The card never says that a link was sent. Its label promises a send and the results, and the send follows seconds later, once the result exists. If it fails, the taker is told once, on the loader, before they leave for the result.

The field

Case Behaviour
The card opens The field is empty and shows the placeholder. It is not focused by the app, so a phone keyboard does not cover the promise and the button
The taker types or pastes The button follows the validity of the text after every change
The browser fills the field in The same as typing
The field loses focus holding text that is not a valid address The hint appears under the field
The taker presses Enter with a valid address The same as pressing "Wyślij i zobacz wyniki"
The taker presses Enter with text that is not a valid address Nothing is handed over and nothing is skipped. The hint appears
The taker presses Enter in an empty field Nothing happens
The text becomes valid or the field is emptied The hint disappears

A typed address is drawn in bold, as the frame shows, so a slip is easier to see before moving on.

Case Behaviour
The card opens The box is unticked
The box or its text is pressed The box toggles
"Polityka prywatności." is pressed The privacy policy opens in a new tab. The box does not toggle and the card keeps what was typed
The address is given with the box ticked The request will say so, and the back-end adds the address to the marketing list
The address is given with the box unticked The link is requested all the same. The address is used once and kept nowhere
The box is ticked and the taker skips Nothing is requested and no consent is recorded. Consent without an address is nothing
The promise is pressed Nothing happens. The promise is not part of the checkbox

The promise is drawn under the consent, where the frame has it, and belongs to the whole card: it is read out as the description of the e-mail field, not as part of the consent.

Controls while the card is on screen

Case Behaviour
Progress bar Drawn, as in the frame. All questions are behind the taker, so it reads full; the short fill in the frame is a placeholder
Pill "Prawie koniec!"
Back Returns to demographics. What was typed and ticked here is still there when the taker comes forward again
Reset, confirmed Clears the session, the field and the box, and returns to the first phase

The endpoint

Case Behaviour
A well-formed request, the message handed to the mail provider Answers "accepted" with an empty body. The address is dropped
The same, with consent The address is put on the marketing list first, then the message is handed over, then "accepted"
The address is malformed, or its domain cannot receive mail Refuses the request as invalid. Nothing is sent or stored
The result identifier is not a UUID v4, or a required field is missing Refuses the request as invalid
Whether the result exists Not checked. The endpoint has no access to the results; the app makes the request only after the result was stored
More than 10 requests from one network address within an hour Answers "too many requests". Nothing is sent or stored
The mail provider refuses or cannot be reached Answers "unavailable". Nothing is stored; an address put on the list by this request is taken off again
The marketing list cannot be written, with consent Answers "unavailable". No message is sent
The same request arrives twice Two messages. The back-end keeps nothing it could recognise a repeat by
Any answer Never repeats the address or the identifier back, and never says whether the address is already on the list

"Accepted" means the provider took the message, not that it reached a mailbox.

The marketing list

Case Behaviour
A consenting address that is not on the list Added, with the day, the consent wording and the language
A consenting address that is already on the list Kept once. The entry keeps its first date and takes the newer wording and language
An address on the list sends again without consent The entry is untouched. Leaving the box empty is not a withdrawal
The unsubscribe link is followed The entry is deleted. Ticking the box in a later quiz adds the address again
The owner asks for erasure The entry is deleted. There is nothing else to delete
A marketing message bounces for good, or is reported as spam The entry is deleted
The message with the result link bounces Nobody is told. The taker has left and no record ties the address to a result
Two spellings of one address that differ only in letter case One entry. Addresses are stored in lower case

States and lifecycle

The card

State Condition What is possible in it
Empty - Figma "e-mail" Nothing in the field Type, tick, "Pomiń", back, reset
Not valid yet Text in the field that is not a valid address The same. The hint shows once the field loses focus
Address entered - Figma "e-mail entered" A valid address in the field Edit, tick, "Wyślij i zobacz wyniki", back, reset
Left out No endpoint address, or an age under 18 declared The card is never drawn

The first and third states are drawn in Figma, and the first is also the screen of the phase strip. "Not valid yet" is not drawn: it is the same card with the typed text and, once the field loses focus, the hint under it. The card has no waiting state and no failed state, because it makes no request.

A marketing list entry

State Condition What moves it
Absent The address was never sent with consent, or was removed A request with consent that the mail provider accepts
Subscribed Address, day, wording and language are stored Unsubscribe, an erasure request, a permanent bounce or a spam report - each deletes it

There is no "pending" state and no second message asking to confirm. The address is on the list as soon as the link is sent, and the same message says so and carries the way off.

Permissions

Role Can Cannot
Anyone, without an account Ask for one link message per request, to any address, for any result identifier, within the rate limit Choose the text or the target of the link, learn whether an address is on the list, read anything back
The owner of an address Leave the list through the unsubscribe link, ask for erasure Find a result by the address - there is no record to find
Foundation staff who send marketing Read and export the list: address, day, wording, language See which quiz, result or answers an address came with. That link does not exist
Support Explain that a lost link cannot be recovered Look a result up by an e-mail, or resend a link
The endpoint Reach the mail provider and the marketing list Reach the store of results. It needs nothing from it and is not given access to it

A request over the limit, or refused as invalid, gets its answer and leaves no trace of its content.

Rules and constraints

  • Send and forget. The address and the result identifier meet once, inside one request, and are never written down together.
  • Consent is not a gate. The link is sent with the box ticked or unticked, and skipping is always possible.
  • Never pretend. With no endpoint there is no card, not a card that does nothing. Nothing in the app says a link was sent, and a link that could not be sent is said so, once, before the taker leaves.
  • The card collects, results calculation sends. No request leaves the card. The link is requested only after the result is stored, so a mailed link always points at a result that exists.
  • The link never holds the result back. Whatever the endpoint answers, the taker reaches their result.
  • Switched off by configuration. One setting turns the phase on. No code change is needed when the endpoint exists.
  • Encrypted or not at all. An endpoint address that is not https is treated as not configured.
  • A link, never the result. The message holds a web address and no political content, so neither the mailbox nor the mail provider holds a profile.
  • The caller cannot shape the message. Its text is fixed per language and the link is built by the back-end.
  • The hand-over to the mail provider happens inside the request. No queue of ours holds an address waiting to be sent.
  • All or nothing. A request either sends the message and, with consent, stores the entry, or does neither.
  • One address per request. Lists of addresses are not supported.
  • Not supported: resending. The link is requested once. After that nobody can send it again - not the taker, not support.
  • Not supported: confirming the address before the list. One message, one step. It costs us the certainty that the person who ticked the box owns the address; the unsubscribe link in that same message is the remedy.

Invalid and edge input

Input Behaviour
Only whitespace in the field An empty field
Space before or after the address Removed before checking and sending
A space inside the text, as in a pasted "Jan Kowalski jan@poczta.pl" Not a valid address. "Pomiń", and the hint
Two addresses, or two @ Not a valid address
No dot in the domain, or a dot at its start or end Not a valid address
More than 254 characters Not a valid address
Capital letters Valid. Sent as typed; the list stores lower case
Letters outside ASCII Valid on the card if the shape fits. The back-end decides
A well-formed address with a typo in the domain Requested. The link goes to a stranger or nowhere, and nothing can call it back. The bold address in the field is the only guard
An address that belongs to someone else Requested. With consent it lands on the list; the message tells the owner and carries the way off
An endpoint address that is empty, not a web address, or not https The phase is switched off
An endpoint that is configured and does not answer The card works and the taker moves on. The request fails in results calculation, and the taker is told there
An answer of the endpoint that is not one of the four Unavailable
An unknown consent wording in a request Stored as sent. The wording is evidence of what the taker saw, not a value to validate
A request without consent that carries a consent wording The wording is ignored
The page is refreshed on this card The card opens empty and unticked. The address was never stored
The page is refreshed in results calculation, before the link was requested The address is gone with the page, so no link is requested. The loader cannot know one was asked for and says nothing; the result is reached all the same

Privacy and data handling

Political views are special-category data, and an e-mail address is the most direct identity this product ever touches. The rule from privacy and legal is that the two never meet in anything that lasts.

In the browser:

  • The address and the tick live in the tab's memory only. They are not written to the session that survives a refresh, to any other storage, to a cookie or to the address bar.
  • They are dropped as soon as they are used: when the endpoint has answered, whatever the answer, and also on skip, on reset, on a refresh and when the tab closes.
  • They cross one boundary: from the card to results calculation, in memory, to be put into the one request.
  • No analytics event carries the address, any part of it, its domain, its length or a value derived from it. Events may say that the card was shown or skipped, that a link was requested, accepted or failed, and whether consent was given.
  • Error reporting and session recording never capture the field or the body of the request.

In the back-end:

Place Rule
Access logs, at every hop we run No line is written for this endpoint
Application logs Nothing from the request body. Counts of accepted, refused, limited and failed requests are allowed, with no label taken from a request
Error traces Reported with the body, headers and local values removed. An error text from the mail provider that names the recipient is not passed on
Analytics No event is sent from this endpoint
Queues, caches, temporary files None hold the address or the identifier
Backups Cover the marketing list and nothing else, because nothing else exists
The mail provider Open and click tracking are off for this message, the message content is not retained after delivery, and processing stays inside the European Economic Area
The marketing list Address, day, wording and language. It is never joined to results, answers, demographics, analytics or a network address

What remains after a request: a result under a random address, which exists with or without this card, and, if the box was ticked, one line on the marketing list. The message in the taker's own mailbox is the only place the address and the link sit together, and it is theirs.

Failure modes

Failure Behaviour
The endpoint cannot be reached, or does not answer in time The result is not held back. The taker is told on the loader that the link could not be sent, then goes to their result
The request reached the endpoint and the answer was lost The taker is told the link could not be sent although a message may arrive. It is the smaller of the two possible errors
The endpoint is down for a long time Every taker who gives an address is told the link could not be sent. Removing the setting switches the phase off until the endpoint is back
The mail provider accepts and the mailbox rejects or files it as spam Invisible to us and to the app. The taker has no link unless they kept the results page open
The answers cannot be stored in results calculation No link is requested. A link is never mailed for a result that does not exist
The result is stored and its calculation is slow The message may arrive before the result is calculated. The link opens the results page, which waits for it
The marketing list is unavailable Requests with consent fail and the taker is told the link could not be sent; requests without consent work
A rate limit hits takers who share a network address They are told the link could not be sent and reach their result
The endpoint is used to flood one mailbox Limited per network address only. No limit per recipient exists, because it would mean remembering recipients

Non-functional

Rendering contexts and accessibility

  • The card takes the width it is given and is checked at the narrowest phone widths: the consent and the promise wrap, and nothing scrolls sideways.
  • The field has a name for assistive technology, is announced as an e-mail field, and lets the browser offer the taker's saved address.
  • The checkbox is a real checkbox whose label is the consent text. The link inside it is reachable on its own from the keyboard.
  • The hint is announced when it appears.
  • The change of the button from "Pomiń" to "Wyślij i zobacz wyniki" is a change of its name, so assistive technology reads the new one.
  • Focus order follows reading order: back, reset, field, checkbox, privacy link, button.

Limits

  • The endpoint answers within 5 seconds in the usual case. The app gives up after 10.
  • 10 requests per hour from one network address.
  • Retention of the address without consent: none.

Dependencies

Build order: the card needs the questionnaire screen and its session first; the endpoint needs nothing from the app and can be built at any time. The phase stays switched off until both exist.

Relies on:

  • Results saving and marketing doc - the idea this implements.
  • Phases model - the place of this phase in the sequence, and the controls bar it sits under.
  • Session and data - what a refresh restores, and that the address and the tick are not part of it.
  • Demographics - the declared age.
  • Privacy and legal - the separation this spec holds, and the age below which no personal data is processed.
  • A mail provider and a store for the marketing list - neither is chosen here.

Relied on by:

  • Engaging loader - the phase that follows. It stores the result, makes the link request with what this card collected, and tells the taker when the request fails.
  • Accounts - which counts on a saved result arriving by e-mail.