Build Doodle

KINDAreplaces $15/mosaves $180/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A scheduling poll whose two real problems are that a time slot means a different wall clock time for every participant and that a poll showing everyone's availability to everyone is a disclosure people do not always expect.

estimated effort weekendthe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLTimeOptions stored as instants, displayed in each viewer's zoneIdentityNo account required, with a durable per-browser identityVisibilityPer-poll setting for whether responses are visible to participantsDeliveryNotification on completion, not on every response

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the notification worker.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Polls, responses and expiring invites with correct instant handling.

    Get it Docker or your package manager. open ↗

    Verify psql connects and stores timestamps with zone correctly

  3. installfree

    Why Zone rules change by government decision and stale data produces a poll that is an hour wrong for some participants.

    Get it Your platform's zone data, kept current, plus a library that handles ambiguous and nonexistent local times explicitly.

    Verify An ambiguous local time during a daylight transition is detected rather than silently resolved

  4. have readyfree

    Why Every claim about time correctness is untestable within one zone, and the interesting failures happen across a daylight transition boundary that different countries cross on different dates.

    Get it Colleagues or friends in three zones, ideally including two that change clocks on different dates.

    Verify A test poll is answered correctly by participants in three zones

  5. decidefree

    Why Whether participants see each other's availability is a disclosure decision, and the default is what almost every poll will use.

    Get it Decide the default and write down why, considering that availability reveals more than people expect.

    Verify A written note states the default and the reasoning

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- polls(id, title, organiser_id, visibility, deadline_at, timezone_hint, finalised_option_id, created_at)
- options(id, poll_id, starts_at_utc, ends_at_utc, ordinal)
- participants(id, poll_id, name, email, identity_hash, responded_at)
- responses(participant_id, option_id, answer) · answer is yes, no or if_needed
- invites(poll_id, token_hash, sent_to, expires_at) · a poll link is a bearer credential to a list of names
- finalisations(poll_id, option_id, decided_at, notified_at)
- Visibility is a poll-level setting evaluated server side, because hiding other people's answers in the interface while returning them in the response is not hiding them.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/pollsNamed participants and their availability.
DEFAULT_VISIBILITYrequiredhiddenWhether participants see each other's answers. Availability reveals more than people expect.
INVITE_EXPIRY_DAYSrequired30A poll link lists named people and gets forwarded indefinitely.
POLL_RETENTION_DAYSrequired180A scheduling poll is not an archive.
NOTIFY_ON_EVERY_RESPONSErequiredneverPresent as a constant. Twelve emails for a twelve person poll is a worse problem.
SMTP_URLsecretrequiredsmtp://user:pass@host:587Completion and finalisation notifications.

The build, in order

  1. Times that mean the same moment

    A slot shown to someone in another country is the same moment.

    1. The slot is a moment. Local times are a rendering.

      Files src/options.ts

    2. A local time that does not exist should be caught before anyone answers.

      Files src/timezones.tssrc/timezones.test.ts

    3. A bare time in a cross-border poll is an ambiguity, not information.

      Files src/display.ts

    done when · tick each as it passes
    watch out
    • A poll spanning a daylight transition can contain a local time that does not exist that day or that happens twice, and naive handling silently shifts the slot by an hour for some participants. Store instants, render per viewer, and detect the ambiguous cases when the poll is created rather than when someone answers wrongly.
  2. Responding without an account

    A participant answers in under a minute with no signup.

    1. The friction is what costs you the responses.

      Files src/participants.ts

    2. If needed is genuinely different information from yes.

      Files src/responses.ts

    3. Two people called Sam is normal and silently merging them loses an answer.

      Files src/participants.ts

    done when · tick each as it passes
    watch out
    • Identifying participants by the name they typed means two people called Sam overwrite each other's responses, and the organiser sees a poll that quietly lost an answer. Use a durable identity alongside the display name, and prompt when a name collides rather than merging.
  3. Who can see whose availability

    A poll's visibility setting is enforced where the data is fetched.

    1. In the query. Interface hiding is not hiding.

      Files src/visibility.tstests/visibility.test.ts

    2. Aggregate information is useful and reveals much less.

      Files src/results.ts

    3. Making a hidden poll visible exposes answers people gave in confidence.

      Files src/polls.ts

    done when · tick each as it passes
    watch out
    • Hiding other people's answers in the interface while the API still returns them means anyone who looks at the network traffic sees the availability of every colleague on the poll, which is exactly the information the setting was meant to protect. Filter server side and assert absence in the suite.
  4. Choosing a time and telling everyone

    Finalising sends one clear notification, not a stream.

    1. Twelve emails for a twelve person poll is a worse problem.

      Files src/notify.ts

    2. And show that is what happened, rather than a bare ordering.

      Files src/ranking.ts

    3. It is the one message that must not be misread.

      Files src/notify.ts

    done when · tick each as it passes
    watch out
    • Emailing the organiser on every response means a twelve person poll sends twelve emails, which is worse than the scheduling problem it was meant to solve. Notify on completion or deadline, and let the organiser opt into more rather than out of it.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.