Build Doodle
KINDAreplaces $15/mosaves $180/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/polls | Named participants and their availability. |
DEFAULT_VISIBILITY | required | hidden | Whether participants see each other's answers. Availability reveals more than people expect. |
INVITE_EXPIRY_DAYS | required | 30 | A poll link lists named people and gets forwarded indefinitely. |
POLL_RETENTION_DAYS | required | 180 | A scheduling poll is not an archive. |
NOTIFY_ON_EVERY_RESPONSE | required | never | Present as a constant. Twelve emails for a twelve person poll is a worse problem. |
SMTP_URLsecret | required | smtp://user:pass@host:587 | Completion and finalisation notifications. |
The build, in order
Times that mean the same moment
A slot shown to someone in another country is the same moment.
The slot is a moment. Local times are a rendering.
Files
src/options.tsA local time that does not exist should be caught before anyone answers.
Files
src/timezones.tssrc/timezones.test.tsA bare time in a cross-border poll is an ambiguity, not information.
Files
src/display.ts
done when · tick each as it passeswatch 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.
Responding without an account
A participant answers in under a minute with no signup.
The friction is what costs you the responses.
Files
src/participants.tsIf needed is genuinely different information from yes.
Files
src/responses.tsTwo people called Sam is normal and silently merging them loses an answer.
Files
src/participants.ts
done when · tick each as it passeswatch 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.
Who can see whose availability
A poll's visibility setting is enforced where the data is fetched.
In the query. Interface hiding is not hiding.
Files
src/visibility.tstests/visibility.test.tsAggregate information is useful and reveals much less.
Files
src/results.tsMaking a hidden poll visible exposes answers people gave in confidence.
Files
src/polls.ts
done when · tick each as it passeswatch 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.
Choosing a time and telling everyone
Finalising sends one clear notification, not a stream.
Twelve emails for a twelve person poll is a worse problem.
Files
src/notify.tsAnd show that is what happened, rather than a bare ordering.
Files
src/ranking.tsIt is the one message that must not be misread.
Files
src/notify.ts
done when · tick each as it passeswatch 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.
Links expire and data goes awayproduct builder
A poll link stops working and the names do not sit around forever.
They contain a list of named people and get forwarded indefinitely.
Files
src/invites.tsA scheduling poll is not an archive.
Files
src/retention.tsThey gave a name and their availability. They can take it back.
Files
src/participants.ts
done when · tick each as it passeswatch out- A poll link is a bearer credential exposing a list of named people and their availability, and those links end up in group chats and forwarded email indefinitely. Expire them, and make expiry the default rather than a setting the organiser has to find.
That is the whole plan for Doodle. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Calendar integration showing real availability.
- Recurring meeting scheduling.
- Paid booking pages.
- Mobile applications.
- participant familiarity, calendar connections, reminders, branding, and hosted delivery
- multiple calendar providers
- team routing and round-robin
- payments
- timezone edge-case maturity
- Suggesting slots from the responses already given, which shortens long polls
- A calendar file for the finalised time, which is what people actually want at the end
- Reminders to non-responders, worded once and sent once
Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.