Skip to content

About

Universal Polling — a free, open-source group scheduler (when2meet/Doodle). Create a poll, share a link, find a time that works for everyone.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Universal Date Polling

A free, open-source group scheduler — the simple "find a time that works for everyone" tool. Create a poll, propose some dates and times, share a link, and watch the best slot rise to the top as people tick their availability.

Live: opensource.unisim.co.uk/polling

Part of the Universal Apps suite by UNI SIM.

How it works

  • Create a poll — title, candidate dates/times, a timezone (defaults to yours). The host signs in with a Universal ID, confirms an email address with a one-time code, or — once the server supports it — skips both and gets a private admin link (/p/<id>#admin=<token>) to manage the poll from any device. The token lives in the URL fragment, so it never reaches a server log; the database keeps only its SHA-256 hash (src/lib/adminLink.ts). Email is then only needed for response alerts and booking pages.
  • Share the generated link. Anyone with it can respond — no account needed.
  • Respond — tick each slot you're free or if-need-be. Times are shown in the poll's timezone, with your own local time alongside if they differ. Optionally leave an email to be notified when the host confirms a time — only the poll's host can read it back (for their calendar guests and "Copy email"), and it's never shown to other respondents. Each answer gets an edit token (platform migration 0284): the server hands it back once and keeps only its SHA-256, the page keeps it on the device and offers it as Your edit link (/p/<id>#edit=<token>), and changing, removing or re-addressing that answer needs it — so typing someone else's name can't overwrite their answers (src/lib/responseToken.ts). Answers from before tokens stay updatable by name until a save claims them. The host can remove any answer.
  • Decide — the results view tallies every slot, shades it by how many people are free, and badges the winner(s).
  • Change the times — straight after creating a poll, "← Change the times" takes the host back to the form, for as long as nobody has answered. While they edit, the poll page tells visitors "The host is just changing the times, please check back shortly" and opens by itself once they save. The server refuses answers meanwhile, and an edit left open for ten minutes lapses on its own.
  • Confirm — the host (and only the host) can click "Confirm this time" on a result slot. Everyone with the link then sees a prominent "Confirmed" banner with the chosen date/time. From that banner the host can email every respondent who left an address the confirmed time, with a .ics invite attached (the notify-poll-respondents Edge Function; never sent automatically — always an explicit host click) — or Copy email to send it from their own mailbox: a preview with the recipients, subject and an editable message, each with a copy button. Once a time is confirmed the answer form and the results fold away, one click from open again.
  • Come back to it — signed in as the host, the create page lists your active polls above the form (collapsible, and collapsed to a one-line header from three polls up), each with how many people have replied, the confirmed time if you've picked one, when the link expires, and a copy-link button — and a Delete, with "delete all" for the expired ones in a single step. A poll id is ten random characters, so before this the only route back to a poll was the link you kept.
  • Your logo, up front — a signed-in host's square company mark (or a logo picked in the Branding box) sits beside the app mark at the top of the create page, so you can see whose brand the poll will carry before you send it.
  • Suggest times — with a calendar connected, one click fills the poll in from the host's own free time: 4 options, weekdays only, between 10:00 and 16:00 in the poll's timezone, and at most one morning and one afternoon on any given day. They're ordinary slots once added — drag one to move it, click to remove it, click again for four more.
  • Add to calendar — each result slot (and the confirmed banner) has an "Add to calendar" button: Google Calendar, Outlook, or an .ics download (Apple Calendar, Outlook desktop). Generated entirely client-side, in the poll's timezone. On the confirmed banner, the host's Google and Outlook links pre-fill the guests with everyone who was free (or free if need be) then.

More options

A collapsible "More options" panel on the create screen covers:

  • Booking-page colour — pastel orange / blue / pink / green themes.

  • Link validity — 7 / 30 / 90 / 180 days. There is deliberately no "never expires": polls are public and link-shared, so respondents' answers shouldn't live on the server forever. Once the link expires the poll is read-only, and 30 days after expiry a daily server job deletes it with its answers, respondents' email addresses and calendar records (the host can delete it sooner). The app shows the countdown on the expired banner and in "Your polls".

  • Timezone — defaults to the host's, override to any IANA zone.

  • Privacy — the website sends the SDK's anonymous daily open count (once a day, the app's name and the kind of platform, no identifier; the old presence beat and its install id were retired in SDK 0.187.0); the iPhone, Android and desktop apps don't send it (openCount in src/main.tsx). The phone builds' store answers are in ios/App/App/PrivacyInfo.xcprivacy and store-assets/data-safety.json; keep them in step with the code and the in-app knowledge base.

Host calendar integration (check the host's own availability while building a poll) is a planned follow-up — see the suite docs.

Stack

Vite + React 18 + TypeScript, Tailwind CSS v4, and a PWA service worker. Poll data and host email verification use the shared suite Supabase project (tables polls + poll_responses, migration 0025_polls.sql in backoffice/universal-platform). The shared navbar comes from @unisim/sdk.

Develop

cd Universal_Apps/Universal_Date_Polling
cp .env.example .env.local   # fill in the shared project's URL + anon key
npm install
npm run dev

Build: npm run build (outputs dist/, served under /polling/ in production). Deploy is a Git-connected Cloudflare Pages project behind the opensource.unisim.co.uk portal Worker, exactly like the other Universal Apps.

The iPhone app

The same React app ships as a native iOS app through Capacitor (uk.co.unisim.polling). Sideloaded only — it is not on the App Store.

cd Universal_Apps/Universal_Date_Polling
npm run cap:sync
npm run cap:open:ios

⚠️ Always go through npm run cap:sync, never a bare npx cap sync. The native build is a different Vite mode (--mode desktop): base ./ instead of /polling/, and no service worker. Sync the ordinary web build instead and the app installs, launches, and shows a blank screen — every asset URL is a 404 against the capacitor://localhost origin, while Xcode still says BUILD SUCCEEDED and the copied bundle is gitignored so nothing local disagrees. cap:sync builds the right mode and then runs npm run check:mobile-bundle (scripts/verify-mobile-bundle.mjs), which fails if the copied index.html holds a root-absolute asset URL or a service worker.

Two things the shell changes that the web never sees, both in src/lib/appUrl.ts and src/lib/saveFile.ts:

  • The running origin is capacitor://localhost, so any URL built from window.location is useless — an unopenable poll link, or a sign-in email Supabase redirects to the suite hub because no allowlist can hold that origin. The hosted site stands in wherever the origin is not a real one.
  • An anchor's download attribute is ignored in a WKWebView: no error, no console warning. The .ics goes to the iOS share sheet instead.

The app icon and launch image are generated from the canonical mark — from backoffice/universal-platform, node scripts/app-marks/native-icons.mjs Universal_Date_Polling. Do not hand-edit them, and note iOS applies its own squircle mask, so the source art must be a full-bleed square with no alpha.

Each build bakes the commit SHA into a <meta name="build-sha"> tag and logs build: <sha> to the console at startup, so you can tell which build is live in-browser. On Cloudflare Pages the SHA comes from CF_PAGES_COMMIT_SHA; locally it falls back to the git short SHA (or dev).

Licence

AGPL-3.0-or-later, with an added permission for app-store distribution. © Universal Simulation Ltd.

About

Universal Polling — a free, open-source group scheduler (when2meet/Doodle). Create a poll, share a link, find a time that works for everyone.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages