Neuer Router unter /api/local mit Upload, Liste, Download, Thumbnail und Loeschen, dazu permissions.readable_local_event als fehlendes Gegenstueck zu _writable_event. Der Lese-Helfer prueft nicht nur den Kalender, sondern auch die Privatsphaere: ein fremder privater Termin wird im Lesepfad maskiert, also duerfen auch seine Anhaenge nicht ueber die UID erreichbar sein. Fremde bekommen durchgaengig 404 statt 403, damit die Existenz nicht durchsickert. Die Mengenbegrenzung zaehlt nach dem Insert innerhalb derselben Transaktion -- zaehlen-dann-einfuegen waere ein Wettlauf, bei dem zwei gleichzeitige Uploads beide 9 sehen. Ausgeliefert wird immer als Download mit nosniff, nie inline. Dazu die Capability-URL unter /api/attach/<token> fuer externe CalDAV-Clients, abschaltbar ueber ATTACHMENT_PUBLIC_LINKS=0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Calendarr
das minimalistische Kalender Frontend
A clean, self-hosted calendar application with support for CalDAV, Google Calendar, iCal subscriptions, and local calendars — all in one dark-themed, privacy-first UI.
Features
Calendar Views
- Month, Week, Day, and Agenda views
- Configurable week start day (Monday / Sunday)
- Adjustable hour height in week/day view
- Calendar week display in month and week views
Calendar Sources
- CalDAV — connect to any standard CalDAV server (Nextcloud, Baikal, Radicale, etc.)
- Google Calendar — OAuth2 integration, individual calendars shown in sidebar
- iCal Subscriptions — subscribe to any
.icsURL with configurable refresh interval - Local Calendars — create and manage calendars directly in Calendarr
Events
- Create, edit, and delete events with title, description, location, and time
- All-day events
- Per-event color overrides
- iCal event overrides (hide or edit individual events from subscriptions)
- Dim past events option
User Management
- First-run setup wizard (first user is automatically admin)
- Admin can create additional users
- Per-user settings, avatar, and 2FA
Authentication
- Username/password login (case-insensitive)
- 7-day JWT sessions
- Two-Factor Authentication (TOTP) — works with any authenticator app (Bitwarden, Google Authenticator, etc.)
Customization (per user)
- Primary, accent, and today-highlight colors
- Text contrast level (4 steps)
- Line/border contrast level (4 steps)
- Hour height in time-grid view
- Default view preference
Privacy
- Fully self-hosted — your data stays on your server
- All application data stored in Switzerland (if hosted there)
- Google data disclaimer shown when Google integration is used
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | Vanilla JS (ES modules), HTML5, CSS3 |
| Backend | Python · FastAPI · Uvicorn |
| Database | SQLite (via SQLAlchemy) |
| Auth | JWT (python-jose) · bcrypt · pyotp |
| CalDAV | caldav · icalendar |
| Image | Pillow · CropperJS |
No frontend framework dependencies — the entire UI is plain HTML/CSS/JS.
Installation
Requirements
- Python 3.9+
- Linux (Debian/Ubuntu recommended)
Quick Start
git clone https://git.scarriffle.com/Scarriffle/calendarr
cd calendarr
./install.sh
./start.sh
The app will be available at http://localhost:8080. On first visit, the setup wizard creates the admin account.
systemd Service
sudo cp calendarr.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now calendarr
Configuration
Configuration is done via environment variables. The install.sh script generates a .env file automatically.
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
(auto-generated) | JWT signing key — use a strong random value in production |
PORT |
8080 |
Server port |
HOST |
0.0.0.0 |
Bind address |
DATA_DIR |
./data |
Directory for SQLite database and avatars |
GOOGLE_CLIENT_ID |
— | Google OAuth2 Client ID (optional) |
GOOGLE_CLIENT_SECRET |
— | Google OAuth2 Client Secret (optional) |
GOOGLE_REDIRECT_URI |
— | e.g. https://yourdomain.com/api/google/callback (optional) |
PUBLIC_BASE_URL |
(derived) | Public origin behind a reverse proxy — required for SSO |
OIDC_PROVIDERS |
— | Comma-separated provider keys, e.g. authentik (optional) |
Per provider <KEY> listed in OIDC_PROVIDERS (variable names are uppercased):
| Variable | Default | Description |
|---|---|---|
OIDC_<KEY>_ISSUER |
— | Required. Issuer URL, e.g. https://authentik.example.com/application/o/calendarr/ |
OIDC_<KEY>_CLIENT_ID |
— | Required. Client ID of the web application |
OIDC_<KEY>_CLIENT_SECRET |
— | Leave empty for a public client (PKCE only) |
OIDC_<KEY>_NAME |
(key) | Label on the login button |
OIDC_<KEY>_SCOPES |
openid profile email |
Requested scopes — must be quoted, see below |
OIDC_<KEY>_MOBILE_CLIENT_ID |
— | Client ID(s) of the native apps, comma-separated |
OIDC_<KEY>_MOBILE_ISSUER |
(issuer) | Only if mobile uses a second application |
OIDC_<KEY>_ALLOW_SIGNUP |
false |
Create accounts on first SSO login |
OIDC_<KEY>_ALLOWED_DOMAINS |
— | Email-domain allowlist for signup |
OIDC_<KEY>_LINK_BY_EMAIL |
false |
Auto-link to an existing account by email |
OIDC_<KEY>_USERNAME_CLAIM |
preferred_username |
Claim used to derive the username |
OIDC_<KEY>_REDIRECT_URI |
(derived) | Overrides the derived callback URL |
Single Sign-On (OpenID Connect / Authentik)
Adds an SSO button to the login screen alongside password login. With
OIDC_PROVIDERS unset, nothing changes — the feature is invisible.
In Authentik: create one OAuth2/OpenID provider, client type Public (the web flow uses PKCE, so a secret buys little), with two redirect URIs:
- Web:
https://calendar.example.com/api/auth/oidc/authentik/callback - iOS:
com.scarriffleservices.calendarr.ios:/oauth2redirect - Android:
com.scarriffle.calendarr:/oauth2redirect
The mobile apps are separate repositories (Calendarr-IOS, Calendarr-Android)
and authenticate as public clients via AppAuth — PKCE, no client secret.
Set OIDC_<KEY>_MOBILE_CLIENT_ID to the client id they use; without it the apps
show no SSO button.
Signing key RS256, scope mappings openid, profile, email — and
offline_access for the mobile apps. Since Authentik 2024.2 that scope has
to be mapped explicitly; without it no refresh token is issued and the apps
appear to log out every week. Restrict access with an Authentik group binding —
that, not ALLOWED_DOMAINS, is the real authorization control.
In .env:
PUBLIC_BASE_URL=https://calendar.example.com
OIDC_PROVIDERS=authentik
OIDC_AUTHENTIK_NAME=Authentik
OIDC_AUTHENTIK_ISSUER=https://authentik.example.com/application/o/calendarr/
OIDC_AUTHENTIK_CLIENT_ID=...
OIDC_AUTHENTIK_MOBILE_CLIENT_ID=calendarr-mobile
OIDC_AUTHENTIK_SCOPES="openid profile email"
OIDC_AUTHENTIK_ALLOW_SIGNUP=false
install.shdoes not overwrite an existing.env. On an existing installation append these lines by hand, thensystemctl restart calendarr.
Quote every value containing a space.
start.shdoesset -a; source .env; set +a, so an unquotedOIDC_AUTHENTIK_SCOPES=openid profile emailmakes the shell try to runprofileand startup fails.
Account matching. An identity is matched on the provider's sub claim.
A first SSO login with no matching account does not fail. The verified identity is parked in a short-lived signed cookie and the login screen asks the user to sign in once with their Calendarr password; the identity is then attached to exactly that account. Both halves are proven — the ID token by us, the account by the password — so no trust in the provider'''s email handling is required.
LINK_BY_EMAIL=true links automatically to an existing account with the same
address instead (additionally requiring email_verified). It saves one step,
but means anyone who can set an arbitrary email at the identity provider
inherits the matching account, including an admin'''s. Prefer the default.
With ALLOW_SIGNUP=true, unknown users get an account on first login. Such
accounts have no usable password; they use app passwords for CalDAV, the
same as 2FA users.
Adding a second provider is configuration only: append its key to
OIDC_PROVIDERS, add the matching OIDC_<KEY>_* block, restart. No code change.
Rollback: remove OIDC_PROVIDERS and restart. Accounts created via SSO
remain and must be removed (or given a password) by an admin.
Google Calendar Setup
- Create a project in Google Cloud Console
- Enable the Google Calendar API
- Create an OAuth 2.0 Client ID (Web application type)
- Add your redirect URI to the authorized redirect URIs
- Set
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET, andGOOGLE_REDIRECT_URIin.env
Project Structure
calendarr/
├── backend/
│ ├── main.py # App entry point, routing, DB migrations
│ ├── models.py # SQLAlchemy ORM models
│ ├── auth.py # JWT, password hashing, user auth
│ ├── caldav_client.py # CalDAV protocol client
│ └── routers/
│ ├── auth_router.py # Login, setup, 2FA
│ ├── users_router.py # Admin user management
│ ├── profile_router.py # Profile, avatar, password, 2FA settings
│ ├── settings_router.py # Per-user preferences
│ ├── local_router.py # Local calendar CRUD
│ ├── caldav_router.py # CalDAV accounts & calendars
│ ├── google_router.py # Google OAuth & calendar sync
│ └── ical_router.py # iCal subscriptions
├── frontend/
│ ├── index.html # Single-page app shell
│ ├── js/
│ │ ├── app.js # Bootstrap, login/setup routing
│ │ ├── calendar.js # Main app logic and UI
│ │ ├── api.js # HTTP client
│ │ ├── utils.js # Date helpers, theme application
│ │ ├── color-picker.js # Gradient color picker component
│ │ └── views/
│ │ ├── month.js # Month view
│ │ ├── week.js # Week/day view
│ │ └── agenda.js # Agenda/list view
│ └── css/
│ └── app.css # All styles (dark theme, CSS variables)
├── install.sh # Installation script
├── start.sh # Start script
├── calendarr.service # systemd unit file
└── requirements.txt # Python dependencies
License
© 2026 Scarriffleservices · All rights reserved