{"id":"8ce6a5ad-1784-45d0-a65c-71d4703d543a","slug":"hourmere","kind":"mcp","name":"hourmere","summary":"Open-source, agent-native scheduling: an AGPL Calendly alternative with a REST API and an MCP server. Self-host it; Hourmere Cloud is in invitation-only preview.","owner":"hourmere","repoUrl":"https://github.com/hourmere/hourmere","license":"AGPL-3.0","licenseTier":"strong-copyleft","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-19T02:42:46.000Z","addedAt":"2026-10-28T09:19:41.893Z","logoUrl":"https://avatars.githubusercontent.com/u/342912436?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra","productivity"],"tags":["appointment-scheduling","booking","calendar","calendly-alternative","open-source","scheduling","self-hosted","typescript"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-28T10:35:59.823Z","sample":false,"url":"https://conduits.dev/tools/hourmere","language":"TypeScript","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Hourmere\nOpen-source scheduling for people and AI agents. Self-host from source; Hourmere Cloud is coming.\n\n> Early development. Nothing here is ready to use yet.\n\n## Features\n\n- Personal and team booking pages, including [collective meetings](https://hourmere.com/docs/collective-scheduling)\n  and [round-robin scheduling](https://hourmere.com/docs/round-robin-scheduling).\n- [Google Calendar](https://hourmere.com/docs/google-calendar) busy-time sync and booking writeback.\n- [REST](https://hourmere.com/docs/rest), [MCP](https://hourmere.com/docs/mcp) and\n  [CLI](https://hourmere.com/docs/cli) access for agents and scripts.\n- [Booking waitlists](https://hourmere.com/docs/booking-waitlist) for fully booked\n  one-on-one single-host types; everyone eligible can be notified, with no reservation.\n- [CSV booking exports](https://hourmere.com/docs/booking-export) for personal owners and\n  team administrators, with bounded streaming and spreadsheet formula protection.\n- [Booking emails](https://hourmere.com/docs/notifications), optional signed\n  [webhooks](https://hourmere.com/docs/webhooks) and first-party [booking activity](https://hourmere.com/docs/analytics).\n\n## Repository layout\n\n| Path | What it is |\n|---|---|\n| `apps/server` | API server (Hono). Serves the built web app in production. |\n| `apps/web` | Web app (React + Vite). Talks to the server only through the public API. |\n| `apps/site` | Website for hourmere.com (Astro + Starlight): homepage, legal pages, docs. Static; not part of the Docker image. |\n| `packages/core` | Scheduling logic with no I/O or framework code, such as the availability engine and its opt-in [explanations](docs/availability-explanations.md). |\n| `packages/db` | Database access (Kysely) and SQL migrations in `packages/db/migrations`. |\n| `packages/scheduling` | Service layer shared by the REST API and the MCP server: reads and writes the database, enforces permissions, throws `DomainError`. |\n| `packages/auth` | Sign-in (Better Auth: email links, Google, Microsoft), sessions, organizations, API keys. |\n| `packages/calendars` | Separate Google Calendar OAuth, encrypted credentials, incremental busy-time sync and booking writeback. See [Google Calendar](https://hourmere.com/docs/google-calendar). |\n| `packages/conferences` | Independent Zoom connections and single-host automatic meetings; see [automatic Zoom](https://hourmere.com/docs/automatic-zoom), separate from sign-in. |\n| `packages/mail` | Outgoing email: SMTP, or the server log when SMTP is not configured; booking email templates and calendar invitations. |\n| `packages/jobs` | Background jobs on Postgres (pg-boss): typed job definitions, enqueueing inside a transaction, workers. |\n| `packages/webhooks` | Optional signed organization booking webhooks, transactional delivery and bounded retries. See [webhooks](https://hourmere.com/docs/webhooks). |\n| `packages/notifications` | Durable booking and sign-in email delivery, retries and the encrypted credential outbox. See [notifications](https://hourmere.com/docs/notifications). |\n| `tools/api-types` | Generates the web app's API types from the server's OpenAPI document (`npm run api:types`). |\n| `tools/cli` | API-key command-line client using the existing MCP tools (`npm run --silent cli -- --help`). See [CLI](https://hourmere.com/docs/cli). |\n\nCalDAV is currently a [design and credential/discovery foundation](docs/caldav.md) only.\niCloud, Fastmail and Nextcloud connections, busy sync and booking writes are not yet\navailable in the app, REST, MCP or CLI; no real-account acceptance has been completed.\n\n## Development\n\nRequires Node.js 26 and Docker.\n\n```sh\ncp .env.example .env   # set BETTER_AUTH_SECRET, e.g. `openssl rand -base64 32`\nnpm install\nnpm run db:up      # Postgres on localhost:5432\nnpm run dev        # API on :3000, web app on http://localhost:5173\n```\n\nWithout `SMTP_URL`, sign-in links are printed in the `npm run dev` output instead of being emailed.\nIn development anyone may sign up unless `SIGNUP` or `SIGNUP_ALLOWED_DOMAINS` is set (see Self-hosting).\n\nOpen the app at http://localhost:5173. Vite serves it and proxies `/api/` and `/sign-in/confirm` (the server's page for emailed sign-in links) to the API server (on `PORT` from the environment or `.env`, default 3000), so the browser talks to one origin, as in production. To run the two separately, start the API with sign-in links and cookies pointing at Vite (and open sign-up, as `npm run dev` does), then Vite:\n\n```sh\nPUBLIC_URL=http://localhost:5173 SIGNUP=open npm run dev --workspace @hourmere/server\nnpm run dev --workspace @hourmere/web\n```\n\nSigned-in pages live under `/app` (`/app/event-types`, `/app/availability`, `/app/bookings`, `/app/api-keys`, `/app/settings`); `/sign-in` stays at the top level, and the rest of the root is for public booking pages (`/{handle}/{slug}` for personal types and\n`/team/{teamSlug}/{slug}` for individual, collective and activated round-robin team types). After signing in, people only return to pages under `/app`.\n\nOne-time booking links open `/book/{token}`. They offer the usual booking form,\nincluding editable invitee prefill, and are consumed only when a booking commits.\nEvery unavailable link shows the same neutral page. These links cannot be embedded;\ntheir pages and API responses send no referrer and are not cached. Create, list and\nrevoke them in the event editor, REST, MCP or CLI; the full URL is shown only in the\ncreation response. See [one-time booking links](https://hourmere.com/docs/one-time-links)\nfor permissions, replay handling and limits.\n\nSaving an event type opens its saved version's **View and share** actions. Open the public\nbooking page or copy its full link there; unsaved address changes cannot be copied as a\npublished link. When setup or enablement needs attention, the panel links to the relevant\nsettings. Round-robin pools still require a separate explicit activation after saving.\n\nWhen the operator enables `SLOT_SNIPPETS_ENABLED` (off by default), saved event views\nlet personal owners and team owners/admins pick 1–10 open alternatives for one meeting\nlength and copy a plain-text or rich-text email snippet. The picker checks ordinary\npublic availability again before every copy; no time is reserved. Hidden event links\ncan be forwarded. Enable this setting only after public direct-slot booking pages are\ndeployed on the same instance. Email language and display zone affect labels only;\nthe booking page keeps the invitee’s own preferences.\nAgents can generate the same text/HTML with `POST /api/v1/open-time-snippets`, MCP\n`generate_open_time_snippet` or CLI `open-time-snippet --json`. A `read` key suffices\nwith owner/admin permission; see [the agent guide](https://hourmere.com/docs/open-time-snippets)\nfor bounded ranges, examples and taken-time fallback.\n\nThe web app only uses the public API (`/api/v1/*` and Better Auth's `/api/auth/*`). Its types for `/api/v1` are generated from the server's OpenAPI document: after changing a route, run `npm run api:types` (no database needed) and commit `apps/web/src/api/schema.d.ts`; CI fails if it is out of date.\n\nOther commands: `npm run lint`, `npm run typecheck`, `npm test`, `npm run build`, `npm run db:migrate`, `npm run db:down`, `npm run api:types`.\n\nThe website runs on its own: `npm run dev --workspace @hourmere/site` (http://localhost:4321). `npm run build` writes it to `apps/site/dist`. A small Cloudflare Worker handles the Hourmere Cloud waitlist and serves the static assets on hourmere.com and www.hourmere.com (config in `apps/site/wrangler.jsonc`). The D1 database already exists. See [website operations](docs/website.md) for local D1/form testing and the migration the lead must apply before running `npm run deploy --workspace @hourmere/site` in the Cobaltmere Cloudflare account.\n\nThe privacy policy's effective and last-updated dates and the terms' effective date share\n`LEGAL_PUBLICATION_DATE` in `apps/site/src/data/legal.ts`, currently `2026-10-22`.\nKeep that date on later deployments, including documentation and homepage changes; change it\nonly when publishing revised legal text. A placeholder or invalid date stops the site build.\nEnvironment variables cannot override the source date. CI retains a fixture-date substitution\nfor a placeholder source date; with the current committed date, that substitution makes no\nchange. Its build steps restore the source with an exit trap and delete the generated site\noutput. Never deploy fixture output or commit a fixture date.\n\n## Self-hosting\n\nThe Starlight documentation includes a [self-hosting guide](https://hourmere.com/docs/self-hosting),\n[configuration reference](https://hourmere.com/docs/configuration),\nand [REST](https://hourmere.com/docs/rest) / [MCP](https://hourmere.com/docs/mcp) quickstarts.\n\nRequires Git, OpenSSL, Docker Engine and Docker Compose 2.24.4 or newer. Point your hostname\nat the host with an A record only and allow TCP 80/443 and UDP 443 (plus SSH) before using automatic HTTPS:\n\n```sh\ngit clone https://github.com/hourmere/hourmere.git\ncd hourmere\n./scripts/setup.sh --source\n./scripts/compose-up.sh -d --build --wait\ncurl --retry 20 --retry-delay 3 --retry-all-errors --fail https://hourmere.example.com/api/health\n```\n\nAccept `proxy`, enter your HTTPS origin (replace `hourmere.example.com` with your hostname),\nand configure SMTP and its sender. Setup generates separate secrets directly into a mode\n`600` `.env`, refuses to overwrite an existing file, and selects the optional Caddy override.\nCaddy obtains/renews TLS certificates; only TCP 80/443 and UDP 443 are published, and the app trusts only\nthe proxy's dedicated address for forwarded client IPs. If its network overlaps an existing\nnetwork or another instance, change `PROXY_SUBNET`, `PROXY_IP` and `PROXY_DYNAMIC_RANGE` together\nin `.env` before starting. **No prebuilt public image is\npublished yet**; Compose builds the checked-out source. Application and job migrations run\nautomatically (`AUTO_MIGRATE=true`). Open `/sign-in` and claim the first account immediately;\n`SIGNUP=first-user` restricts later registration as described below.\n\nUse `./scripts/compose-up.sh` for starts and upgrades. It hashes the current host `Caddyfile`\non each run and makes Compose recreate only the proxy when that content changes, including\noperator edits. This briefly interrupts HTTPS while Caddy restarts. Keep editing the root\n`Caddyfile`; retain its global log filter that removes request URIs and request/response\nheaders. Review upstream changes alongside customizations before starting the new revision.\nDirect `docker compose up` bypasses this digest update.\n\nSelect `plain` for an existing proxy or an explicitly acknowledged private loopback trial.\nIt keeps the app on `127.0.0.1:3000` and PostgreSQL unpublished. For your own proxy, set its\nexact address as the app sees it in `TRUSTED_PROXIES` and preserve Host/forwarded headers;\nsee the [self-hosting guide](https://hourmere.com/docs/self-hosting) for the complete\nTLS, backup/restore, upgrade and host-hardening procedures. The default app also runs\nbackground jobs; to split them after setup, run:\n\n```sh\n(\n  unset COMPOSE_FILE &&\n  ./scripts/enable-worker.sh &&\n  compose_services=$(docker compose config --services) &&\n  { printf '%s\\n' \"$compose_services\" | grep -qx worker ||\n    { echo \"Worker service is missing; check COMPOSE_FILE before continuing.\"; exit 1; }; } &&\n  { printf '%s\\n' \"$compose_services\" | grep -qx proxy ||\n    { echo \"Proxy service is missing; check COMPOSE_FILE before continuing.\"; exit 1; }; } &&\n  ./scripts/compose-up.sh -d --build --wait\n)\n```\n\nFor plain mode, remove both lines of the proxy verification (the `grep -qx proxy` line\nand its following failure-message line) before pasting the block.\n\nThe helper safely updates `COMPOSE_FILE`\nin the private `.env`, preserving the proxy selection. The tracked overlay automatically\nselects `web` for the app and `worker` for the separate worker, using the same image and\nsecrets.\nThe block unsets a shell-exported `COMPOSE_FILE` only inside its subshell; unset it\nseparately before standalone Compose commands so they read `.env` too.\nThe `.env` backup preserves this choice, and upgrades need no tracked Compose edits.\nGive containers at least 35 seconds to stop.\n\nBefore upgrading, take a full backup, including the host `Caddyfile` with any operator\ncustomizations, and record the current branch name or tag alongside\nthe backup's saved commit. A checkout-only rollback with\n`git checkout <previous branch or tag>` is safe if a guard, `docker compose pull` or\n`docker compose stop` fails before the new-version `docker compose up` command runs.\nOnce that `up` is attempted, follow [Back up and restore](https://hourmere.com/docs/self-hosting#back-up-and-restore),\neven if it fails: a new app or worker may already have applied migrations; old code against\nthe new schema is unsafe. In the supplied Compose files, `AUTO_MIGRATE=true` runs application\nand pg-boss migrations at process startup for every role (`all`, `web` and `worker`),\nnot during `pull` or `stop`; there is no separate migrate service. A separately started\nnew-version worker, `npm run db:migrate` or a custom migration job can migrate earlier;\nif any has started, use backup/restore even before this Compose `up`.\nFor a failed main-tracking pull, the backup's saved commit identifies\nthe exact pre-upgrade revision: `git pull` advances `main`, so its branch name alone does\nnot undo the update. If rollback leaves a pre-1.0 `main`-tracking checkout detached, run\n`git switch main` before the next upgrade so `git pull --ff-only` works.\n\nGit does not undo `enable-worker.sh`'s private `.env` edit. Before checking out the prior\nrevision, restore only the pre-upgrade `COMPOSE_FILE` assignment from the saved private\n`.env` (remove the assignment if it was absent), preserving the proxy/custom overlays\nand other current settings. See the [guarded upgrade procedures](https://hourmere.com/docs/self-hosting#upgrade-and-roll-back).\n\nFor a pre-start failure, if the app or worker was already stopped (including a partial\n`stop` failure), bring the old version back with the block below after restoring that\nselection. Replace `PREVIOUS_BRANCH_OR_TAG` with the recorded name. If no service was\nstopped, run the same block: checkout can replace the host Caddyfile while the running\nproxy still holds its old inode. The block uses the existing old application image;\ndo not add `--build` or pull an application image during this recovery.\n\n```sh\n(\n  git checkout \"PREVIOUS_BRANCH_OR_TAG\" &&\n  unset COMPOSE_FILE &&\n  compose_services=$(docker compose config --services) &&\n  docker compose up -d --wait app db &&\n  if printf '%s\\n' \"$compose_services\" | grep -qx worker; then\n    docker compose up -d --wait --no-deps worker\n  fi &&\n  docker compose up -d --wait --force-recreate --no-deps proxy\n)\n```\n\nStarting app/database and the selected worker explicitly leaves the proxy untouched\nuntil the final command, avoiding a second HTTPS interruption. Named certificate and\ndata volumes are preserved. The final command also works when the previous revision\nlacks the helper; it briefly restarts the proxy so it binds the prior revision's actual `Caddyfile`. For plain mode,\nomit that command and remove the preceding `&&`. Full restore must restore the backed-up\nhost `Caddyfile` before creating the proxy, as shown in the self-hosting guide.\n\nWhen upgrading from an older release, compare `sha256sum Caddyfile` with\n`docker compose exec -T proxy sha256sum /etc/caddy/Caddyfile` (use `shasum -a 256 Caddyfile`\non macOS). If the hashes differ, run\n`docker compose up -d --wait --force-recreate --no-deps proxy`, then compare again.\n\nThe application image retains small public attribution and booking-confirmation links to the\nhosted service. `HOSTED_SIGNUP_URL` sets their destination (default `https://hourmere.com`);\nthere is no setting to hide them.\n\nSet `PRIVACY_POLICY_URL` and `TERMS_URL` to your operator's absolute HTTP(S) legal-page\nURLs to display them in the app. Both default to empty, which hides the corresponding\nlink; self-hosted installations do not inherit Hourmere Cloud's policies. The hosted\ninstance must explicitly set `https://hourmere.com/privacy` and `https://hourmere.com/terms`.\n\nTeam owners and admins can configure [collective meetings](https://hourmere.com/docs/collective-scheduling)\nwith two to eight required hosts, or [round-robin pools](https://hourmere.com/docs/round-robin-scheduling)\nwith one to eight hosts and explicit activation.\n\nWebsite widgets support inline and popup booking; see [embedding](https://hourmere.com/docs/embedding) for the\nexplicit parent-origin allow list, integration snippets and privacy contract.\n\nResponses carry security headers: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY` (except explicitly approved dedicated embed pages),\na `Referrer-Policy`, and on pages a Content-Security-Policy that lets only Hourmere's own\nscripts, styles and requests run. When `PUBLIC_URL` is https, browsers are also told to use\nonly https for that host, for a year (`HSTS_MAX_AGE`). Clients that send too slowly are\ndisconnected: 20 s to send a request's headers and 60 s for the whole request\n(`HTTP_HEADERS_TIMEOUT`, `HTTP_REQUEST_TIMEOUT`). Idle connections stay open for 125 s\n(`HTTP_KEEP_ALIVE_TIMEOUT`): keep it above your reverse proxy's idle timeout for connections to\nthe app, or the proxy may reuse a connection the app has just closed and answer 502. The\ndefault is above the usual ones: nginx's upstream keepalive and AWS ALB 60 s, Traefik 90 s,\nCaddy 120 s.\nCSV exports reserve at most two database connections per server process by default.\n`BOOKING_EXPORT_MAX_CONCURRENT` accepts 1–8 (invalid values use 2); keep this below\nthe database pool capacity, with headroom for ordinary traffic. Excess requests fail\nbefore acquiring a connection with HTTP 409 `export_busy`. Each stream closes after\n30 seconds without reader progress, or after five minutes overall. These limits apply\nin addition to the one-export-per-organization database lease.\n\n\nPeople sign in with a link sent by email (no passwords), or with Google or Microsoft once their\nclient ids are set in `.env`. Accounts are only created for email addresses the provider\nconfirms: for Microsoft, add the optional ID token claims `verified_primary_email` and\n`verified_secondary_email` in the Entra app registration (Token configuration), or Microsoft\nusers get `EMAIL_NOT_VERIFIED` and have to use an email link. Until `SMTP_URL` and `MAIL_FROM` are set, the server prints each\nsign-in link to its log (`docker compose logs app`), so you can sign in right after the first\nstart. Anyone who can read that log can sign in as anyone: configure SMTP before inviting\nanyone else, and always on a hosted or internet-facing instance. The setup script requires\nSMTP for public deployment; empty SMTP is accepted only for an explicitly acknowledged\nprivate loopback trial.\n\n`SIGNUP` decides who may create an account; existing users can always sign in. With\n`first-user` (the default), the first person to sign in gets an account, and after that only\naddresses in `SIGNUP_ALLOWED_DOMAINS` (comma-separated email domains, e.g.\n`example.com,example.org`; exact match, so list subdomains separately). So sign in right\nafter the first start: the first account should be yours. `closed` allows those domains and\nthe invitation exception below. With an empty domain list, only invitations from the first\nuser admit new accounts, so bootstrap an empty instance in `first-user` mode or through an\nadministrator before using `closed`. `open` lets anyone who can reach the instance sign up.\nA pending, unexpired team invitation allows its exact email address in either restricted\nmode only if the inviter's current email domain is allowed or the inviter is the instance's\nfirst user (the earliest remaining auth user by creation time, with user id breaking ties).\nThe original inviter must also still belong to the invitation's team; demotion while\nremaining a member preserves this eligibility. A void invitation grants no account or\nsign-in email to an otherwise ineligible address.\nInvitation-based registration is non-transitive: an invitee outside the allowed domains\ncannot admit another address by creating a team and inviting it. Team creation and invitation\nacceptance grant no registration authority. These rules apply to existing invitations too,\nusing the current domain allowlist. Sign-up still requires proving that email address;\njoining the team requires accepting the invitation separately. Expired, canceled or already\naccepted invitations do not allow new accounts. A new\naddress that may not sign up gets no sign-in email, and the request is answered the same way\nas for any other address. Every request queues an encrypted, short-lived email job before eligibility is checked by\na worker; the request does not wait for SMTP or check whether the address has an account.\nWorkers make at most four delivery attempts and discard expired links. Request a new link\nif delivery fails. In web-only mode, a separate worker\nis required for sign-in emails as well as booking notifications. Failures appear in the log\nwithout the recipient or link.\n\nTeam invitation links open `/app/join?invitationId=<uuid>`. Sign in with the exact email\naddress that received the invitation; email-link and enabled social sign-in return to that\npage. Signing in does not join the team: review the team and signed-in account, then choose **Join team**.\nAfter joining, the event types page confirms the team name and links to your default\navailability. Dismiss this one-time notice, navigate away or reload to clear it.\nIf you used a different account, **Change account** signs you out while keeping the invitation\nfor your next sign-in. Invitation details remain restricted to the verified recipient.\nAn existing member can open the team without accepting again.\nIf the original inviter leaves or is removed from the team, their pending invitations are\nno longer valid. Ask a current team owner/admin for a new invitation; those old links cannot\nbe accepted or resent, and recipient invitation discovery omits them. An inviter's demotion\nwhile remaining a member does not invalidate links.\n\nThe signed-in header's space menu switches between **Personal** and your member organizations\nand shows your role in the current space. Switching asks before discarding unsaved changes\nand starts the page over in the selected space. A failed switch keeps the current page;\nreturning to personal after team access or active context changes shows a notice. Switching\nthe web app does not change an API key's organization or an existing MCP OAuth grant.\n\nIn a team space, Settings lists joined members by name and role with pagination.\nOwners and admins also see a separate paged section of pending invitations and their\nmail submission status. Mail server acceptance does not confirm inbox receipt;\npending invitations never count as members or hosts. Owners/admins can resend or cancel each\npending invitation, and select expired invitations to atomically cancel the old invitation\nand send a new invitation with a new link under the acting admin, without a duplicate pending\nrow. Cancellation never removes a member who has already joined.\nInvitations from someone who no longer belongs to the team show **No longer valid**;\ncancel the old invitation and use the invite form to send a fresh invitation in your own name.\nThe expired-row replacement action cannot reuse these invitations.\nOwners can promote members to admins or return admins to member access after reviewing\nthe permission change. Only owners manage admins; owner roles cannot be changed, and\nownership transfer is not available yet. A team must always retain at least one owner.\n\nMember rows show shared configuration and cached calendar health checks, without\npromising available times. Owners and admins see only coarse states for other members,\nincluding whether they have selected a conflict calendar or connected a conference account;\nordinary members see their own readiness. Providers, calendar and connection IDs, account\ncounts, private calendar names, account emails, events and busy times are excluded. Your own\nrow links to schedule, calendar and conference settings and refreshes when you return. Your\npersonal default schedule is reused across every team you belong to; an external calendar\nis optional for basic bookings.\n\nThe team event host editor shows the same coarse checks for selected hosts. Your own setup\nlinks open your settings; other hosts' setup links open the members page with guidance to\nask the member to make private changes. Owners and admins can open a member's hosted\nteam event types from that member's row, including dormant round-robin pool candidates.\n\nThe database enforces the sign-up rules too, using settings the app passes in the `options`\nstartup parameter when it connects (`search_path` and `hourmere.*`), and it needs read\ncommitted isolation (Postgres's default) or serializable. Connect the app to Postgres\ndirectly, as the compose file does; connection poolers such as PgBouncer are not supported,\nsince they do not pass custom startup settings like `hourmere.*` on to Postgres. The app reads\nthe settings back at startup and refuses to start if they are missing.\n\nThat check looks at one connection, once, at startup: a pooler slipped in later at the same\naddress would turn the database's check off without notice (the app's own check still\napplies). It also means the app needs the database at startup even with `AUTO_MIGRATE=false`;\nif the database is down, the app exits and compose restarts it.\n\nSign-in links open a page with a \"Sign in to Hourmere\" button, and only pressing it signs\nin: email security scanners that open every link (such as Microsoft Safe Links) would\notherwise use up the one-time link before the person clicks it.\n\nAgents use API keys (`hm_…`) sent as `Authorization: Bearer <key>`. Create them on the web\napp's API keys page (`/app/api-keys`), or with Better Auth's endpoints while signed in (`POST\n/api/auth/api-key/create`, `GET /api/auth/api-key/list`, `POST /api/auth/api-key/delete`). The API is described at\n`/api/v1/openapi.json`; its conventions are in [API conventions](https://hourmere.com/docs/api-conventions).\nThe same keys connect MCP clients such as Claude Code to the remote MCP server at `/mcp`; see\n[MCP](https://hourmere.com/docs/mcp).\nChoose **Bookings** (the web default) for an AI booking agent, **Read** for\nanalytics/reporting, or **Full account access** when you need configuration writes. Existing\nkeys retain `full` access. A key's scope cannot change: create a replacement, verify it, then\nrevoke the old key. See [API key scopes](https://hourmere.com/docs/api-keys) for the generated\noperation table and the required stop-before-upgrade procedure.\n\nFor terminal scripts, the dependency-free [CLI](https://hourmere.com/docs/cli) supports configuration discovery,\nslots, booking, host history, rescheduling and cancellation with the same API key.\n\nPersonal and single-host team event types can enable a booking waitlist in the event editor.\nInvitees join for an unavailable date or period and confirm their email before receiving\navailability notifications. Eligible invitees are notified together, subject to mail limits;\nnothing is reserved and the first successful ordinary booking wins. Waitlists use the default\nmeeting length. Hosts can view and remove their event's entries; team contacts are restricted\nto owners/admins. REST, MCP and CLI provide the same settings, list and removal operations.\n\nConfigure event-type reminders and follow-ups through REST, MCP\n`set_event_type_notifications` or CLI `set-event-notifications` with `full` scope.\nFollow-up rules may carry bounded plain-text host notes. These are sent from the instance's\nmail domain to unverified invitee addresses; operators can disable writes and sending of\nnotes with `FOLLOW_UP_HOST_NOTES_ENABLED=false` (self-hosting default: `true`). Configure\nweb and worker identically. Hourmere Cloud must explicitly set this to `false` until\nreviewed host-text abuse controls are implemented. See [notification settings and host-note policy](https://hourmere.com/docs/notifications#configure-reminders-and-follow-ups).\n\nOrganization owners can view first-party [booking activity](https://hourmere.com/docs/analytics) at\n`/app/analytics`: daily creations, reschedules and actual cancellations, without visitor\ntracking or attendee details.\n\n## Integrations\n\nOptional [independent Zoom account connections](https://hourmere.com/docs/zoom-connections)\nprovide a separate minimum-scope identity foundation. Explicit opt-in [automatic Zoom](https://hourmere.com/docs/automatic-zoom) supports personal and single-host team\nbookings with durable reconciliation, same-meeting rescheduling and cancellation, ready-only\ninvitations and an optional ordinary Google calendar copy. Zoom is not a sign-in provider. See the\n[Zoom account guide](https://hourmere.com/docs/zoom-connections) before configuring a real client.\n\nOptional [automatic Google Meet](https://hourmere.com/docs/automatic-google-meet) creates one conference per explicitly opted-in individual, collective or round-robin booking, with durable pending/failure state and final invitations held until its link is ready.\n\n## Known limitations\n\nOrganization owners and team admins can store a logo and primary colour through settings,\nREST, MCP and CLI. [Branding settings](https://hourmere.com/docs/branding) include safe image\nuploads and automatic readable button-text selection; public pages and emails do not apply\nthe stored theme yet.\n\nSee the [consolidated limitations](https://hourmere.com/docs#known-limitations)\nand linked operating guides before deploying:\n\n- [Booking limits](https://hourmere.com/docs/booking-limits) apply per event type and actual host, across\n  all meeting lengths. Organization-wide quotas and rolling windows are unavailable.\n  Lowering a limit does not cancel existing bookings.\n- Public booking and sign-in pages have no CAPTCHA; invitee/guest addresses remain unverified.\n  [Email abuse limits](https://hourmere.com/docs/notifications#email-abuse-controls) apply per\n  instance/database; separate instances do not share budgets. Per-IP request limits remain\n  in process memory, except the additional durable booking-waitlist admission limits.\n- Microsoft Calendar lists calendars only; no busy-time sync or event writes. Microsoft sign-in and\n  calendar connections have not had real-account acceptance.\n- Automatic Zoom supports personal event types and individual team event types with one\n  actual host; collective and round-robin event types are unsupported.\n- [Meeting location choices](https://hourmere.com/docs/location-choices) are configured through REST, MCP or CLI;\n  the editor does not yet offer a multiple-location editor. Rescheduling preserves the choice.\n- [Multiple meeting lengths](https://hourmere.com/docs/multiple-durations) share one set of\n  hosts, location and booking rules; per-length overrides are unavailable. Web rescheduling\n  keeps the original length; changing it requires REST, MCP or CLI.\n- Calendar RSVP replies are not processed; declining in a calendar does not cancel a booking.\n- PostgreSQL 18+ is required; PgBouncer is unsupported.\n- Hourmere must be served at the root of its own host name (no path prefix) in every deployment.\n  Bundled Caddy additionally requires ports 80/443, an HTTPS host name without a port,\n  and an IPv4-only ingress network.\n- Rate limiting uses in-process memory at the transport layer; multiple web replicas multiply the limits.\n  Booking waitlists additionally enforce durable mailbox, event and trusted-IP admission\n  budgets in PostgreSQL across replicas. Run a single web process; a separate worker is supported.\n- [Public booking pages and emails](https://hourmere.com/docs/interface-languages) support English,\n  Simplified Chinese, German, French, Spanish and Brazilian Portuguese. Account and sign-in interfaces support English and\n  Simplified Chinese. Missing translations fall back to English; dates follow the active locale.\n- No automatic schema downgrade; some migrations require stopping all old app and worker\n  processes. Review the [migration notes](docs/data-model.md#changing-the-schema).\n\n## Contributing\n\nBug reports and ideas are welcome in [GitHub Issues](https://github.com/hourmere/hourmere/issues),\nas are pull requests. [CONTRIBUTING.md](CONTRIBUTING.md) covers\nsetup, checks, conventions and the DCO sign-off; everyone follows the\n[code of conduct](CODE_OF_CONDUCT.md).\n\n## Security\n\nReport vulnerabilities privately through the repository's\n[Security tab](https://github.com/hourmere/hourmere/security) or to\n[security@hourmere.com](mailto:security@hourmere.com), not in a public issue. See [SECURITY.md](SECURITY.md).\n\n## License\n\n[AGPL-3.0](LICENSE)\n"}