Fast, self-hostable scheduling. Like Cal.com, but written in Rust.
Website · Documentation · Releases
"Your time, your stack."
calrs is an open-source scheduling platform built in Rust. Connect your CalDAV calendar (Nextcloud, Fastmail, BlueMind, iCloud, Google...), define bookable meeting types, and share a link. No Node.js, no PostgreSQL, no subscription.
- Event types — bookable meeting templates with duration, buffer times, minimum notice, and availability schedule
- Per-event-type calendar selection — choose which calendars block availability for each event type (e.g. only check work calendar for work meetings); defaults to all calendars if none selected
- Availability engine — free/busy computation from availability rules + synced calendar events
- Recurring event support — RRULE expansion (DAILY/WEEKLY/MONTHLY with INTERVAL, UNTIL, COUNT, BYDAY, EXDATE)
- Conflict detection — validates against both calendar events and existing bookings
- Pending bookings — optional confirmation mode: host approves or declines from the dashboard or directly from the email
- Reschedule — guests and hosts can reschedule bookings without cancelling. Guests pick a new slot and the host approves; hosts reschedule instantly. Tokens are regenerated, CalDAV events are updated in place, and both parties are notified
- Timezone support — guest timezone picker with browser auto-detection, times displayed in the visitor's timezone
- Timezone-aware CalDAV events — events are stored with their original calendar timezone and converted to your host timezone for availability checks, so a 10:00 New York event correctly blocks 16:00 in Paris
- Availability troubleshoot — visual timeline showing why slots are available or blocked, with event details
- Calendar view toggle — guests can switch between month grid, week columns, and column (list) views on the slot picker. Hosts set the default view per event type
- Booking limits — cap bookings per day/week/month/year, or show only the earliest slot per day (one slot per day mode)
- Dynamic group links — combine usernames in a URL (
/u/alice+bob/intro) for instant collective meetings without creating a team. All participants' calendars are intersected. Autocomplete user picker in the event type editor, opt-out toggle in settings
- CalDAV sync — pull-based sync from any CalDAV server (Nextcloud, BlueMind, Fastmail, iCloud, Google, Zimbra, SOGo, Radicale...), with multi-VEVENT support for recurring event modifications
- On-demand sync — booking pages automatically sync the host's calendars if stale (>5 min), using RFC 4791 time-range filtering to fetch only future events
- CalDAV write-back — confirmed bookings pushed to the host's calendar, deleted on cancellation
- Calendar source management — add, test, sync, and remove sources from the web dashboard or CLI
- Provider presets — selecting BlueMind, Nextcloud, etc. auto-fills the CalDAV URL and shows setup tips
- Auto-discovery — principal URL and calendar-home-set discovered via PROPFIND (RFC 4791)
- Web booking page — public slot picker, booking form, and confirmation page
- User dashboard — manage event types, calendar sources, pending approvals, and upcoming bookings
- Admin dashboard — user management, auth settings, OIDC config, SMTP status, user impersonation
- Event type management — create/edit from the dashboard with availability schedule, location, and confirmation toggle
- Location support — video link, phone, in-person, or custom — displayed on booking pages, emails, and
.icsinvites - Dark/light theme — automatic via system preference, with manual toggle (System/Light/Dark) on public pages and dashboard settings
- Theme engine — 7 built-in themes (Default, Nord, Dracula, Gruvbox, Solarized, Tokyo Night, Vates) plus custom colors, configurable from the admin dashboard. Every theme adapts to both dark and light modes
- Cal.com-style slot picker — 3-panel layout (meeting info sidebar, calendar, time slots) with switchable month/week/column views, dynamic timezone labels with UTC offsets, filled calendar grid with clickable prev/next navigation
calrs supports seven distinct booking scenarios. Each serves a different use case — there is no overlap:
| Type | Who books? | How do they find it? | Assigned to | Example |
|---|---|---|---|---|
| Personal (public) | Anyone | Listed on your profile | You | Freelancer "30min intro call" |
| Personal (internal) | Invited guests only | Any colleague generates a link | You | Senior engineer: any teammate can share a "Code Review" link with an external contributor |
| Personal (private) | Invited guests only | You send an invite link | You | Executive coaching for selected clients |
| Team (public) | Anyone | Listed on team page | Round-robin (least busy) | Public "Support Call" page |
| Team (internal) | Invited guests only | Any employee generates a link | Round-robin (least busy) | Cross-team: Sales shares Support booking links with customers |
| Team (private) | Invited guests only | Owner sends an invite link | Round-robin (least busy) | Demo team: sales manager sends links to qualified leads |
| Dynamic group | Anyone with the URL | Ad-hoc link: /u/alice+bob/slug |
Event type owner | One-off sales call needing engineering support |
- Unified Teams — create teams from OIDC groups, hand-picked users, or both. Public or private visibility
- Scheduling modes — round-robin (any member free, least-busy assignment) or collective (all members must be free)
- Team admin role — team admins manage event types and settings without needing global admin
- Multi-timezone teams — set a wide availability window and let each member's synced CalDAV calendar handle the actual blocking
- Public team pages — bookable at
/team/{slug}/{event-slug} - Private teams — require an invite token link, preventing unsolicited external bookings of your colleagues
-
Create a team — go to Teams in the sidebar, click New team. Name it, pick public or private visibility, and add members (individual users and/or OIDC groups) from the unified search bar.
-
Create a team event type — go to Event Types, click New event type, and select your team as the owner. Choose a scheduling mode:
- Round-robin — picks the least-busy available member (with optional per-member weight priority)
- Collective — requires all members to be free at the same time
-
Share the booking link — public teams are bookable at
/team/{slug}/{event-slug}. Private teams use an invite token link from the team settings page.
Each team member's CalDAV calendars are checked for conflicts. The availability rules and overrides on the event type apply to all members.
- Public — listed on your profile or team page, bookable by anyone with the URL
- Internal — not listed publicly. Available for both personal and team event types. Any authenticated colleague can generate a single-use booking link from the Invite Links page and share it with an external contact (e.g., paste in Slack or a support ticket). The link expires after 7 days and can't be reused. Unlike private event types where only the owner distributes the link, internal lets anyone in the organization be a link distributor — ideal for cross-team services (support, IT help desk) and personal event types that colleagues need to share on your behalf
- Private — not listed publicly. Only the event type owner or team admin can send invite links to specific guests
- Booking invites — tokenized links with guest name, email, optional message, expiration, and usage limits. Guest info auto-filled on the booking form
- Quick link generation — one-click "Get link" on the Invite Links page and invite management page generates a single-use invite URL and copies it to clipboard. No form to fill
- Availability overrides — block specific dates (holidays, conferences) or set custom hours per event type. Overrides replace weekly rules for that day
Private team vs internal event type: A private team gates access at the team level — the team admin shares one invite link covering all the team's event types. Internal visibility gates access at the event type level — any authenticated employee can generate per-event-type links on the fly from the Organization dashboard. Use private teams when you want controlled distribution by a team admin. Use internal when you want self-serve link generation across the org (e.g., any Sales rep can generate a Support Call link for a customer).
- Local accounts — email/password with Argon2 hashing, server-side sessions, HttpOnly cookies
- OIDC / SSO — OpenID Connect via Keycloak, Authentik, etc. (authorization code + PKCE, auto-discovery)
- User roles — admin/user, first registered user becomes admin
- Registration controls — enable/disable open registration, restrict by email domain
- Email notifications — HTML emails with plain text fallback and
.icscalendar invites on booking, cancellation, and approval - Email approve/decline — approve or decline pending bookings directly from the notification email (token-based, no login required)
- Guest self-cancellation — guests can cancel or reschedule their own bookings via links in the confirmation email
- Reschedule notifications — rescheduled booking emails include old and new times, updated
.icsinvites, and reschedule/cancel action buttons - Additional attendees — guests can invite additional people to bookings (configurable per event type: 0/1/3/5/10 max). Additional guests receive ICS invites and appear on the confirmation page
- Booking reminders — automated email reminders before meetings, configurable per event type (1h / 4h / 1 day / 2 days)
- SMTP configuration — configure from CLI or admin dashboard
- Credential encryption — CalDAV and SMTP passwords encrypted at rest with AES-256-GCM; secret key auto-generated or provided via
CALRS_SECRET_KEY - Hidden password input — passwords never echoed to the terminal
- Automated test suite — 500+ tests covering web handlers, CLI commands, auth lifecycle, email rendering, RRULE expansion, iCal parsing, timezone conversion, availability computation, slot generation, database migrations, rate limiting, and more
- CI pipeline — every push and pull request runs
cargo fmt,cargo clippy,cargo test, and template validation via GitHub Actions - Docker images — pre-built multi-arch images (
amd64+arm64) published to GHCR on every release
- SQLite storage — single-file WAL-mode database, zero ops
- CLI — full command set for headless operation (init, source, sync, event-type, booking, config, user)
- Single binary — no runtime dependencies beyond the binary itself
- Structured logging —
tracing+tower-httpfor request-level observability, configurable viaRUST_LOG
Pre-built images are available on GitHub Container Registry for amd64 and arm64:
docker run -d --name calrs \
-p 3000:3000 \
-v calrs-data:/var/lib/calrs \
-e CALRS_BASE_URL=https://cal.example.com \
ghcr.io/olivierlambert/calrs:latestPodman works as a drop-in replacement — just use
podmaninstead ofdockerin all commands.
Then visit http://localhost:3000, register an account, and add your calendars from the dashboard.
services:
calrs:
image: ghcr.io/olivierlambert/calrs:latest
ports:
- "3000:3000"
volumes:
- calrs-data:/var/lib/calrs
environment:
- CALRS_BASE_URL=https://cal.example.com
restart: unless-stopped
volumes:
calrs-data:Works with both docker compose and podman-compose.
To build from source instead, replace
image: ghcr.io/olivierlambert/calrs:latestwithbuild: .(or usedocker build -t calrs .).
# Build from source
cargo build --release
# Install
sudo cp target/release/calrs /usr/local/bin/
sudo cp -r templates /var/lib/calrs/templates
# Create a system user
sudo useradd -r -s /bin/false -m -d /var/lib/calrs calrs
# Install and configure the service
sudo cp calrs.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now calrsEdit /etc/systemd/system/calrs.service to set CALRS_BASE_URL to your public URL. The service runs on port 3000 by default — put a reverse proxy in front for TLS (see Reverse proxy below).
cargo build --release
calrs serve --port 3000Then register at http://localhost:3000 — the first user becomes admin.
# Install mdBook (one time)
cargo install mdbook
# Build and serve the docs
cd docs
mdbook serve --openThis builds the user documentation from docs/src/ and opens it in your browser. The docs are also available as static HTML in docs/book/.
Once installed, you can manage everything from the web UI or use the CLI:
# Connect your CalDAV calendar
calrs source add --url https://nextcloud.example.com/remote.php/dav \
--username alice --name "My Calendar"
# Pull events
calrs sync
# Create a bookable meeting type
calrs event-type create --title "30min intro call" --slug intro --duration 30
# Check your availability
calrs event-type slots intro
# Book a slot
calrs booking create intro --date 2026-03-20 --time 14:00 \
--name "Jane Doe" --email jane@example.comcalrs connects to any CalDAV server. You need the DAV root URL for your provider — not a calendar-specific or public link. When adding a source from the web dashboard, selecting a provider auto-fills the URL pattern.
- BlueMind —
https://mail.yourcompany.com/dav/ - Nextcloud —
https://cloud.example.com/remote.php/dav - Fastmail —
https://caldav.fastmail.com/dav/calendars/user/you@fastmail.com/(use an app-specific password) - iCloud —
https://caldav.icloud.com/(use an app-specific password from appleid.apple.com) - Google —
https://apidata.googleusercontent.com/caldav/v2/your@gmail.com/ - Zimbra —
https://mail.example.com/dav/ - SOGo —
https://mail.example.com/SOGo/dav/ - Radicale —
https://cal.example.com/
calrs auto-discovers your principal URL and calendar-home-set via PROPFIND (RFC 4791). If the connection test hangs or fails, use the "skip connection test" option and try syncing directly.
-
In your Keycloak realm, create a new OpenID Connect client:
- Client ID:
calrs - Client authentication: ON (confidential)
- Valid redirect URIs:
https://your-calrs-host/auth/oidc/callback - Web origins:
https://your-calrs-host
- Client ID:
-
Copy the Client secret from the Credentials tab.
-
Configure calrs:
calrs config oidc \
--issuer-url https://keycloak.example.com/realms/your-realm \
--client-id calrs \
--client-secret YOUR_CLIENT_SECRET \
--enabled true \
--auto-register true- Set the base URL and start:
export CALRS_BASE_URL=https://your-calrs-host
calrs serve --port 3000The login page will show a "Sign in with SSO" button. With --auto-register true, users are created automatically on first OIDC login. Existing local users are linked by email.
calrs listens on HTTP (port 3000 by default). Use a reverse proxy for TLS termination.
The simplest option — automatic HTTPS with Let's Encrypt:
cal.example.com {
reverse_proxy localhost:3000
}
Save as /etc/caddy/Caddyfile and reload: sudo systemctl reload caddy.
server {
listen 443 ssl http2;
server_name cal.example.com;
ssl_certificate /etc/letsencrypt/live/cal.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cal.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name cal.example.com;
return 301 https://$host$request_uri;
}Get certificates with certbot: sudo certbot --nginx -d cal.example.com.
Important: Set
CALRS_BASE_URLto your public URL (e.g.https://cal.example.com) so that OIDC redirect URIs and email links point to the right host.
calrs uses structured logging via the tracing crate. All log output goes to stderr and is captured by systemd journal, Docker logs, or any log aggregator.
Set the log level via the RUST_LOG environment variable:
# Default (recommended for production)
RUST_LOG=calrs=info,tower_http=info
# Verbose (debug HTTP requests + internal events)
RUST_LOG=calrs=debug,tower_http=debug
# Quiet (errors only)
RUST_LOG=calrs=error| Category | Level | Events |
|---|---|---|
| Auth | info/warn | Login success/failure, registration, logout, OIDC login |
| Bookings | info | Created, cancelled, approved, declined, guest self-cancel, reminder sent |
| CalDAV | info/error | Sync started/completed, write-back success/failure, source added/removed |
| Admin | info/warn | Role changes, user enable/disable, auth/OIDC config updates, impersonation |
| debug/error | Delivery success, send failures | |
| HTTP | info | Every request via tower-http TraceLayer (method, path, status, latency) |
| Database | info | Migration applied on startup |
| Server | info | Startup, shutdown |
2026-03-12T14:30:00Z INFO calrs: calrs server listening on 127.0.0.1:3000
2026-03-12T14:30:05Z INFO calrs::auth: user login email=alice@example.com ip=192.168.1.1
2026-03-12T14:31:00Z INFO calrs::web: booking created booking_id=a1b2c3 event_type=intro guest=bob@example.com
2026-03-12T14:31:01Z INFO tower_http::trace: response{method=POST path="/u/alice/intro/book" status=200 latency="45ms"}
2026-03-12T14:31:02Z ERROR calrs::web: CalDAV write-back failed uid=a1b2c3@calrs error="connection refused"
2026-03-12T14:32:00Z WARN calrs::auth: login failed email=eve@example.com ip=10.0.0.5
2026-03-12T15:00:00Z WARN calrs::web: rate limited ip=10.0.0.5
calrs source add [--no-test] Connect a CalDAV calendar
calrs source list List connected sources
calrs source remove <id> Remove a source
calrs source test <id> Test a connection
calrs sync [--full] Pull latest events from CalDAV
calrs event-type create Define a new bookable meeting
calrs event-type list List your event types
calrs event-type slots <slug> Show available slots
calrs calendar show [--from] [--to] View your calendar
calrs booking create <slug> Book a slot
calrs booking list [--upcoming] View bookings
calrs booking cancel <id> Cancel a booking
calrs config smtp Configure SMTP for email notifications
calrs config show Show current configuration
calrs config smtp-test <email> Send a test email
calrs config auth Configure registration/domain restrictions
calrs config oidc Configure OIDC (SSO via Keycloak, etc.)
calrs user list List users
calrs user create Create a user
calrs user set-password <email> Set a user's password
calrs user promote <email> Promote user to admin
calrs serve [--host 127.0.0.1] [--port 3000] Start the web booking server
calrs/
├── Cargo.toml
├── migrations/ SQLite schema (incremental)
├── templates/ Minijinja HTML templates
│ ├── base.html Base layout + CSS (dark mode)
│ ├── auth/ Login + registration
│ ├── dashboard_base.html Sidebar layout for all dashboard pages
│ ├── dashboard_overview.html Overview with stats
│ ├── dashboard_event_types.html Event types listing
│ ├── dashboard_bookings.html Bookings listing
│ ├── dashboard_sources.html Calendar sources
│ ├── dashboard_teams.html Teams listing
│ ├── dashboard_internal.html Internal/organization event types
│ ├── admin.html Admin panel
│ ├── settings.html Profile & settings
│ ├── event_type_form.html Create/edit event types
│ ├── invite_form.html Invite management (send + list)
│ ├── overrides.html Date overrides per event type
│ ├── source_form.html Add CalDAV source (provider presets)
│ ├── source_test.html Connection test / sync results
│ ├── source_write_setup.html Write-back calendar selection
│ ├── team_form.html Create/manage teams
│ ├── team_settings.html Team settings (members, groups)
│ ├── team_profile.html Public team page
│ ├── profile.html Public user profile
│ ├── troubleshoot.html Availability troubleshoot timeline
│ ├── slots.html Slot picker (timezone aware)
│ ├── book.html Booking form
│ ├── confirmed.html Confirmation / pending page
│ ├── booking_approved.html Email approve success
│ ├── booking_decline_form.html Email decline form
│ ├── booking_declined.html Email decline success
│ ├── booking_cancel_form.html Guest self-cancel form
│ ├── booking_cancelled_guest.html Guest cancel success
│ ├── booking_host_reschedule.html Host reschedule page
│ ├── booking_reschedule_confirm.html Reschedule confirmation
│ └── booking_action_error.html Invalid/expired token error
└── src/
├── main.rs CLI entry point (clap)
├── db.rs SQLite connection + migrations
├── models.rs Domain types
├── auth.rs Authentication (local + OIDC)
├── email.rs SMTP email with .ics invites + HTML templates
├── rrule.rs Recurring event expansion (RRULE)
├── utils.rs Shared utilities (iCal splitting/parsing)
├── caldav/mod.rs CalDAV client (RFC 4791) + write-back
├── web/mod.rs Axum web server + all handlers
└── commands/ CLI subcommands
Storage: SQLite (WAL mode). Single file, zero ops.
CalDAV: Pull-based sync for free/busy, write-back for confirmed bookings.
- CalDAV sync (pull) with auto-discovery
- Availability engine with conflict detection
- Recurring event expansion (RRULE)
- Email notifications with
.icsinvites - Web booking page with dark mode
- Authentication (local + OIDC/SSO)
- User and group management
- Team event types (combined availability + round-robin)
- Timezone support (guest picker + CalDAV event timezone conversion)
- Calendar source management from the web UI
- Docker image + systemd service
- CalDAV write-back (push confirmed bookings to your calendar)
- Availability troubleshoot page
- HTML emails with action buttons
- Email approve/decline for pending bookings
- Admin impersonation
- Per-event-type calendar selection
- Unified Teams (public/private, round-robin/collective, team admin role)
- Private event types with invite links
- Cal.com-style slot picker (month calendar, 3-panel layout)
- Dark/light theme toggle
- Theme engine (7 presets + custom colors)
- Additional attendees on bookings
- Reschedule flow (change date/time without cancelling)
- Availability overrides (block specific dates, set custom hours)
- Three-level visibility (public / internal / private) with quick invite link generation
- Calendar view toggle (month / week / column)
- Booking frequency limits + one slot per day
- Webhooks (per-event-type HTTP callbacks on new/cancelled bookings)
- Delta sync using CalDAV
sync-token/ctag - Multi-language support (i18n)
- REST API for third-party integrations
AGPL-3.0 — free to use, modify, and self-host. Contributions welcome.
























