Library / SDK
dennisklappe/CloudMeet avatar
dennisklappe/CloudMeet

CloudMeet runs a booking system on Cloudflare Pages and D1

Open-source Calendly alternative running on Cloudflare's free tier. Meeting scheduler with Google Calendar and Outlook sync.

550 stars65 forksTypeScriptMIT

At a glance

What is it?
CloudMeet is a TypeScript and SvelteKit meeting scheduler that deploys to Cloudflare Pages with a D1 database and syncs Google and Outlook calendars. It suits a team that wants a self-hosted booking page, and it trades multi-tenant polish for a single-admin GitHub template.
Who is it for?
CloudMeet is a sound choice for a small team or an individual consultant who wants a booking page with Google and Outlook sync without paying for a hosted scheduler, since the whole application fits inside Cloudflare's free tier and the deploy is one GitHub Actions run.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A SvelteKit app split into Pages, D1 and a scheduled Worker

CloudMeet is written in TypeScript and built on SvelteKit with the Cloudflare adapter. The repository layout shows the pieces: a `src/` directory for the SvelteKit application, a `functions/` directory, a `workers/` directory, a `migrations/` directory, and a `schema.sql` file at the root that defines the database. The package.json keywords name svelte, sveltekit, cloudflare and google-calendar, and the dev dependencies pin the Cloudflare Pages adapter and the wrangler toolchain. The storage is D1, Cloudflare's hosted SQLite, and the two database scripts in package.json make the split between local and remote explicit: `db:init` runs the schema against a local database with `--local`, while `db:init:remote` applies the same file to the deployed database. The dev and deploy scripts show the same symmetry, with the dev script serving the built `.svelte-kit/cloudflare` output through wrangler pages dev, and the deploy script pushing that same directory to Cloudflare Pages.

Deployment is a GitHub Actions run, not a server you administer

The documented path to production is a template repository and a workflow run, and there is no server to provision. The README's own sequence is: create a Cloudflare API token using the Edit Cloudflare Workers template with Account and D1 Edit permissions, set up Google OAuth, create the repository from the template, add repository secrets, then run the Deploy to Cloudflare Pages workflow. The application is then live at a pages.dev URL. Updating works the same way in reverse, with a Sync and Deploy workflow that pulls from the upstream CloudMeet template and redeploys, and the README notes that Upstream Sync and Deploy to Cloudflare Pages can also be run separately. The one documented failure mode is a permissions error during sync, which the documentation answers with a personal access token that has Contents and Workflows permissions. That is the whole operational model: a fork, some secrets, and two workflows.

Google and Outlook sync sit behind one availability check

The scheduling core checks availability across connected calendars and then creates an event with a meeting link. Google Calendar is the primary path, and Outlook Calendar is an optional second one; the documentation is explicit that you can use Google alone, Outlook alone, or both, and that when both are connected availability is checked across both. Meeting links are created automatically through Google Meet or Microsoft Teams, and the dashboard lets a user pick their preferred provider, with the option of none. The Microsoft side needs its own OAuth application in the Azure Portal, a Web redirect URI at `https://YOUR-DOMAIN/auth/outlook/callback`, and three delegated Microsoft Graph permissions: `Calendars.ReadWrite`, `User.Read`, and `OnlineMeetings.ReadWrite` for the Teams link. Admin consent can be granted up front, or left to individual users who consent on first login. Adding it means setting `MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` and redeploying.

The admin account is the login gate, which caps it at one organization

The most consequential design decision is a single line in the environment file. The `ADMIN_EMAIL` value is described as your Google email, and the note in the repository's example file says that only this account can login. Every other deployment secret in `.env.example` is a credential, but this one is an authorization rule, and it is the reason the project is a template for one team rather than a hosted product. There is no organization model, no team workspace and no per-booker account in the described configuration. The .env.example file also shows the full optional surface, including `EMAILIT_API_KEY` and `EMAIL_FROM` for booking mail through Emailit, and `CRON_SECRET` for the reminder endpoint. The session secret is `JWT_SECRET`, and the file's own comment gives the generation command as `openssl rand -hex 32`, while the README links to an online generator for the same value. Anyone evaluating this for a business with several staff should assume the multi-user story has to be built.

Reminders run from a Worker every five minutes, and CRON_SECRET gates them

Reminder emails are enabled automatically on deploy, and a Cloudflare Worker checks every five minutes for meetings due in 24 hours and one hour. The security detail is documented with unusual care. `CRON_SECRET` is optional, and without it the reminder endpoint is publicly accessible, which the README says means anyone could trigger reminder sends. With it set, only the cron worker can trigger reminders. That is a real footgun baked into the default configuration, since the secret that protects it is marked optional in both the secret table and the example environment file, while the reminders themselves are on by default. The remedy is simple, adding the secret and redeploying through the Deploy to Cloudflare Pages workflow, and the cron worker is deployed alongside the main app rather than as a separate step. If you take one operational precaution from this project, it is setting `CRON_SECRET` before the first booking arrives.

Local development needs a dev vars file, a schema import and a wrangler dev server

For a developer who wants to work on the code rather than deploy it, the README gives a four-line sequence: copy the example environment file to `.dev.vars` and add credentials, install the npm dependencies, initialise the local database, and start the dev server.

bash
cp .env.example .dev.vars  # Add your credentials
npm install
npm run db:init
npm run dev

The `db:init` step matters more than it looks, because it applies `./schema.sql` to a local D1 instance through wrangler, so the local database has the tables the app expects instead of failing on the first query. The dev script itself builds the project first and then serves it with wrangler pages dev, while a separate `dev:watch` script runs vite dev for a faster loop without the Cloudflare runtime. For a hosted database there is a matching `db:init:remote` script, and a `prebuild` hook that runs a script in the repository's `scripts/` directory before every build. The repository also carries a `package-lock.json` alongside Tailwind and PostCSS configuration files, so the styling pipeline is part of the project rather than something you add on top.

Where CloudMeet stops short of a hosted scheduler

The feature list is short and honest, and several things a paid scheduling product treats as basic are simply absent from it. The documentation describes availability and working hours, multiple event types, automatic event creation, configurable email notifications with confirmation, cancellation and reminder messages, and a dashboard for enabling and customizing those emails. It does not describe round-robin distribution, team availability, routing forms, payments, or any kind of API for programmatic booking. The Outlook integration is a second calendar to check, not a second identity system, and the Microsoft Graph permissions requested are read-write on the calendar plus meeting creation, nothing about a directory of people. There is also no release history to speak of, since the repository has no GitHub releases, and the last push was on 2026-07-02, so a deployment is a moving target that follows the upstream template rather than a pinned version you can point at.

How the open-source scheduling alternatives differ in approach

The comparison worth making is not with other open-source schedulers but with the hosted service it positions against. A hosted scheduler runs as a multi-tenant product, so it has to solve identity, per-organization isolation, plan limits and a public API for booking pages that other products embed, and it charges for those problems being solved. CloudMeet sidesteps them by not attempting them. The application is a single-organization scheduler where the operator configures one Google admin account, one D1 database and one set of repository secrets, and the deploy target is a static bundle on Cloudflare Pages with a scheduled Worker on top. The practical difference in approach is operational rather than functional. There is no database to patch, no server to scale and no per-booker billing, and the cost model is Cloudflare's free tier, but there is also no support boundary, no version pinning and no way to roll back a template sync other than by editing the fork yourself.

Editorial conclusion

CloudMeet is a sound choice for a small team or an individual consultant who wants a booking page with Google and Outlook sync without paying for a hosted scheduler, since the whole application fits inside Cloudflare's free tier and the deploy is one GitHub Actions run. It is a poor fit for a multi-tenant scheduling business, because `ADMIN_EMAIL` decides who can log in at all, and a poor fit if you need audit logs, round-robin distribution or a hardened multi-tenant data model. Before adopting it, read `.env.example` to see every credential the app expects, decide whether the reminder endpoint stays public without `CRON_SECRET`, and check whether the deploy workflow token needs Contents and Workflows permissions, because the last push to the repository was on 2026-07-02 and there are no tagged releases to fall back on.

Frequently asked questions

How do I deploy CloudMeet to Cloudflare?

Create a Cloudflare API token with the Edit Cloudflare Workers template plus Account and D1 Edit permissions, set up Google OAuth, create a repository from the template, add the repository secrets, then run the Deploy to Cloudflare Pages workflow. The app is then live at a pages.dev URL.

Can CloudMeet sync both Google Calendar and Outlook Calendar?

Yes. You can use Google alone, Outlook alone, or both, and when both are connected availability is checked across both calendars. The Outlook path needs a Microsoft Entra ID app registration and the delegated Graph permissions Calendars.ReadWrite, User.Read and OnlineMeetings.ReadWrite.

Is the CloudMeet reminder endpoint safe without CRON_SECRET?

The documentation says CRON_SECRET is optional but recommended, and that without it the reminder endpoint is publicly accessible, meaning anyone could trigger reminder sends. With the secret set, only the cron worker can trigger reminders.

How do I run CloudMeet locally?

Copy .env.example to .dev.vars with your credentials, run npm install, initialise the local database with npm run db:init, which applies schema.sql through wrangler, and then start the dev server with npm run dev.

Official sources

  1. dennisklappe/CloudMeet on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dennisklappe-cloudmeet.svg)](https://hysenlabs.com/projects/dennisklappe-cloudmeet)

Was this useful?