Skip to main content

just-book-my-cal

Test suites REUSE status The latest version can be found on PyPI. Information on what versions of Python are supported can be found on PyPI.

A small, self-hosted alternative to cal.com for one person: others book a meeting with you in the free times of your calendar. No accounts, no admin interface, no external services.

  • Reads your busy times from one or more public ICS feeds, including recurring events and moved or cancelled instances.
  • Offers only slots that fit your rules: working hours per weekday (with breaks), the meeting lengths you choose (e.g. 15/30/60 minutes), minimum notice, booking horizon and buffers before and after your meetings.
  • Books instantly. You and the booker both get a calendar invitation (ICS) by email, with a video link for each meeting (a room of its own, or a fixed address) and an optional reminder.
  • Both sides get a private link to reschedule or cancel. The booker can only move the meeting into slots that are free under your rules. You can move it anywhere, with a warning if that overlaps with something.
  • Visitors see the times in their own time zone (a few lines of JavaScript; without them, the times are shown in your time zone). Free times are listed one calendar week at a time, with arrows to the previous and next week.
  • Abuse protection: rate limiting, a honeypot field, CSRF protection and a cap on upcoming meetings per email address.

The only thing it stores is the bookings, in SQLite. Everything else is in one YAML file.

Quick start

You need uv and Python 3.10 or newer.

uv sync --no-dev
cp config.example.yaml config.yaml    # then edit it, at least the keys marked REQUIRED
uv run justbookmycal-cli -c config.yaml --db upgrade # creates the database; run again after updates
uv run justbookmycal-cli -c config.yaml --debug      # development server on http://127.0.0.1:5262

Without -c/--app-config, the config file is looked for in your user config directory, independent of the directory you start the app from: ~/.config/justbookmycal/config.yaml on Linux, ~/Library/Application Support/justbookmycal/config.yaml on macOS. Put your config.yaml there for a permanent installation, and the commands work without the option. The database (justbookmycal.db) lives next to it unless you set DATABASE_URI. For local development, set DATABASE_URI in your config to a path inside the repository, such as instance/justbookmycal.db, to keep it apart. If the config file is missing, the error message names the full path that was tried.

Before you publish the page, check that the app sees your calendar the way you expect:

export FLASK_APP='justbookmycal.wsgi:main()' CONFIG_FILE=config.yaml
uv run flask busy --days 7        # busy blocks found in your feeds
uv run flask slots --duration 30  # slots people could book

Configuration

All settings are explained in config.example.yaml, and validated against a JSON schema on start. Only five keys are required: SECRET_KEY, OWNER_NAME, OWNER_EMAIL, SYSTEM_EMAIL and ICS_FEEDS.

How your feed is read:

  • Events with TRANSP:TRANSPARENT ("free" in most calendar apps) and STATUS:CANCELLED never block time.
  • Everything else blocks, including all-day events. A missing TRANSP counts as busy, as the standard says. If birthdays or holidays in your feed block whole days, mark them as free or move them to another calendar.
  • Feeds are cached for FEED_CACHE_SECONDS. If a feed cannot be loaded, the last copy (up to 24 hours old) is used. If there is none, the page says that the calendar is unavailable and takes no bookings, rather than risking a double booking.
  • Bookings made through the app block their time immediately, even if your calendar app has not published the invitation in the feed yet.

Texts: EVENT_TITLE, EVENT_DESCRIPTION, EMAIL_BODY_BOOKER and EMAIL_BODY_OWNER are Jinja2 templates, so you can use the names of the participants, the message of the booker and conditions like {% if booker_message %}. The available variables are listed in the example configuration.

The invitation

The system address (SYSTEM_EMAIL, e.g. noreply@example.com) sends the mails and is the ORGANIZER of the event. Its display name is SYSTEM_NAME (default: your name). You and the booker are both ATTENDEEs. This is deliberate: an invitation that arrives in the mailbox of its own organizer is not handled as a normal invitation by Outlook, so with the system address as organizer both of you get an ordinary invitation, and updates and cancellations are recognised as such. A rescheduled meeting keeps its UID and gets a higher SEQUENCE, a cancellation is sent as METHOD:CANCEL.

Each mail contains a plain text part, the invitation as text/calendar; method=… (for the Accept/Decline buttons of mail clients) and the same ICS as an attachment. Times are sent in UTC. Each side receives its own private link in every mail (the first one and all updates and cancellations), never the link of the other side. The links are not stored: they are derived from SECRET_KEY and the booking, and only their SHA-256 hashes are in the database. Changing SECRET_KEY later breaks the links in mails sent afterwards for bookings that already exist (links sent before keep working), so avoid changing it while there are upcoming meetings.

If the mail server rejects the mails, the booking is kept and the booker is told. Because the owner's admin link would be lost, it is written to the log in this case.

Each meeting gets a video link rendered from the MEETING_URL template. The default creates a room like https://meet.jit.si/MaxMehl-JaneDoe-20261020-k7p2x: the random suffix makes it hard to guess, but the room name contains both names, so the operator of the video server can read them. Use your own Jitsi instance in the template if that matters to you, or a fixed address without variables to use one permanent room. You can change the link of a single meeting on its admin page, for example if the room needs to be different; the update is sent to both of you like a reschedule.

Deployment

Run the app with gunicorn behind a reverse proxy that terminates TLS:

uv run justbookmycal --app-config /path/to/config.yaml   # listens on 0.0.0.0:5262

The bundled gunicorn configuration uses a single worker on purpose: the rate limiter and the feed cache live in memory of the process. Pass your own with --gunicorn-config if you need something else.

By default, gunicorn listens on all interfaces (0.0.0.0:5262), so that a reverse proxy on the same machine, in another container or on another host can reach it. Only do this where the port is not reachable from the internet except through the proxy: the app has no login, and it trusts the X-Forwarded-For header of the proxy (see TRUSTED_PROXIES below), so visitors who reach the port directly could fake their IP and escape the rate limit. If the proxy runs on the same machine, restrict the app to localhost with --bind:

uv run justbookmycal --app-config /path/to/config.yaml --bind 127.0.0.1:5262

An nginx location for it:

location / {
    proxy_pass http://127.0.0.1:5262;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
}

TRUSTED_PROXIES (default 1) tells the app how many proxies to believe when determining the client IP, which the rate limit depends on. Set 0 only if the app is reachable directly. With too high a value, visitors can fake their IP and escape the rate limit. Use a systemd service or a container to keep it running, and create the database with --db upgrade after every update.

Security notes

  • The booking page is public and has no login, by design. It exposes your free and busy pattern (not the content of your events), and anyone can use it to put a meeting into your calendar, within the limits set by the rate limit, the per-email cap and your rules.
  • The manage and admin links are capabilities: whoever has the link can change or cancel the meeting. They must not be forwarded. The pages are not cached and send no Referer. Only a SHA-256 hash of each link is stored.
  • The database contains names, email addresses and messages of the people who booked. Keep the file out of any web root, restrict its permissions and treat backups as sensitive.
  • The mail text is built from your templates and the input of visitors. Names are restricted to characters that are safe in mail headers and ICS files.
  • Anyone can enter any email address when booking. The invitation goes to that address, so the app can be used to send mails to third parties (one invitation per booking, limited by the rate limit and the cap). If this worries you, lower RATE_LIMIT_POST.

Known limitations

  • Your own copy of a booking can block neighbouring times. When you accept the invitation in your calendar app and that app publishes it in your feed, the event is counted twice (once from the database, once from the feed). This is harmless for new bookings. When a booker moves a meeting, the feed's copy of the old time keeps blocking until the feed shows the change. Calendar apps that keep the invitation's UID (e.g. Thunderbird, Nextcloud) are ignored correctly during rescheduling. Exchange/Outlook assigns its own UID, so the old copy stays in the way until it disappears from your feed.
  • Only one organiser, one language (English) and one set of rules.
  • Calendar clients differ in how they show invitations. The mails follow the common conventions for Outlook and Thunderbird, but check that they look right in your setup before you rely on them.

Development

uv sync                  # with dev tools
mise run test-all        # ruff, ty, reuse and pytest
mise run fix-all         # auto-fix formatting and lint issues

See AGENTS.md for the layout of the code. Please use conventional commits. Releases are created by release-please.

License

Apache-2.0 for the code, CC0-1.0 for configuration files, CC-BY-4.0 for documentation. See the REUSE headers and the LICENSES directory.

Metadata

Release files for justbookmycal 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for justbookmycal 0.1.2
File Size Uploaded Provenance
justbookmycal-0.1.2.tar.gz 53.2 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for justbookmycal 0.1.2
File Interpreter ABI Platform
justbookmycal-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 116.9 kB

Release files / justbookmycal-0.1.2.tar.gz

Download URL justbookmycal-0.1.2.tar.gz
Size 53.2 kB
Tags Source
SHA-256 checksum
How to use checksums
2a125aa13778b3f00c2a5f43c59fd63f4e7b1b4cc0d5c068733fbdf0f5753b4d
BLAKE2b-256 checksum
How to use checksums
7f0b70deb9b2fe1e8e9d2620d73f8fbf333d6bcb29c1c6a3478578d8039c46b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / justbookmycal-0.1.2-py3-none-any.whl

Download URL justbookmycal-0.1.2-py3-none-any.whl
Size 63.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
403a1a43535f8cee9d221e32c6d15d88e7dbe3cad6d255cb34fab8a015109d8a
BLAKE2b-256 checksum
How to use checksums
2778ea33b6efb44b3538cfaf0d0baf275b8741089927a88c9dfc4418bace7469
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Security for justbookmycal 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for justbookmycal-0.1.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page