I've been on the wrong end of a recurring call at 6am Sydney time for long enough to want a tool that at least admits it's doing that to someone.
The scheduling tools all work well enough. When2meet, LettuceMeet, Crab Fit, Rallly, Doodle: pick one, drag some boxes, get a heat map. Time zones are the finicky part, and the maths is usually the half they get right. It's the UI that trips. You get a grid in your zone with no reminder anywhere on screen of what it means for anyone else, DST shifts land mid-poll and quietly move the boxes people already ticked, and a few of them guess your zone from the browser and never show you which one they picked.
VennMeet is what I built instead. Free, no accounts, one link.
Every one of these tools converts the grid to your time zone. That's the right call. What none of them show you is everyone else's local time at the same time. You're looking at a dark cell at 4pm your time and the page won't tell you it's 7am for one person and 11pm for another. You can usually hover to confirm those people marked themselves available. You can't see what it cost them to do that.
With everyone in one or two zones this is fine. Across four it's the whole problem, and the heat map hides it, because a slot everyone marked looks like consensus.
The second thing, which took me longer to notice, is that availability in most of these tools is one bit. Free or not free. There's no way to say "I can do 7am but I'd rather not". So people mark it and get scheduled into it, having never registered a complaint, or they leave it blank and the group's options shrink for a reason nobody said out loud. Rallly is the exception, it has yes/if-need-be/no, and it's the closest thing to what I wanted.
Two things, both small.
Each person gets their own row, labelled with their city and their clock. Every zone on screen at once. When you're picking the time you can see that Tuesday 9am UTC is Wednesday 8pm for Sam, while you can still pick something else.
Answers are "works" or "if needed". A clean yes scores 1, an if-needed scores 0.5, and the ranking prefers times that work for everyone over times that technically fit. Each candidate slot also carries the least sociable local start hour across the group, so a time that's fine for five people and 6am for one gets labelled 6am, with the person's name against it.
Then you confirm a time and everyone gets an .ics file and a Google Calendar link. That's the part that surprised me most about the category. Rallly puts "finalize date" behind Pro at $4.67/seat/month, and most of the others don't have it at all, so you read the grid and announce the answer in Slack, which is the thread you were trying to avoid.
I mostly wanted an excuse to try Cloudflare Workers properly. I've done a fair bit of Lambda, and Workers is the same idea with even less setup: no VPC, no cold start worth measuring, no build artefact to ship anywhere. wrangler deploy and it's live in about eight seconds. The rest of it (Hono, React 19, Tailwind, Zod) is the boring choice in each slot.
D1 is the part I was most sceptical about, and it's fine for this. It's SQLite, you write migrations, they run. Meetings, people and answers, three tables. A nightly cron deletes anything more than 60 days past its last proposed window, which is about six lines and means the privacy page can say something true and short.
The whole thing is one worker serving the API and the static assets together, with run_worker_first for /api/* and /m/* so meeting links hit the worker before the asset handler.
No accounts anywhere, which is the constraint that shaped most of the rest. There's no user table, no session, no OAuth, no password reset, no email. A meeting is identified by an id plus a token: 32 random characters for the organiser, 24 for the share link, drawn from a 31 character alphabet with crypto.getRandomValues.
The token only ever lives in the URL fragment, after the #. Browsers don't send fragments to the server, so it stays out of referrer headers, access logs and anything sitting in front of the worker. The client reads it on load and passes it in a header. Prerendering makes that easy, since the shell is static and doesn't need the token to render anything.
The other half of the trade has no fix. No accounts means no notifications, so somebody still has to chase the people who haven't answered. For a one-off meeting with people outside your org, I'll take that over asking eleven people to sign in with Google.
The first version was a normal SPA, so every route served the same shell. Google saw one page with an empty <div id="root"> and a <title> of "VennMeet" for /, /new and /privacy alike, which is a fine way to build a tool nobody finds.
Fixing it properly meant a second Vite build targeting Node SSR, deliberately without the Cloudflare plugin, since that targets workerd and fights renderToString. The prerender script walks the route list, renders each one with a memory history, and stamps the head tags and JSON-LD in. Output is nested index.html files plus a sitemap generated from the same page list that drives the titles, so those can't drift apart.
One bug took a while to spot. After prerendering, /index.html became the full landing page, and the worker was serving that same file as the shell for every /m/* meeting link. So every private meeting page carried the landing page's copy and four blocks of structured data. Now there's a separate bare meeting-shell.html with a noindex tag and an empty root, and the worker serves that instead.
The other thing prerendering forces you to notice is anything that reads the environment during render. The landing page has a live demo that calls detectTimeZone() and computes dates, so a naive prerender baked my build machine's zone and the build date into every page. It's behind a useSyncExternalStore hook that returns false on the server now, with a static placeholder until hydration.
vennmeet.app. It's free, there's nothing to sign up for, and if you don't like it you've lost about a minute.
If your group is in one city, use When2meet, it's good and it's faster for that. If you need self-hosting, Crab Fit is GPLv3 and has guides for it. I wrote up where each of those wins properly, including the cases where they beat what I built.