 | tech_notesA collection of notes on setups and implementations.
|
|
Log | Files | Refs | README |
commit 06ecc88efcb24f743d2cb45cdd7c38a4a405f24a
parent de66c3d5a7947dae6a5643a69c36de1c75506093
Author: Julian Piribauer <julian.piribauer@gmail.com>
Date: Sat, 2 May 2026 06:50:35 +0000
Add calendar app installation guide
Diffstat:
| A | calendar.md | | | 94 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 94 insertions(+), 0 deletions(-)
diff --git a/calendar.md b/calendar.md
@@ -0,0 +1,94 @@
+# Calendar app
+
+We document the installation of the self-hosted calendar app running at [calendar.piribauer.ch](https://calendar.piribauer.ch).
+The source is available at [git.piribauer.ch/calendar](https://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:
+
+```console
+$ 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:
+
+```console
+# 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:
+
+```bash
+DATABASE_URL=sqlite:////var/lib/calendar/calendar.db
+```
+
+## Systemd service
+
+We install the unit file included in the repo:
+
+```console
+# 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
+
+```console
+$ systemctl status calendar
+```
+
+## Nginx
+
+We copy the nginx config from the repo and enable it:
+
+```console
+# 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:
+
+```nginx
+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:
+
+```console
+$ 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.