Build Guide
Two working replacements you build yourself and host on a domain you already have — a booking page instead of Calendly, and a group time poll instead of Doodle. Both run on Google Calendar. Neither one costs anything per month.
The whole design rests on one idea — your calendar is the database. Open time slots are just calendar events, and the poll answers live in a spreadsheet. That single choice is what keeps this free and keeps it yours.
Two tools that share one backend. Build the booking calendar first — the poll reuses its Apps Script project, so the second build is far shorter than the first. You can also build either one by itself.
Someone opens your page, sees your open times, picks one, and fills out a short form. The slot vanishes for everyone else, they get a confirmation email and a calendar invite, and you get a notification. You control availability by creating recurring calendar events titled … - OPEN. Nothing else to maintain.
You propose a few possible times and get one share link. The group votes — works, works if needed, or no — and a grid shows who chose what. You also get a second, private organizer link. Click one time from it and the meeting is created with a video link, invitations going to everyone who voted.
Three moving parts, both tools. The first is a static web page you upload to any host. The second is a Google Apps Script Web App that runs as your calendar's Google account and does the calendar reading and writing. The third is Google Calendar, where the data lives. No database server, no login screen, no framework, no monthly fee.
Hand this to Claude, or another capable AI coding assistant. Fill in the bracketed parts first. It is also in the kit as prompt-1-booking-calendar.txt, ready to copy.
Build me a free, self-hosted booking calendar to replace Calendly.
What it does. Visitors come to a web page, see a list of open dates and times, pick one, fill out a short form, and submit. On submit, the slot is instantly claimed, the booker gets a branded confirmation email and a calendar invite, and I get an admin notification.
My setup. I have a Google account at [scheduling@yourdomain.com], and its calendar is the one bookings should read from and write to. I have web hosting at [host name] where I can upload static files, or I need you to recommend a free option. I want to host it at [book.yourdomain.com].
The booking flows I need. [Describe each one — for example, one public flow for prospective clients and one private flow, shared by link only, for existing clients. Say how long each meeting runs.]
Architecture I want (keep it free, no subscriptions). The frontend is static HTML, CSS, and JS that I upload to my host. The backend is a Google Apps Script Web App that runs as my calendar's Google account and does the calendar read and write. Availability is controlled by pre-creating recurring calendar events titled exactly [Meeting Name] - OPEN. The script lists those as open slots, and on booking it rewrites the title to claim the slot and adds the booker as a guest.
Availability, the other way. Instead of one recurring event per slot, offer me the option of one recurring daily block titled [Meeting Name] - OPEN that the script slices into start times. Use a fixed step, and pick a step that goes evenly into the meeting length plus the buffer so bookings pack without odd gaps. In that design a booking becomes its own event and the block is left alone; the busy check hides the taken time. Explain the trade-off and let me choose.
Page shape. [Pick one: a flat list of upcoming times, or a month calendar where I click a day, see its times, then fill in the form. If you are not sure, ask me.]
Booking rules I want. [Pick what applies — a minimum notice window such as 24 hours, a buffer around every booking such as 15 minutes, a cap on bookings per day, and how far ahead people can book. Put each one in a named constant at the top of the script so I can change it later.]
Details the script must get right.
Calendar, and set the project time zone in Project Settings. Every date in every email comes from that setting.singleEvents: true and loop on nextPageToken. Without both, recurring series never expand and a busy calendar gets cut off.conferenceDataVersion: 1 whenever you create or remove a Meet link.Testing. Add a LIVE constant, false by default, that suppresses all guest emails until I flip it to true for go-live. Add a testSetup() function I can run from the editor that checks calendar access, counts my OPEN blocks, and lists the next few bookable start times.
Build me the HTML pages, the shared CSS and JS, the complete Apps Script .gs file, and a DEPLOY.md with setup instructions. Show me how to deploy the Apps Script Web App and wire its /exec URL into the frontend. Use my colors and fonts — [paste your brand specs, or write "ask me"]. Ask me any clarifying questions before you start.
Why it works. It specifies the architecture, not just the goal. Naming the static-frontend-plus-Apps-Script-plus-calendar pattern is what makes the build free and durable. Leave it out and the assistant reaches for a paid service or a server you have to maintain.
Also in the kit as prompt-2-meeting-poll.txt. If you already built the booking page, tell the assistant to merge these actions into that same Apps Script project so both tools share one /exec URL.
Build me a free, self-hosted group scheduling poll to replace Doodle.
What it does. I propose a handful of possible meeting times and get back one share link. Anyone with that link votes on each time — works, works if needed, or no — with their name and email. A results grid shows who chose what. I also get a private organizer link. When I pick the winning time from that link, the script creates a calendar event with a video link and sends the invite to every person who voted.
My setup. I have a Google account at [scheduling@yourdomain.com] whose calendar this uses and whose address sends the mail. I have static hosting at [host name]. I want the poll pages at [book.yourdomain.com/poll/].
Architecture I want (keep it free, no subscriptions). The frontend is static HTML, CSS, and JS. The backend is a Google Apps Script Web App that runs as that Google account. A Google Sheet is the database — the script creates it on first use and remembers its id in Script Properties, with one tab for polls and one for votes. Every poll gets a random public id and a separate secret organizer token. The actions I need are poll-create, poll (read), poll-vote, and poll-finalize. On finalize, the script writes the calendar event, attaches a video link, and invites every voter. A time that already carries its own join link keeps it and never gets an auto-generated room. If the video service fails, fall back to a Google Meet link so the group is never left without a room.
Guardrails I want.
Features I want beyond the free tier of Doodle. [Keep the ones you want.] A voting deadline, after which votes are refused and I get a nudge to pick a time. An invitee email list — send the invitation at creation, then one automatic reminder to people who have not voted, in the final stretch before the deadline. A "who hasn't voted yet" view that only I can see. Hidden polls, where voters see the totals but only I see the names. An email to me each time someone votes. A check that flags any proposed time that collides with something already on my calendar, before I send the poll out.
Reminders and deadline nudges run from a pollReminders() function on an hourly time-driven trigger. Tell me to install that trigger in DEPLOY.md. Give each poll a flags column so every reminder goes out exactly once.
Optional extras. [Keep the ones you want, or delete this block.]
guestsCanInviteOthers to true, so guests I add from the calendar survive.Build me the vote and results page, the poll creation page, the complete Apps Script .gs file, and a DEPLOY.md with setup instructions. Use my colors and fonts — [paste your brand specs, or write "ask me"]. Ask me any clarifying questions before you start.
Why it works. A poll needs somewhere to keep answers between visits, and naming a Google Sheet as that store is what keeps the whole thing free. The secret organizer token is the other load-bearing detail — it separates the person who picks the time from the crowd who only votes, with no login screen anywhere.
| Need | What to use | Cost | Notes |
|---|---|---|---|
| Calendar account | A dedicated Google account | Free | Use a role account such as scheduling@, not your personal one. Events live on its calendar and mail comes from its address. |
| Backend | Google Apps Script | Free | No server to run. Executes as the calendar account, with a generous free quota. One project can serve both tools. |
| Poll storage | Google Sheets | Free | Created automatically on the first poll. Open it any time to inspect a poll. Section 7 says which cells are safe to edit by hand. |
| Web hosting | Any static host | Free or existing plan | Anything that serves plain files works — your current host, Netlify, Cloudflare Pages, GitHub Pages. |
| Domain | A subdomain of a domain you already have | Existing domain | Something like book.yourdomain.com, with the poll living at /poll/ under it. |
| Video links | Google Meet or Zoom | Free tier fine | Meet is created automatically. For Zoom, either store one standing link in the script or connect the Zoom API. |
| AI assistant | Claude | Your existing plan | Generates the code and guides you through the deploy. |
What you do not need — no database, no paid scheduling tool, no login screen, no JavaScript framework, and no monthly fee.
Open slots are recurring events titled … - OPEN. That keeps it free and keeps you managing availability from the calendar app you already use. You need to be comfortable creating and editing recurring events — that is the one skill this asks of you.
Intro Call - OPEN. A hyphen versus an em-dash, or Open versus OPEN, makes a slot invisible with no error anywhere. Copy your titles, never retype them.
styles.css?v=6 becomes ?v=7, and app.js?v=6 becomes ?v=7 in the same edit. Miss the script tag and you get new CSS against stale JS, which looks like a broken page. The stale copy is often held by your host, not your browser, so an incognito window proves nothing. Only a changed query string gets through.
poll/ by hand, upload files one at a time into the folder you have confirmed, and open the live URL of each file to check it landed.
Calendar. A different identifier makes every calendar call fail. Then open Project Settings and set the project time zone to yours. That one setting drives every date and time the script writes into an email, so a wrong zone means every confirmation is off by hours.
robots.txt file, a noindex tag in every page head, and a noindex header from your host. Decide too what a voter can see — showing names is friendly for a small team, and totals-only is kinder for a mixed group.
Phase 1 — Prep, before any code
Intro Call - OPEN.Phase 2 — Build with the assistant
.gs file, and a DEPLOY.md.Phase 3 — Stand up the backend
.gs code, and save. Under Services, add the Google Calendar API with the identifier exactly Calendar. In Project Settings, set the time zone to yours./exec URL.Phase 4 — Stand up the frontend
… - OPEN calendar events as far out as you want to take bookings. A year ahead is a reasonable default.Phase 5 — Test before you share it
testSetup(). It should report that it can read the calendar, how many OPEN blocks it found, and the next few bookable start times. If any of those is empty, fix that before touching the page.LIVE still false, book a test slot from another browser. Confirm the slot disappears and that your admin notification arrives. Guest emails stay off in this mode, so a test booker's inbox stays clean.LIVE to true, redeploy with Manage deployments → pencil → New version, and book one more test slot. This time confirm the booker's confirmation and calendar invite arrive too. Then decline it the Section 7 way.Phase 6 — Add the poll
POLL_CREATE_KEY as a Script Property, and run the script once from the editor so it authorizes access to Sheets and creates its spreadsheet.pollReminders function, set the event source to Time-driven and the type to Hour timer, every hour. Without this trigger, deadlines and reminders never fire and nothing tells you.| To do this | Do this |
|---|---|
| Decline a booking | Rename the event back to … - OPEN and remove the guest. With no cache, it reopens on the next page load. If your build caches the slot list, which is worth doing on a busy calendar, it can take up to about 10 minutes unless the script clears that cache on every claim and every decline. Ask for that. |
| Skip a date, such as a holiday | Delete that single occurrence, not the series. |
| Change one week's time | Edit the single occurrence, not the series. |
| Add a new weekly time | Create another recurring … - OPEN series. The page picks it up on the next load. |
| Stop taking bookings for a while | Delete or rename the upcoming OPEN events. The page shows nothing available. |
| Fix a poll by hand | Open the spreadsheet the script created. The title, note, deadline, invitee list and privacy cells are safe to edit, and the change is live at once. Never edit the times cell. Votes are stored as position-based lists, so adding or removing a time there misaligns every vote already cast and blocks re-voting. Change times through the edit page if you built one (Section 8), or create a new poll. |
| Change a booking rule | Edit the constant at the top of the script, then Manage deployments → pencil → New version. |
| Change what the script writes to events | Events already on the calendar keep the old shape. Ask for a one-time helper function that updates the existing events with sendUpdates: "none", so nobody gets a fresh invite email for a change they cannot see. |
None of these is needed for a working build. Each one is already written into the prompts as an option; keep the lines you want and delete the rest.
The plain version is a flat list of upcoming times. The other is a month calendar: click a day, see its times, then fill in the form. The flat list is easier to build and fine for a few slots a week. The month view is kinder when you offer many times a day. Prompt 1 asks you to pick. It also offers a second way to set availability: one recurring daily OPEN block that the script slices into start times, instead of a year of individual recurring slots. That means less calendar upkeep, at the cost of a slightly more involved script.
Without it, a poll's times are fixed the moment you create it. With it, the organizer link opens an edit page where you can change the title, note, length, deadline, invitees, privacy and the times while the poll is still open. The one rule that matters: when the times change, every answer already cast has to be carried across by the time it belongs to, never by its position in the list. Get that wrong and people inherit a neighbour's answer.
Add a switch that emails the voters after an edit, off by default so a typo fix sends nothing. The email has to say outright that nobody needs to do anything unless the new list stops working for them. An email that only asks people to check their answer leaves them guessing what checking means.
If everyone who votes is in your zone, skip this. If not, ask for conversion: you enter times in your zone, and the page shows each voter the same times in theirs, with the zone name printed beside them. The stored times never change; only the display does.
The same poll, turned around. Instead of voting on which time wins, every time is a session that will run, and each one holds a set number of places. A person claims one session, the claim is the booking, and they go straight onto that session's calendar event. A session that fills closes itself, and switching to a different session frees the old place. Useful for office hours, workshop cohorts, or any run of repeated sessions. Prompt 2 has two lines for it under Optional extras.
This is the same setup running live booking pages and group polls today. Once it is standing, the running cost stays at nothing.