Calendar app
We document the installation of the self-hosted calendar app running at calendar.piribauer.ch. The source is available at git.piribauer.ch/calendar.
The app is built with FastAPI and stores its data in a SQLite
database. Authentication is handled entirely by an Authentik outpost
via nginx forward-auth; the app reads the
X-Authentik-Username and X-Authentik-Email
headers injected by nginx and never touches the Authentik API
directly.
We assume that nginx and an Authentik outpost are already
running, and that the repo has been cloned to
/home/julian/calendar.
Python environment
The system Python on Arch Linux is PEP 668-managed and rejects
direct pip install calls. We therefore use a virtual
environment:
$ python -m venv /home/julian/calendar/venv
$ /home/julian/calendar/venv/bin/pip install -r /home/julian/calendar/requirements.txt
Database directory
The app stores its data in a SQLite database under
/var/lib/calendar/. We create the directory and assign
ownership to the service user:
# mkdir -p /var/lib/calendar
# chown julian:julian /var/lib/calendar
Environment file
The app reads its configuration from
/home/julian/calendar/.env. We create it with the
database URL:
DATABASE_URL=sqlite:////var/lib/calendar/calendar.db
Systemd service
We install the unit file included in the repo:
# cp /home/julian/calendar/calendar.service /etc/systemd/system/calendar.service
# systemctl daemon-reload
# systemctl enable --now calendar
The service runs as user julian, listening on
127.0.0.1:8765. We can verify it started cleanly
with
$ systemctl status calendar
Nginx
We copy the nginx config from the repo and enable it:
# cp /home/julian/calendar/nginx/calendar /etc/nginx/sites-available/calendar
# ln -s /etc/nginx/sites-available/calendar /etc/nginx/sites-enabled/calendar
# nginx -t && systemctl reload nginx
The config forwards all requests through the Authentik outpost
running on 127.0.0.1:9000. On success, nginx extracts
the username and email from the outpost response and passes them to
the app:
auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_email $upstream_http_x_authentik_email;
proxy_set_header X-Authentik-Username $authentik_username;
proxy_set_header X-Authentik-Email $authentik_email;
Unauthenticated requests are redirected to the Authentik login page.
Schema migrations
SQLAlchemy creates missing tables on startup with
create_all, but does not modify tables that already
exist. If a column is added to the models, it must be applied to the
existing database manually. For example, adding the
description column to the events
table:
$ sqlite3 /var/lib/calendar/calendar.db "ALTER TABLE events ADD COLUMN description TEXT;"
Invitation flow
Users are invited by email address from within the app. The invitation is stored as a pending record linked to the calendar. When the invited user logs in for the first time, their Authentik email is matched against all pending invitations and the corresponding calendar memberships are created automatically.