Get started · Features · First steps · Update & back up · Configure · Releases
Private user accounts · Custom calendar feeds · Encrypted credentials
Your accounts, feeds and last sync at a glance. Screenshots use example data.
Important
Unofficial and independent of Untis. The bridge uses school-dependent internal WebUntis interfaces, not the registered Platform API.
Note
Tested without problems with student accounts at three different schools. WebUntis instances differ in features and permissions, so another school may behave differently. If yours does, please open an issue.
|
Lessons, cancellations, substitutions, exams and holidays are synchronized in the background and stored on your server. |
Subscribe in Apple Calendar, Google Calendar, Outlook or another ICS-compatible calendar. Create as many feeds as you need. |
|
Combine several students in one feed and choose the event types for each account separately. |
An exam gets its own calendar event. The lesson stays visible, even in feeds that hide exams. |
|
Write your own titles, notes and locations with variables and optional blocks. A live preview uses your synchronized events. |
Separate user accounts with admin-managed roles, optional two-factor authentication, encrypted WebUntis passwords and revocable feed links. No cloud, no telemetry. |
The interface is available in English and German. Original school texts stay unchanged.
Requires Docker with Compose v2, curl and OpenSSL. Docker images are available for amd64 and arm64; no Node.js installation is needed on the host.
Run the installer in a new directory:
mkdir -p webuntis-ics-bridge
cd webuntis-ics-bridge
curl -fSL --proto '=https' --proto-redir '=https' \
https://raw.githubusercontent.com/14flavius/webuntis-ics-bridge/main/scripts/install.sh \
-o install.sh &&
sh install.shFollow the prompts for the instance name, browser URL, port and listen address.
Use the exact URL you will open in your browser, including https:// behind a
reverse proxy. The installer creates the configuration and starts the bridge.
For an existing installation, follow Updates and backups.
Open the URL you configured during installation and choose a username and a password of at least 12 characters. The first account becomes the administrator.
Important
Setup is open for two hours after the first start. If it expires, run
docker compose restart bridge to open a new window.
Create more accounts and manage roles in Administration. There is no public registration; administrators cannot view another user's WebUntis accounts, events or calendar links. Personal settings, including two-factor authentication, are under Settings.
- Add a WebUntis account. On Overview, choose Add account, search for your school and enter the student login. Test connection checks the login before anything is saved.
- Create a feed. Under Calendar feeds, choose the accounts and what each one contributes: lessons, cancellations, substitutions, exams and holidays.
- Subscribe. Copy the ICS link and add it to your calendar app as an internet calendar subscription.
Important
Anyone with the ICS link can read that calendar. Use Rotate link to revoke an old link.
Back up and update from your installation directory:
sh scripts/backup.sh && docker compose pull && docker compose up -d --waitKeep your existing .env and data/ directory. Accounts, feeds and events are
retained; database migrations run automatically on startup. If you pin an image
version, update BRIDGE_IMAGE first. New images are available once the
release build finishes.
To create a backup without updating:
sh scripts/backup.shThe backup asks for a passphrase, takes a consistent snapshot while the bridge
keeps running and creates a folder in backups/. It holds the encrypted database
with both secrets, docker-compose.yml.backup and the image ID, and is marked
COMPLETE when finished. Keep a protected copy outside the server; the passphrase cannot
be recovered. The database lives in ./data; preserve this directory when replacing
containers.
Restore a backup
Only use a backup folder marked COMPLETE. Decrypting it needs a
development checkout with Node.js 24. Restore into a new,
nonexistent directory:
node scripts/backup.mjs restore <backup-file> <new-directory>This writes <new-directory>/.env and <new-directory>/data/bridge.sqlite. Copy
BETTER_AUTH_SECRET and ENCRYPTION_KEY from the restored .env into the .env
of your installation, restore docker-compose.yml.backup as
docker-compose.yml, stop the bridge and place the restored database at
data/bridge.sqlite, owned by UID/GID 1000. Verify the restored
installation before deleting anything.
Edit .env in your installation directory, then apply changes:
docker compose up -d --pull never --wait| Setting | Purpose |
|---|---|
PUBLIC_URL |
Full browser URL, such as https://calendar.example.com. Subpaths are not supported. |
HOST_PORT |
Published HTTP port on the host, usually 3000. |
BIND_ADDRESS |
127.0.0.1 for local access or a proxy on the same host; 0.0.0.0 for LAN access. |
BRIDGE_IMAGE |
Optional fixed image, e.g. ghcr.io/14flavius/webuntis-ics-bridge:1.0.0. Defaults to latest. |
Keep the generated BETTER_AUTH_SECRET, ENCRYPTION_KEY and COMPOSE_PROJECT_NAME
unchanged. For HTTPS, terminate TLS at your reverse proxy and forward to the
bridge's HTTP port. Plain HTTP is only accepted for localhost and 127.0.0.1;
every other address needs HTTPS. A proxy in another container needs network access
to the bridge; its localhost is separate from the host's.
Reverse proxy examples (nginx, Apache)
Both examples forward https://calendar.example.com to a bridge listening on
127.0.0.1:3000 and keep feed links out of the access log. Replace the domain,
port and certificate paths with your own, and set
PUBLIC_URL=https://calendar.example.com in .env. They are starting points and
have not been tested against every setup.
nginx
server {
listen 80;
server_name calendar.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name calendar.example.com;
ssl_certificate /etc/letsencrypt/live/calendar.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/calendar.example.com/privkey.pem;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
location / {
proxy_pass http://127.0.0.1:3000;
}
# Feed links are secrets: do not write them to the access log.
location /feed/ {
access_log off;
proxy_pass http://127.0.0.1:3000;
}
}Apache (needs mod_ssl, mod_proxy, mod_proxy_http, mod_headers and mod_setenvif)
<VirtualHost *:80>
ServerName calendar.example.com
Redirect permanent / https://calendar.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName calendar.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/calendar.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/calendar.example.com/privkey.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "https"
# Feed links are secrets: do not write them to the access log.
SetEnvIf Request_URI "^/feed/" no_log
CustomLog ${APACHE_LOG_DIR}/access.log combined env=!no_log
</VirtualHost>The sync interval is under Settings and can be changed by administrators.
See docker-compose.yml for all deployment options.
Questions or problems? Open an issue.
For startup problems, check docker compose logs --tail=100 and
docker compose ps. Remove secrets, feed links and personal information before
sharing logs. Found a security problem? Report it privately as described in
SECURITY.md.
Event templates
Each feed has a title, notes and location template for every event type:
Title: {{subjectShort | teacherShort}} [[({{classShort}})]]
Notes: [[Subject: {{subjectLong}}]] [[Note: {{info}}]]
Location: {{roomShort}}
{{a | b}} joins the values that exist, for example MA · TA or just MA. A
[[...]] block disappears when a value inside it is missing. Templates are plain
text; nothing is executed.
Variables: subject, teacher, room, class, info, status,
substituteTeacher, originalTeacher, examType, examName, examText,
holidayName, account, date, start, end. The first four and both teacher
variants are also available with a Short, Long or Display suffix, for
example {{teacherShort}}.
Exam source and cancelled lessons
Choose the exam source for each account:
| Option | Behavior |
|---|---|
| Prefer Exams tab | Uses the WebUntis Exams tab and falls back to timetable exams if it is unavailable. |
| Timetable | Uses only timetable entries that are marked as exams. |
Choose how each feed handles cancelled lessons:
| Option | Behavior |
|---|---|
| Display as cancelled | Keeps the event with a cancellation title. Recommended. |
| Hide | Leaves cancelled lessons out. |
| Send CANCELLED | Sends STATUS:CANCELLED. Some calendar apps then remove the event. |
Multiple instances
Use a separate directory, instance name and host port for each instance. Use separate hostnames for independent browser logins; cookies are shared across ports on the same hostname.
Security details
- Passwords are hashed by Better Auth. Sessions use HTTP-only cookies, origin checks and rate limiting.
- WebUntis passwords are encrypted with AES-256-GCM under a separate key and are never returned by the API.
- The bridge only connects to HTTPS hosts under
*.webuntis.com. Private addresses and redirects are rejected. - Feed links are 256-bit secret tokens. Reading a feed never contacts WebUntis. Avoid logging
/feed/URLs at your proxy. - The container runs without root, with a read-only filesystem and no extra capabilities.
Known limitations
- Student username and password accounts on
*.webuntis.comonly. No SSO, parent or teacher accounts. - WebUntis interfaces differ per school and can change without notice. Unexpected responses are rejected and existing data is kept.
- An empty exam or holiday response cannot prove a deletion, so a removed event can stay until it leaves the feed window.
- Users cannot share accounts or feeds with each other. Share the ICS link instead.
- No homework, external holiday calendars, subject filters or manual events.
Run locally and check your changes
Use Node.js 24. The app uses TypeScript, Fastify, React, Vite, Tailwind, Better Auth and SQLite.
git clone https://github.com/14flavius/webuntis-ics-bridge.git
cd webuntis-ics-bridge
bash scripts/setup.sh
npm ci --ignore-scripts
npm run devOpen http://127.0.0.1:5173. Checks: npm run typecheck, npm run lint,
npm run format:check, npm test, npm run build. Browser flows run with
npm run test:browser and need a local Chrome. npm run screenshots regenerates
the README screenshots from example data.
To try your own image, build it and point the Compose file at it:
docker build -t bridge:local ., then set BRIDGE_IMAGE=bridge:local in .env
and start with docker compose up -d --pull never --wait.
Install WebUntis ICS Bridge · Get help · MIT License
By Flavius Hager
Not affiliated with or endorsed by Untis.
Most of the original WebUntis ICS Bridge foundation was vibe-coded with AI assistance.