How to host a booking page on Cloudflare
How to host a booking page on Cloudflare puts Slot on one Worker, stores each booking in D1, and serves the hours from a domain you already own.
How to host a booking page on Cloudflare is the public URL for Slot, the 6-hour Calendly v1. The catalog line is “Your availability rules, one pleasant booking page.”
You pay $12–$20 a person a month. That is $144–$240 a year, and the card says the price scales with the team. The verdict is “Very vibeable”. The first estimate is 6 hours: one meeting type, weekly availability, bookings.
The Calendly teardown is the availability math. The Calendly guide builds the screens and the slot lock. This guide is where that app lives. One Worker, a D1 database named slot, and book.flaviocopes.com.
The hosting post is the general map. Typeform is rows only. Buffer adds a file and a cron. Slot sits with Typeform. A vote grid with no weekly hours is the Doodle guide. Doodle is $7–$9 a person a month, about 3 hours.
How to host a booking page on Cloudflare as one Worker
The Calendly page splits the work into three rows.
The first row is the one you put on a hostname. One meeting type. Weekday hours. A public page. A booking that lands in a table. Six hours.
The second row is 2 days, 16 hours. Google Calendar sync, timezones, reschedule links. The third row is 1 month and up, 160 hours. Team routing, payments, every calendar provider. The non-goal names those, and it also names extra calendar providers.
The product pages put the bill this way. A focused replacement costs your build time plus close-to-zero hosting. D1 is on Cloudflare’s Free and Paid plans. The Workers Paid plan includes 100,000 databases, and another database is $0.10 a month. One database named slot stays inside that allowance.
This catalog is static Astro on Cloudflare Pages. npm run deploy builds dist/ and runs wrangler pages deploy. There is no D1 binding in this repo, because the posts are markdown in git. Slot is a different project. A page that writes a booking needs the Astro adapter, the same way the hosting post says a form that writes a row needs it.
workers.dev is the hostname you get before a zone exists. Cloudflare treats that subdomain as a Free website and points production Workers at a custom domain or a route. The link in the email to Jordan is book.flaviocopes.com.
Where the page, the rows, and the domain split
Every product prompt ships in two flavors: Node.js plus Astro, and TanStack Start. The card lists Astro, HTMX, and D1. This site is an Astro app on Cloudflare, so the pages are Astro and the rows are D1. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. On that tab the database is whatever SQL database you already run, and the custom domain still points at one app.
The guest opens /book/intro-30. The owner opens /admin/bookings. Both requests hit the same Worker. Astro reads the weekly hours, writes the booking, and renders the confirmation. D1 stores the meeting type, the weekly rule, the holds, and the bookings. D1’s get started creates that database with npx wrangler d1 create slot and prints the binding. Nothing in this v1 waits on a queue.
Cloudflare’s custom domains page is the routing rule. You need an active zone on the account, and a Worker to call. A custom domain cannot sit on a hostname that already has a CNAME, and it cannot sit on a zone you do not own. After it attaches, Cloudflare creates the DNS record and the certificate. Every path on that hostname goes to the Worker, so /book/intro-30 and /admin/bookings share an origin.
The binding uses the names from wrangler d1 create slot.
{
"d1_databases": [
{
"binding": "DB",
"database_name": "slot",
"database_id": "replace-after-wrangler-d1-create"
}
]
}
Paste the id the command prints. The hostname is a second block, with workers_dev off so the overview URL is not the one you copy into email.
{
"workers_dev": false,
"routes": [
{
"pattern": "book.flaviocopes.com",
"custom_domain": true
}
]
}
npx wrangler deploy creates that hostname. Run it after the zone for flaviocopes.com is on this account, and after book has no CNAME of its own.
What the remote database holds
The card lists MeetingType, AvailabilityRule, CalendarConnection, and Booking. This host stores the meeting type, the weekly rule, and the booking. CalendarConnection is the 2-day row. Leave that table out, so the Worker has no OAuth client to configure.
The meeting type you seed is intro-30. Title Intro call, 30 minutes, timezone America/New_York, Monday to Friday 09:00 to 17:00, 15 minutes of buffer after, 4 hours of notice (noticeMin 240), and a 14-day horizon. A booking stores a UTC start, the guest name, the guest email, and the guest timezone. A hold stores that same start with an expiry five minutes out. The Calendly guide builds the page. How to prevent double booking is the hold, the batch, and the unique start. Here the rows have to exist in remote D1, which is a different file from the SQLite database under .wrangler.
The screens the hostname has to serve
Three routes. The public page, the confirmation, and the owner list. When one of them 404s on the custom domain and still renders on your laptop, the route is the bug.
Public page
/book/intro-30 shows Intro call, 30 minutes, a date, and start times with a zone label. On Monday 26 October 2026, 14:00 UTC is 10:00 in New York and 15:00 in Rome. The address bar reads book.flaviocopes.com.
Confirmation
Jordan Lee books 13:00 UTC on Tuesday 27 October 2026. The confirmation shows that instant, 09:00 in New York, and 14:00 in Rome. Guests type a name and an email. They do not get an account on this row. The row has to be in remote D1 before you close the tab.
Owner list
/admin/bookings is the same Worker, behind the owner login. Refresh it from a phone, on a network that is not your laptop, and Jordan’s row is still there. An empty phone list next to a full wrangler dev screen means the migration ran with --local and the remote database is still empty.
The path from a local row to the public URL
Build the 6-hour app on your machine first. Create D1, apply the migration locally, book a test slot, apply the same migration remotely, deploy, and book again on the public URL.
The decision is the zone. If flaviocopes.com is not on this account, add it before wrangler deploy. If book already has a CNAME, delete that record first. Cloudflare’s custom-domain page says the attach fails while the CNAME is there, and their migrate-from-routes steps say to delete it, then add the domain.
Cloudflare issues the certificate with the custom domain. If the first HTTPS load fails, reload it before you change the route.
Prompt 1. An Astro app that can take a binding
Copy the Astro prompt from Build your own Calendly. If you write React, copy the TanStack Start prompt instead, and keep the hostname block below. Paste this above the prompt.
Host the 6-hour row on Cloudflare.
Node.js and Astro, with the Cloudflare adapter, so a D1 binding works.
Project name: Slot.
One meeting type, weekly hours, bookings.
Binding name DB. Database name slot.
Leave database_id as a placeholder until I run wrangler d1 create.
Skip Google Calendar, R2, cron, and Durable Objects.
Skip team routing, payments, and a second meeting type.
When the plan lists routes and tables, stop. Do not deploy.
On the TanStack Start tab, the same block names server functions and one SQL database. The custom domain still points at that one app. You add D1 only on the Astro tab, because that is the capability line on the card.
What to check after the scaffold
The Astro config uses the Cloudflare adapter. wrangler.jsonc has d1_databases and no r2_buckets. The dev server shows empty admin screens. The public booking URL 404s until a meeting type exists. A pages_build_output_dir with no adapter is how this catalog ships. Slot needs the other shape.
Prompt 2. The tables, local first
Add the tables for Slot and seed the meeting type.
meeting_types, availability_rules, holds, bookings.
bookings.start_at is a UTC timestamp.
Leave out calendar_connections.
Seed intro-30:
- Intro call, 30 minutes
- Mon-Fri 09:00-17:00 America/New_York
- 15 minute buffer after
- 4 hour minimum notice
- 14 day horizon
Document these two commands and add no extra flags:
npx wrangler d1 migrations apply slot --local
npx wrangler d1 migrations apply slot --remote
Run the local apply. Stop before the remote apply and before deploy.
Done when a local booking is still there after I restart wrangler dev.
What to check after the local booking
Create Intro call if the seed missed it. Book a slot as yourself. Restart the dev server. The row remains. database_id is the id from npx wrangler d1 create slot, not the placeholder text. The migration file is in the repo.
Refuse a weekly window that starts at 17:00 and ends at 09:00. The Calendly guide keeps overnight rules out because DST math breaks on them.
Prompt 3. Attach the hostname
Attach the production hostname. Deploy only after the zone check passes.
In wrangler.jsonc:
- workers_dev false
- routes pattern book.flaviocopes.com
- custom_domain true
Stop if any line fails:
- The zone flaviocopes.com is on this Cloudflare account.
- book.flaviocopes.com has no CNAME record.
- The remote migration has been applied.
Then run npx wrangler deploy.
Done when https://book.flaviocopes.com/book/intro-30
shows Intro call over HTTPS.
What to check after deploy
The address bar is book.flaviocopes.com. The certificate is valid. /admin/bookings on that host asks the owner to sign in. A request to the workers.dev host for this Worker does not serve the app, because workers_dev is false.
If TLS fails on the first try, wait and reload. If deploy says the hostname already has a CNAME, delete that record on the zone and deploy again.
Prompt 4. Book from a phone
Add no features. I will book on the public URL.
Open https://book.flaviocopes.com/book/intro-30 on a phone.
Submit Jordan Lee, [email protected], Europe/Rome,
for 13:00 UTC on 2026-10-27.
Done when /admin/bookings on that same host shows the row
after a refresh, and a second browser gets the slot-taken
response for that start.
Keep the lock rules from the Calendly guide. Leave them as they are.
What to check after the public booking
The phone is on the public URL. The admin list shows Jordan. A second submit does not insert a second row. If the phone shows open slots and the admin list stays empty, the form and the list are reading different databases. One binding, named DB.
Failures the agent ships anyway
A Pages deploy for a form that writes
wrangler pages deploy is how this catalog goes out. Slot writes a row, so the script is wrangler deploy after the adapter is in the Astro config. A guest page that renders and a POST that 404s is a static build.
The local database treated as production
wrangler d1 migrations apply slot --local fills the file under .wrangler. The public Worker reads remote D1. Apply --remote before the link goes out. The D1 docs use those two flags for the same migration.
A CNAME already on book
The custom domain never appears, or deploy stops on that hostname. Delete the CNAME for book, then deploy again. The hostname also has to be on a zone this account owns.
The workers.dev URL in the email
The overview copies slot.<account>.workers.dev. Cloudflare calls that host a Free website for hobby projects. workers_dev: false leaves book.flaviocopes.com as the URL you send.
Admin on a second project
The agent puts /admin on Pages and /book on a Worker. The list and the form then use two databases. Both paths stay on the one Worker that holds the DB binding.
A calendar key in the host config
CALENDAR_CLIENT_ID in the env file is the 2-day row. The non-goal says no extra calendar providers in v1. Remove the key before deploy. The guardrail you do ship is the booking check inside the write, which the Calendly guide already walks.
Deploy, then send one link
Slot wants Astro, HTMX, and D1. The domain is book.flaviocopes.com.
Create slot, apply the migration locally, apply it remotely, deploy, then book from a phone. Export D1 before you cancel Calendly. The card also stands in for Cal.com, SavvyCal, and Acuity Scheduling when you wanted a personal booking link.
The hosting post covers R2 and cron for cards that store a file or publish later. Slot’s first row uses neither.
What to skip on purpose
Leave the zone’s other hostnames alone. This catalog stays on its own Pages project. Slot keeps its own tables.
Skip team routing, payments, and a second calendar provider. Skip a cron whose only job is deleting expired holds. The Calendly guide releases a hold on the next read, after expires_at.
Reschedule links and an .ics file are that 2-day row. Add them after a guest asks. A one-question form in front of the call is Typeform, $29–$99 a month, about 2 hours. The Typeform guide is that form and its table.
Start now
Go to Build your own Calendly. Copy the Astro prompt, or TanStack Start if that is what you write.
Put the host in the same message as the 6-hour row. One Worker, binding DB, database slot, hostname book.flaviocopes.com.
Book one intro call on that URL, from a phone, before you cancel the $12 plan.