New
Apps 1,609Makers 1,607This week 1,591
Promote your app−30%
Hourmere screenshot 1
0views in
the last 30 days

Open-source scheduling for people and AI agents. Self-host from source; Hourmere Cloud is coming.

Early development. Nothing here is ready to use yet.

Features

Repository layout

Path What it is
apps/server API server (Hono). Serves the built web app in production.
apps/web Web app (React + Vite). Talks to the server only through the public API.
apps/site Website for hourmere.com (Astro + Starlight): homepage, legal pages, docs. Static; not part of the Docker image.
packages/core Scheduling logic with no I/O or framework code, such as the availability engine.
packages/db Database access (Kysely) and SQL migrations in packages/db/migrations.
packages/scheduling Service layer shared by the REST API and the MCP server: reads and writes the database, enforces permissions, throws DomainError.
packages/auth Sign-in (Better Auth: email links, Google, Microsoft), sessions, organizations, API keys.
packages/calendars Separate Google Calendar OAuth, encrypted credentials, incremental busy-time sync and booking writeback. See Google Calendar.
packages/conferences Independent Zoom connections and single-host automatic meetings; see automatic Zoom, separate from sign-in.
packages/mail Outgoing email: SMTP, or the server log when SMTP is not configured; booking email templates and calendar invitations.
packages/jobs Background jobs on Postgres (pg-boss): typed job definitions, enqueueing inside a transaction, workers.
packages/webhooks Optional signed organization booking webhooks, transactional delivery and bounded retries. See webhooks.
packages/notifications Durable booking and sign-in email delivery, retries and the encrypted credential outbox. See notifications.
tools/api-types Generates the web app's API types from the server's OpenAPI document (npm run api:types).
tools/cli API-key command-line client using the existing MCP tools (npm run --silent cli -- --help). See CLI.

Development

Requires Node.js 26 and Docker.

cp .env.example .env   # set BETTER_AUTH_SECRET, e.g. `openssl rand -base64 32`
npm install
npm run db:up      # Postgres on localhost:5432
npm run dev        # API on :3000, web app on http://localhost:5173

Without SMTP_URL, sign-in links are printed in the npm run dev output instead of being emailed. In development anyone may sign up unless SIGNUP or SIGNUP_ALLOWED_DOMAINS is set (see Self-hosting).

Open 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:

PUBLIC_URL=http://localhost:5173 SIGNUP=open npm run dev --workspace @hourmere/server
npm run dev --workspace @hourmere/web

Signed-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 /team/{teamSlug}/{slug} for individual, collective and activated round-robin team types). After signing in, people only return to pages under /app.

The 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.

Other commands: npm run lint, npm run typecheck, npm test, npm run build, npm run db:migrate, npm run db:down, npm run api:types.

The 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 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.

The privacy policy's effective and last-updated dates and the terms' effective date share LEGAL_PUBLICATION_DATE in apps/site/src/data/legal.ts, currently 2026-10-22. Keep that date on later deployments, including documentation and homepage changes; change it only when publishing revised legal text. A placeholder or invalid date stops the site build. Environment variables cannot override the source date. CI retains a fixture-date substitution for a placeholder source date; with the current committed date, that substitution makes no change. Its build steps restore the source with an exit trap and delete the generated site output. Never deploy fixture output or commit a fixture date.

Self-hosting

The Starlight documentation includes a self-hosting guide, configuration reference, and REST / MCP quickstarts.

Requires Git, OpenSSL, Docker Engine and Docker Compose 2.24.4 or newer. Point your hostname at the host with an A record only and allow TCP 80/443 and UDP 443 (plus SSH) before using automatic HTTPS:

git clone https://github.com/hourmere/hourmere.git
cd hourmere
./scripts/setup.sh --source
docker compose up -d --build --wait
curl --retry 20 --retry-delay 3 --retry-all-errors --fail https://hourmere.example.com/api/health

Accept proxy, enter your HTTPS origin (replace hourmere.example.com with your hostname), and configure SMTP and its sender. Setup generates separate secrets directly into a mode 600 .env, refuses to overwrite an existing file, and selects the optional Caddy override. Caddy obtains/renews TLS certificates; only TCP 80/443 and UDP 443 are published, and the app trusts only the proxy's dedicated address for forwarded client IPs. If its network overlaps an existing network or another instance, change PROXY_SUBNET, PROXY_IP and PROXY_DYNAMIC_RANGE together in .env before starting. No prebuilt public image is published yet; Compose builds the checked-out source. Application and job migrations run automatically (AUTO_MIGRATE=true). Open /sign-in and claim the first account immediately; SIGNUP=first-user restricts later registration as described below.

Select plain for an existing proxy or an explicitly acknowledged private loopback trial. It keeps the app on 127.0.0.1:3000 and PostgreSQL unpublished. For your own proxy, set its exact address as the app sees it in TRUSTED_PROXIES and preserve Host/forwarded headers; see the self-hosting guide for the complete TLS, backup/restore, upgrade and host-hardening procedures. The default app also runs background jobs; to split them after setup, run:

(
  unset COMPOSE_FILE &&
  ./scripts/enable-worker.sh &&
  compose_services=$(docker compose config --services) &&
  { printf '%s\n' "$compose_services" | grep -qx worker ||
    { echo "Worker service is missing; check COMPOSE_FILE before continuing."; exit 1; }; } &&
  { printf '%s\n' "$compose_services" | grep -qx proxy ||
    { echo "Proxy service is missing; check COMPOSE_FILE before continuing."; exit 1; }; } &&
  docker compose up -d --build --wait
)

For plain mode, remove both lines of the proxy verification (the grep -qx proxy line and its following failure-message line) before pasting the block.

The helper safely updates COMPOSE_FILE in the private .env, preserving the proxy selection. The tracked overlay automatically selects web for the app and worker for the separate worker, using the same image and secrets. The block unsets a shell-exported COMPOSE_FILE only inside its subshell; unset it separately before standalone Compose commands so they read .env too. The .env backup preserves this choice, and upgrades need no tracked Compose edits. Give containers at least 35 seconds to stop.

Before upgrading, take a full backup and record the current branch name or tag alongside the backup's saved commit. A checkout-only rollback with git checkout <previous branch or tag> is safe if a guard, docker compose pull or docker compose stop fails before the new-version docker compose up command runs. Once that up is attempted, follow Back up and restore, even if it fails: a new app or worker may already have applied migrations; old code against the new schema is unsafe. In the supplied Compose files, AUTO_MIGRATE=true runs application and pg-boss migrations at process startup for every role (all, web and worker), not during pull or stop; there is no separate migrate service. A separately started new-version worker, npm run db:migrate or a custom migration job can migrate earlier; if any has started, use backup/restore even before this Compose up. For a failed main-tracking pull, the backup's saved commit identifies the exact pre-upgrade revision: git pull advances main, so its branch name alone does not undo the update. If rollback leaves a pre-1.0 main-tracking checkout detached, run git switch main before the next upgrade so git pull --ff-only works.

Git does not undo enable-worker.sh's private .env edit. Before checking out the prior revision, restore only the pre-upgrade COMPOSE_FILE assignment from the saved private .env (remove the assignment if it was absent), preserving the proxy/custom overlays and other current settings. See the guarded upgrade procedures.

For a pre-start failure, if the app or worker was already stopped (including a partial stop failure), bring the old version back with the block below after restoring that selection. Replace PREVIOUS_BRANCH_OR_TAG with the recorded name. If no service was stopped, only the checkout is needed. The block uses the existing old application image; do not add --build or pull an application image during this recovery.

(
  git checkout "PREVIOUS_BRANCH_OR_TAG" &&
  unset COMPOSE_FILE &&
  docker compose up -d --wait
)

The application image retains small public attribution and booking-confirmation links to the hosted service. HOSTED_SIGNUP_URL sets their destination (default https://hourmere.com); there is no setting to hide them.

Set PRIVACY_POLICY_URL and TERMS_URL to your operator's absolute HTTP(S) legal-page URLs to display them in the app. Both default to empty, which hides the corresponding link; self-hosted installations do not inherit Hourmere Cloud's policies. The hosted instance must explicitly set https://hourmere.com/privacy and https://hourmere.com/terms.

Team owners and admins can configure collective meetings with two to eight required hosts, or round-robin pools with one to eight hosts and explicit activation.

Website widgets support inline and popup booking; see embedding for the explicit parent-origin allow list, integration snippets and privacy contract.

Responses carry security headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY (except explicitly approved dedicated embed pages), a Referrer-Policy, and on pages a Content-Security-Policy that lets only Hourmere's own scripts, styles and requests run. When PUBLIC_URL is https, browsers are also told to use only https for that host, for a year (HSTS_MAX_AGE). Clients that send too slowly are disconnected: 20 s to send a request's headers and 60 s for the whole request (HTTP_HEADERS_TIMEOUT, HTTP_REQUEST_TIMEOUT). Idle connections stay open for 125 s (HTTP_KEEP_ALIVE_TIMEOUT): keep it above your reverse proxy's idle timeout for connections to the app, or the proxy may reuse a connection the app has just closed and answer 502. The default is above the usual ones: nginx's upstream keepalive and AWS ALB 60 s, Traefik 90 s, Caddy 120 s.

People sign in with a link sent by email (no passwords), or with Google or Microsoft once their client ids are set in .env. Accounts are only created for email addresses the provider confirms: for Microsoft, add the optional ID token claims verified_primary_email and verified_secondary_email in the Entra app registration (Token configuration), or Microsoft users get EMAIL_NOT_VERIFIED and have to use an email link. Until SMTP_URL and MAIL_FROM are set, the server prints each sign-in link to its log (docker compose logs app), so you can sign in right after the first start. Anyone who can read that log can sign in as anyone: configure SMTP before inviting anyone else, and always on a hosted or internet-facing instance. The setup script requires SMTP for public deployment; empty SMTP is accepted only for an explicitly acknowledged private loopback trial.

SIGNUP decides who may create an account; existing users can always sign in. With first-user (the default), the first person to sign in gets an account, and after that only addresses in SIGNUP_ALLOWED_DOMAINS (comma-separated email domains, e.g. example.com,example.org; exact match, so list subdomains separately). So sign in right after the first start: the first account should be yours. closed allows those domains and the invitation exception below. With an empty domain list, only invitations from the first user admit new accounts, so bootstrap an empty instance in first-user mode or through an administrator before using closed. open lets anyone who can reach the instance sign up. A pending, unexpired team invitation allows its exact email address in either restricted mode only if the inviter's current email domain is allowed or the inviter is the instance's first user (the earliest remaining auth user by creation time, with user id breaking ties). Invitation-based registration is non-transitive: an invitee outside the allowed domains cannot admit another address by creating a team and inviting it. Team creation and invitation acceptance grant no registration authority. These rules apply to existing invitations too, using the current domain allowlist. Sign-up still requires proving that email address; joining the team requires accepting the invitation separately. Expired, canceled or already accepted invitations do not allow new accounts. A new address that may not sign up gets no sign-in email, and the request is answered the same way as for any other address. Every request queues an encrypted, short-lived email job before eligibility is checked by a worker; the request does not wait for SMTP or check whether the address has an account. Workers make at most four delivery attempts and discard expired links. Request a new link if delivery fails. In web-only mode, a separate worker is required for sign-in emails as well as booking notifications. Failures appear in the log without the recipient or link.

Team invitation links open /app/join?invitationId=<uuid>. Sign in with the exact email address that received the invitation; email-link and enabled social sign-in return to that page. Signing in does not join the team: review the invitation and choose Accept invitation.

The database enforces the sign-up rules too, using settings the app passes in the options startup parameter when it connects (search_path and hourmere.*), and it needs read committed isolation (Postgres's default) or serializable. Connect the app to Postgres directly, as the compose file does; connection poolers such as PgBouncer are not supported, since they do not pass custom startup settings like hourmere.* on to Postgres. The app reads the settings back at startup and refuses to start if they are missing.

That check looks at one connection, once, at startup: a pooler slipped in later at the same address would turn the database's check off without notice (the app's own check still applies). It also means the app needs the database at startup even with AUTO_MIGRATE=false; if the database is down, the app exits and compose restarts it.

Sign-in links open a page with a "Sign in to Hourmere" button, and only pressing it signs in: email security scanners that open every link (such as Microsoft Safe Links) would otherwise use up the one-time link before the person clicks it.

Agents use API keys (hm_…) sent as Authorization: Bearer <key>. Create them on the web app's API keys page (/app/api-keys), or with Better Auth's endpoints while signed in (POST /api/auth/api-key/create, GET /api/auth/api-key/list, POST /api/auth/api-key/delete). The API is described at /api/v1/openapi.json; its conventions are in API conventions. The same keys connect MCP clients such as Claude Code to the remote MCP server at /mcp; see MCP.

For terminal scripts, the dependency-free CLI supports configuration discovery, slots, booking, host history, rescheduling and cancellation with the same API key.

Organization owners can view first-party booking activity at /app/analytics: daily creations, reschedules and actual cancellations, without visitor tracking or attendee details.

Integrations

Optional independent Zoom account connections provide a separate minimum-scope identity foundation. Explicit opt-in automatic Zoom supports personal and single-host team bookings with durable reconciliation, same-meeting rescheduling and cancellation, ready-only invitations and an optional ordinary Google calendar copy. Zoom is not a sign-in provider. See the Zoom account guide before configuring a real client.

Optional 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.

Known limitations

See the consolidated limitations and linked operating guides before deploying:

  • Public booking and sign-in pages have no CAPTCHA; invitee/guest addresses remain unverified. Email abuse limits apply per instance/database; separate instances do not share budgets. Per-IP request limits remain in process memory.
  • Microsoft Ca

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#time-tracking · #productivity · #saas