Skip to content

About

Self-hosted WebUntis to ICS calendar sync with customizable feeds, multiple accounts and a modern dashboard.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

WebUntis ICS Bridge - a self-hosted WebUntis to ICS calendar sync

Get started  ·  Features  ·  First steps  ·  Update & back up  ·  Configure  ·  Releases

Private user accounts  ·  Custom calendar feeds  ·  Encrypted credentials

WebUntis ICS Bridge overview showing synced accounts, active feeds and stored events

Your accounts, feeds and last sync at a glance. Screenshots use example data.

A closer look at calendar feeds

Calendar feeds with a combined timetable for two schools and an exams-only feed

A closer look at the feed editor

Feed editor with content selection per account and event templates

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.

Features

Timetable sync

Lessons, cancellations, substitutions, exams and holidays are synchronized in the background and stored on your server.

Calendar feeds

Subscribe in Apple Calendar, Google Calendar, Outlook or another ICS-compatible calendar. Create as many feeds as you need.

Multiple accounts

Combine several students in one feed and choose the event types for each account separately.

Exams as extra entries

An exam gets its own calendar event. The lesson stays visible, even in feeds that hide exams.

Event templates

Write your own titles, notes and locations with variables and optional blocks. A live preview uses your synchronized events.

Private by design

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.

Installation

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.

1. Install

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.sh

Follow 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.

2. Create your administrator

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.

First steps

  1. 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.
  2. Create a feed. Under Calendar feeds, choose the accounts and what each one contributes: lessons, cancellations, substitutions, exams and holidays.
  3. 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.

Updates and backups

Update

Back up and update from your installation directory:

sh scripts/backup.sh && docker compose pull && docker compose up -d --wait

Keep 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.

Back up

To create a backup without updating:

sh scripts/backup.sh

The 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.

Configuration

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.

Help

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.

More

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.com only. 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.

Development

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 dev

Open 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.

About

Self-hosted WebUntis to ICS calendar sync with customizable feeds, multiple accounts and a modern dashboard.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages