Changelog
This changelog is maintained manually. The canonical release notes, including asset downloads, are on GitHub Releases.
1.18.0
Section titled “1.18.0”Optional OpenID Connect single sign-on.
-
Sign in with SSO (OIDC). calit can now sit behind any OpenID Connect provider (Authelia, Keycloak, Auth0, Zitadel, Authentik, …) as a relying party. It is optional and off by default (
OIDC_ENABLED=false) — form login is unchanged whether or not OIDC is configured. SSO is login-only: once the provider verifies the identity, calit bridges into its normal session, so there is no OIDC session to manage. Accounts link by the verifiedemailclaim; an unmatched login provisions a new account only whenSIGNUP_ENABLED=true, and a verified email matching more than one account is rejected rather than guessed. OptionalOIDC_ADMIN_GROUPgrants calit admin from agroupsclaim (grant-only — never demotes a locally-granted admin). Configure withOIDC_ENABLED,OIDC_ISSUER_URL,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET, and optionalOIDC_ADMIN_GROUP; the redirect URI is${APP_BASE_URL}/api/oidc/login. See OIDC / SSO setup. -
Localised booking-page dates. The timezone bar and slot-day date on the public booking pages now localise to the viewer’s language (en/de/he) instead of always rendering in English.
1.17.0
Section titled “1.17.0”Self-hosted ALTCHA CAPTCHA and email invitations for admin-created users.
-
Invite users by email. When a site admin adds a user, calit now sends the person an invitation email with a link to set their own password, instead of the admin setting a temporary password to share out-of-band. The new account stays dormant (cannot log in) and shows Awaiting activation until the link is used; the activation link is valid for 48 hours, and admins can Resend invite for a fresh link. Uses the existing
MAIL_*andAPP_BASE_URLconfig — no new settings. See Users & admin. -
Self-hosted ALTCHA CAPTCHA. The booking form’s bot protection is now pluggable via
CAPTCHA_PROVIDER(none|turnstile|altcha). The new ALTCHA option is a privacy-first, self-hosted proof-of-work challenge — no external service, no third-party account, and it works air-gapped (the widget script is served by calit, not a CDN). Configure it withCAPTCHA_PROVIDER=altcha+ALTCHA_HMAC_KEY(and optionalALTCHA_MAX_NUMBER); the widget localises to en/de/he automatically. See ALTCHA setup. Existing Turnstile deployments are unaffected —TURNSTILE_ENABLED=truestill selects Turnstile.
1.16.0
Section titled “1.16.0”Multi-host meeting types — a meeting type can now require more than one host.
- Multi-host meeting types. A meeting type can have up to 10 hosts
total (the creator plus up to 9 co-hosts). Add co-hosts by username from
the meeting-type form — an autocomplete suggests matching usernames as you
type. The type only becomes bookable once every co-host has accepted;
each invited co-host gets a one-click accept/decline email link and a
pending request on their own
/medashboard, under a new Shared section. Each co-host sets their own working hours and buffers for the shared type independently — duration, minimum notice, and booking horizon still come from the creator’s settings. Bookable slots are the intersection of every host’s availability, and one booking creates a single calendar event shared by all hosts. The public page is reachable at/<anyHost>/<slug>— every accepted host’s username is a valid alias for the same booking page, though the creator’s URL is the canonical one used in emails. A shared type shows as temporarily unavailable — no bookable slots — while any host hasn’t accepted, is disabled, or has a disconnected Google Calendar; calit never offers a slot it can’t verify for every host. A requested slug is blocked if it collides with any host’s existing slugs, in either direction. See Multi-host meeting types. - Cancel and reschedule act on the whole group. Cancelling or rescheduling a multi-host booking (from any host’s Manage link, or the invitee’s) applies to every host at once — one shared calendar event, one set of notifications. For an approval-required shared type, any host’s decline kills the booking, and rescheduling returns the whole group to pending approval.
- Behavior change: single-host approval reschedule. As part of the same work, an owner-initiated reschedule of a single-host, approval-required booking now stays confirmed instead of reverting to pending approval — only an invitee-initiated reschedule still sends it back to pending. Previously any reschedule of an approval-required booking reverted it, regardless of who initiated it.
- Fix: booking-hold constraint is now owner-scoped. The database constraint that rejects overlapping held bookings was instance-wide, ignoring which owner a booking belonged to — a latent bug that could also let per-host rows of a multi-host booking collide with each other. It is now scoped per owner, so different owners may legitimately hold overlapping bookings, but no single owner can ever double-book itself (V22 migration).
- Progressive enhancement, not “no JavaScript.” calit’s long-standing “no JavaScript ships at runtime” rule is now “progressive enhancement” — every feature must work fully without JavaScript, and JavaScript may only enhance it. The multi-host co-host autocomplete is the first feature built this way: typing a username works and suggests matches with JavaScript on, and still works as a plain text field with JavaScript off.
- Booking page layout fixes. The time-slots column now matches the calendar’s height and scrolls on its own instead of looking stunted (or running longer than the calendar) beside it; available times render as a compact grid rather than one lonely full-width button per row, and the day and time you pick are echoed in the sidebar. The month calendar is right-sized instead of oversized.
- Cleaner, more consistent screens. Public pages are centered with the footer pinned to the bottom — now a single row with a clearer language switcher. Admin forms share one comfortable width, the co-host box is a roomier card, and time fields no longer overlap their picker icon.
1.15.1
Section titled “1.15.1”A follow-up fix to the 1.15.0 booking editor.
- A too-long meeting name or description now shows a clear error instead of silently failing. Longer text — especially in non-Latin scripts like Hebrew, or with emoji — could be rejected by the server with an opaque low-level error before the form’s own length check ran. The size limit was raised so the normal “too long” validation always applies.
1.15.0
Section titled “1.15.0”Both the host and the invitee can now edit a booking after it’s made — its name, description, and guest list — not just reschedule or cancel it.
- Edit a booking’s name, description, and guests after booking. From the
Manage page — both the host (/me → a booking’s Manage link) and the
invitee (their manage link) — you can now rename a meeting, set or clear its
description, and add or remove guests, all without rescheduling. The change is
emailed to the other party and pushed to the Google Calendar event and the
.icsinvite (guests get an updated invite too). An untouched save changes nothing and notifies no one. - Reschedule is now time-only. Guest editing moved into the new Edit name & description section, so the reschedule step only moves the meeting; picking the same time again is a no-op. The owner can now manage a booking’s guest list too (previously guests were owner-read-only).
- The Google Calendar event description now reflects the meeting’s description
(previously a fixed “Booked via calit.” placeholder). The meeting’s displayed
name across emails, the
.ics, and the calendar event follows any per-booking rename.
1.14.1
Section titled “1.14.1”Reschedule and cancellation emails now name the right person.
- Host-initiated reschedules and cancellations are attributed correctly. When the owner rescheduled or cancelled a booking (from /me or an owner email link), the notifications still read “{guest} rescheduled their booking” — blaming the guest for something the host did. The wording now follows who actually acted: a host-initiated change tells the guest “{owner} rescheduled/cancelled your booking” and gives the host a neutral notice, while guest-initiated changes are unchanged.
1.14.0
Section titled “1.14.0”Google-native guest invites — when Google is connected, Google is the single calendar source for everyone on a booking.
- Guests now appear on the Google Calendar event. When the owner has Google connected, invitee-added guests are added as attendees on the Google event (previously only the invitee and owner were), so they show up in the participant list and receive Google’s own invitation. Guest changes stay in sync: declining a guest or rescheduling re-syncs the event’s attendees, and a removed guest gets Google’s cancellation.
- No duplicate calendar entries when Google is connected. calit no longer
attaches its own
.icsto booking emails when Google is connected — Google sends the authoritative invite/update/cancellation. calit still emails everyone so its Reschedule (invitee) and Decline (guest) links, which Google’s native invite doesn’t carry, still reach them. When Google is not connected, calit’s.icsremains the only calendar source, unchanged. - Tidier emails. Removed the redundant “This message was sent to the …” footer line from all booking emails.
Known limitation: because guests are now Google attendees, Google shows them its own Accept/Decline buttons; a guest who responds in Google instead of via calit’s decline link won’t update calit’s guest list. No configuration or migration steps.
1.13.0
Section titled “1.13.0”Owner-side booking management, plus a friendlier email sender name.
- Owners can reschedule and cancel bookings. Every upcoming booking on the
owner dashboard (
/me) now has a Manage link to a page that reschedules (from your own availability slots) or cancels the booking, notifying the invitee and guests. The same Reschedule or cancel link appears in your copy of the confirmation, reschedule, and reminder emails; it is login-gated and only works for the signed-in owner. Rescheduling preserves the booking’s guests. - Friendlier email sender name. Booking emails are now sent from
<Owner name> via calitinstead of a bare address that some clients rendered as “Notify”. The underlyingMAIL_FROMaddress and the.icsorganizer are unchanged, so SPF/DKIM and Gmail invite rendering are unaffected. No configuration or migration steps.
1.12.1
Section titled “1.12.1”A fix for booking invites in Gmail.
- Gmail “Unable to load event” fixed. Booking
.icsinvitations now set the calendarORGANIZERto the address mail is actually sent from (MAIL_FROM), keeping the owner’s name as the organizer display name. Gmail refuses to render an invitation whose organizer differs from the sender, so invitees and guests previously saw “Unable to load event” instead of the event card. No configuration or migration steps — pull:1.12.1(or:1.12.1-native) as usual.
1.12.0
Section titled “1.12.0”Invitee guests, plus internal code-formatting tooling.
- Invitee guests. Invitees can now add guests to a booking — a chips field on
the booking form (and on the reschedule page) takes up to 10 guest emails. Guests
receive their own calendar invite and stay in sync: they get an
.icsinvitation when the meeting is created, an update when it is rescheduled, and a cancellation when it is cancelled. Guests cannot reschedule or cancel the meeting; a guest who can’t attend uses a decline link in their invitation, which removes them and notifies the invitee. No configuration or migration steps beyond the usual upgrade. - Code formatting (contributor-facing). The codebase is now auto-formatted with
Spotless + palantir-java-format (Java) and Prettier (JS/CSS), enforced by a lefthook
pre-commit hook and the CI
verifygate. No runtime or configuration impact — pull:1.12.0(or:1.12.0-native) as usual.
1.11.1
Section titled “1.11.1”A small fix for the native image.
- Native image footer shows the real version again. The page footer on the
native (
-native) image displayeddev devinstead of the release version and commit. The native build was compiling out the build-stampedgit.properties; it is now explicitly bundled. The JVM image was unaffected. No configuration or upgrade steps are needed — pull:1.11.1-native(or:latest-native).
1.11.0
Section titled “1.11.0”An optional GraalVM native container image with a much smaller runtime footprint, published alongside the default JVM image.
- Native image variant (
-nativetags). Every published tag now has a GraalVM native counterpart —:latest-native,:edge-native,:1.11.0-native, etc. — built ahead-of-time and run on a minimal Alpaquita musl base with no JRE. Compared to the JVM image it is roughly half the size (~115 MB vs ~205 MB), uses far less memory at idle (~60 MB vs ~300 MB), and starts in well under a second. It is functionally identical and multi-arch (amd64 + arm64); the JVM image remains the default. Pick whichever fits your host — see Docker Compose install.
1.10.0
Section titled “1.10.0”Hebrew (right-to-left) localization, plus a round of booking-email improvements: approve/decline straight from email, role-specific owner and invitee copies, valid calendar invites, an in-email cancel link, and immediate locale switching in Settings.
-
Hebrew localization with right-to-left (RTL) support. The entire UI — public booking pages, the owner admin UI, and all notification emails — is now available in Hebrew (
עברית) alongside English and German. When Hebrew is active, calit automatically mirrors the layout right-to-left (<html dir="rtl">) for both web pages and emails; no setting controls this, it follows the chosen language. Like German, Hebrew needs no configuration — it is always available, selectable from the footer language switcher (visitors) or Settings (owners), with untranslated phrases falling back to English. See Language & localization. -
Approve or decline pending bookings straight from email. When a booking requires approval, the request email now carries one-click Approve and Decline links. They open the owner console — if you are not signed in you log in first and are returned to the action — so only the authenticated owner can act on their own request. See Bookings & approvals.
-
Cancel link in invitee emails. Booking emails now include a direct Cancel this booking link (alongside the manage link), which opens a confirmation page before releasing the slot.
-
Role-specific booking emails. Owner and invitee copies of every booking email now differ appropriately: the owner copy is addressed to the owner and names the invitee, the invitee copy is addressed to the invitee. Each side only sees the links relevant to it.
-
Calendar invites (
.ics) fixed for Gmail. The attached invite is now a valid iTIP request (it includes the attendee), so Gmail and other clients render the event card instead of showing “Unable to load event”. -
Language changes in Settings apply immediately. Changing your admin language under Settings now updates the page in the same response, rather than after navigating away and back.
Google OAuth verification, German localization, and footer & first-run polish.
-
Google OAuth verification support. A hosted instance can now pass Google’s OAuth verification: set
OPERATOR_NAMEandPRIVACY_CONTACT_EMAILto serve a complete privacy policy at/privacyand terms at/terms(including Google’s required Limited Use disclosure), and optionallyGOOGLE_SITE_VERIFICATIONto render the Search Console<meta>tag for domain verification. All three are optional; unset leaves the feature off (no tag; pages fall back toAPP_BASE_URL). See Google OAuth setup. -
German localization (English default + fallback). The entire UI — public booking pages, the owner admin UI, and all notification emails — is now available in English and German. No configuration or environment variables are required: both languages are always on, and any untranslated phrase falls back to English. Booking visitors get a language switcher in the page footer (choice persisted in a
calit_langcookie, otherwise detected fromAccept-Language), and the language used when booking is reused for that booking’s follow-up emails. Account owners choose their own language in Settings, applied to their admin UI and the notification emails they receive. See Language & localization. -
Build info in the footer. Every page now shows the running release version and short git commit in the footer (e.g.
calit 1.8.0 · a1b2c3d), so you can tell at a glance which build a deployment is running. -
Footer, language switcher & first-run polish. The footer is now a single shared component on every page (public and admin) with improved contrast, and the language switcher is a no-JS dropdown that scales past a handful of languages.
/privacyand/termsare reachable before the first user is created (so Google’s verification crawler can read them on a fresh instance). First-run setup auto-detects the visitor’s timezone (falling back to UTC instead of a hardcoded zone). The privacy/terms pages now carry the full canonical policy in the site’s visual style, and the marketing landing page is pinned to its light theme so its footer stays readable in dark-mode browsers.
Scheduler timing control and crash-safe dispatch.
- Configurable grace window. New
SCHEDULER_GRACE_SECONDSsetting (default30,0= exact). The reminder and pending-expiry ticks now treat a row as due up to N seconds early (send_at <= now() + grace), so replicas ticking on independent timers fire on time instead of waiting up to a whole extra tick. Postgresnow()remains the single clock authority, so app-replica clock skew never affects which rows are due — this only smooths per-node tick latency. - Crash-safe reminder & auto-decline dispatch. Both ticks now render the outgoing email and write it to the email outbox inside the same transaction that claims the row (marks the reminder sent / flips the booking to declined), instead of firing a post-commit in-memory event that a node crash between commit and send could drop. Claim and intent-to-send now commit atomically; the existing outbox tick delivers with retry/backoff. The manual owner-decline path is unchanged.
- New
SCHEDULER_GRACE_SECONDSconfig. Dependency updates: Quarkus 3.36.3,google-api-services-calendar, andactions/checkoutv7.
Google Calendar disconnect detection.
- Booking page fails closed when Google is unreachable. Previously a silent disconnect (dead refresh token) made every slot appear free, risking double-bookings. Now the public page shows “Scheduling temporarily unavailable” and new bookings are blocked while the calendar can’t be read, so nothing lands on an event calit can’t see.
- Hourly connection probe. Each connected Google account is checked on a
schedule (a forced refresh-token round-trip), distinguishing a permanently
dead grant from a transient blip. The probe also keeps the token warm,
preventing the 6-months-unused expiry. Multi-node-safe with
SELECT … FOR UPDATE SKIP LOCKED, no leader. - Reconnect email. The owner is emailed once per outage with a link to
reconnect (
/me/google); the alert re-arms after the account recovers. - Most recurring disconnects come from leaving the Google OAuth app in “Testing” publishing status (7-day refresh-token expiry) — publish it to “In production” to avoid them.
- New
GOOGLE_PROBE_INTERVALsetting (duration, default1h). New V15 migration addsreconnect_notified_atandlast_probed_atcolumns.
Resilient email delivery and health probes.
- Email survives SMTP outages. Mail is sent synchronously; if a send
fails, the message is parked in a new database outbox instead of being lost
and retried by a background tick (every 60 s, on every replica, claimed with
SELECT … FOR UPDATE SKIP LOCKED— multi-node-safe, no leader). Retries use exponential backoff (1 min doubling to 1 h, capped at 10 attempts). Booking and password-reset flows no longer fail when SMTP is unavailable. - Time-sensitive mail carries a deadline: a queued password-reset email is dropped once its 30-minute token has expired, so a recovered SMTP server never delivers a dead reset link.
- Health probes.
GET /q/health/live(liveness, process only) andGET /q/health/ready(readiness). The SMTP and Google checks are informational — alwaysUP, exposing reachability underdata.state— so a down mail server never pulls a replica out of rotation now that the outbox covers delivery. - New V14 migration adds the
email_outboxtable. No new configuration — the outbox is always on and reuses the existing mailer settings.
Self-service password reset.
- Users who forget their password can reset it from the sign-in page via Forgot password?. Requesting by username emails a single-use, 30-minute reset link to the account’s stored address.
- The request never reveals whether an account exists (anti-enumeration); only a hashed token is stored server-side.
- Google-only accounts can set a password through the same flow.
- New V13 migration adds the
password_reset_tokentable. No new configuration — reuses the existing mailer settings.
Token-at-rest encryption and security audit remediation.
- Google OAuth tokens are now encrypted at rest using AES-256-GCM
(
TOKEN_ENCRYPTION_KEY). Existing plaintext tokens are back-filled automatically on first boot — no reconnection required. - Added
TOKEN_ENCRYPTION_KEYconfig; production startup fails closed if the key is absent or too weak (mirrors the existingSESSION_ENCRYPTION_KEYguard from 1.3.1). - Security audit remediation: CSRF tokens on all state-changing form POSTs,
structured audit log for admin actions and failed logins, ReDoS-safe email
regex, outbound HTTP timeouts and redirect policy, self-lockout and
last-admin removal blocked, owner-scope invariant asserted at the JSON API
layer, SQL logging restricted to
%dev. - Container hardened: non-root runtime user, base-image digest pinning, Trivy image-scan gate in CI, CodeQL analysis added.
- Google OAuth redirect URIs now derived from
APP_BASE_URL(no localhost leak in production). TOKEN_ENCRYPTION_KEYmust not be rotated after first boot without re-linking all Google accounts (see Upgrading).
Production startup secret guard.
- App now fails fast at startup in
%prodif required secrets (SESSION_ENCRYPTION_KEY, etc.) are missing or set to weak/dev defaults.
Sign in with Google.
- Users can authenticate via “Sign in with Google” in addition to username/password.
- Existing accounts are auto-linked by verified email; unknown Google identities can be provisioned as new passwordless users.
- Single-use login tickets bridge the Google OAuth callback to the existing form-auth session.
- New V11 migration: nullable
password,google_sub, andlogin_ticketcolumns onapp_user. - Copy-meeting-type-link button added to meeting-type cards.
Seven-day schedule grid and brand favicon.
- Weekly availability is now displayed and edited as a seven-day grid (global schedule and per-meeting-type overrides).
- Bulk replace-all endpoints for weekly schedule slots.
- Brand favicon added matching the landing-page chip.
- Google Meet hint hidden on booking pages when the host has no connected Google account.
Multi-account Google Calendar.
- Users can connect more than one Google account; each is tracked with its own credentials.
- New
/me/googleUI for selecting which calendars to read for free/busy and which account to write new events to. - FreeBusy checks fan out across all connected accounts; write-target routes to the selected account.
- New V4-extension migration for multi-account schema fields.
Postgres 18 volume fix, trademark disclaimer, version bump.
- Fixed Docker Compose volume configuration incompatible with Postgres 18.
- Added trademark disclaimer to README.
- Dependency and version bumps.
Initial release.
- Self-hosted, multi-user scheduling application on Quarkus / Java.
- Per-user booking pages at
/<username>/<slug>. - Google Calendar integration (read free/busy, write events).
- Email confirmations with
.icsinvites. - Admin UI at
/mefor managing meeting types, availability, and settings. - Site-admin user management at
/me/users. - Docker Compose deployment; native multi-arch images published to
ghcr.io/asm0dey/calit. - CI pipeline (GitHub Actions) with build, test, and release stages.