# Web App Launcher A self-hosted dashboard for organizing, monitoring, and launching web applications. Built with SvelteKit, Prisma (SQLite), and Tailwind CSS. ## Features - **App registry** — add apps with icons, tags, and categories; automatic healthcheck monitoring with sparkline history - **Boards & widgets** — customizable dashboards with drag-and-drop, resizable widget columns, and inline WYSIWYG editing - **Service integrations** — connect to media services, Planka, and more to display live data in widgets - **Authentication** — local accounts + OAuth/Authentik; per-board access control; API tokens - **Localization** — English and Russian - **PWA** — installable, multi-tab sync, auto-discovery bookmarklet - **SQLite backup/restore** — full database backup from the admin panel ## Quick Start ```bash git clone https://git.dolgolyov-family.by/alexei.dolgolyov/web-app-launcher.git cd web-app-launcher # Generate two strong secrets export JWT_SECRET=$(openssl rand -hex 32) export INTEGRATION_ENCRYPTION_KEY=$(openssl rand -hex 32) docker compose up -d ``` The app is available at `http://localhost:3000`. On first launch, create an admin account at the setup page. The launcher **refuses to start** if `JWT_SECRET` or `INTEGRATION_ENCRYPTION_KEY` is missing, shorter than 32 characters, or set to a known placeholder. This is intentional — running with the old `change-me-…` defaults would let anyone mint admin tokens. ## Configuration Environment variables (set in `docker-compose.yml` or `.env`): | Variable | Default | Description | |----------|---------|-------------| | `APP_PORT` | `3000` | Port to expose | | `JWT_SECRET` | **required** | Strong secret for JWT signing. Generate with `openssl rand -hex 32`. | | `INTEGRATION_ENCRYPTION_KEY` | **required** | Strong secret for encrypting stored integration credentials. Must differ from `JWT_SECRET`. Generate with `openssl rand -hex 32`. | | `ORIGIN` | `http://localhost:$APP_PORT` | Public URL users visit. When set to `https://...`, session cookies are issued with the Secure flag. **Set this to your public https URL when behind a reverse proxy.** | | `GUEST_MODE` | `true` | Allow unauthenticated access to guest-flagged boards | | `HEALTHCHECK_CRON` | `*/5 * * * *` | App healthcheck interval | | `HEALTHCHECK_TIMEOUT_MS` | `5000` | Healthcheck request timeout | | `ALLOW_PRIVATE_NETWORK_FETCH` | `false` (`true` in dev) | Allow outbound fetches to RFC1918/loopback/link-local. Self-hosted users monitoring LAN services usually want `true`. Off by default in prod to mitigate SSRF. | | `RUN_SCHEDULERS` | `true` | Run background jobs (healthcheck, backup) in this process. Set `false` on extra horizontal replicas. | | `OAUTH_CLIENT_ID` | — | OAuth provider client ID | | `OAUTH_CLIENT_SECRET` | — | OAuth provider client secret | | `OAUTH_DISCOVERY_URL` | — | OpenID Connect discovery URL | | `METRICS_TOKEN` | — | Optional bearer token for `/api/metrics`. Unset = open (private-network setups) | ## Production deployment ### Reverse proxy (Traefik / Caddy / Nginx) The launcher must know its public URL to issue secure cookies. Set `ORIGIN=https://launcher.example.com` and terminate TLS at the proxy. Example Traefik labels: ```yaml services: web-app-launcher: # remove `ports:` mapping networks: [traefik, launcher-net] labels: - traefik.enable=true - traefik.http.routers.launcher.rule=Host(`launcher.example.com`) - traefik.http.routers.launcher.entrypoints=websecure - traefik.http.routers.launcher.tls.certresolver=letsencrypt - traefik.http.services.launcher.loadbalancer.server.port=3000 environment: - ORIGIN=https://launcher.example.com ``` ### Volume backup ```bash docker run --rm \ -v web-app-launcher_launcher-data:/data \ -v "$PWD":/backup \ alpine tar czf /backup/launcher-backup.tar.gz -C /data . ``` ### Upgrade ```bash docker compose pull && docker compose up -d ``` Database migrations run automatically on container start via `prisma migrate deploy`. The previous `db push` fallback was removed because it can silently drop columns on schema drift. ### Breaking changes when upgrading from versions ≤ 0.0.x The 0.1.0 hardening release is a one-way upgrade with three breaking changes: 1. **`INTEGRATION_ENCRYPTION_KEY` is required and must differ from `JWT_SECRET`.** The launcher will refuse to start without it. Previously the integration key was derived from `JWT_SECRET`; all stored integration credentials (Planka, Authentik, Pi-hole, Portainer, Gitea, Immich, etc.) **will be undecryptable after the upgrade** and must be re-entered through the admin UI. 2. **All users will be logged out and all API tokens / invites will be revoked.** The hardening migration drops the `Session`, `Invite`, and `ApiToken` tables to switch from bcrypt-hashed storage to sha256 (so token validation is O(1) instead of O(N) bcrypt comparisons). Users will need to log in once; admins need to reissue API tokens and pending invites. 3. **Uploaded icons / wallpapers move from `static/uploads/` to `/app/data/uploads/`.** This makes them persist across container rebuilds. On upgrade, copy any existing files from your previous `static/uploads/` mount into the `launcher-data` volume: ```bash # if you previously mounted `./static/uploads:/app/static/uploads` docker run --rm \ -v "$PWD/static/uploads:/src:ro" \ -v web-app-launcher_launcher-data:/dst \ alpine sh -c "mkdir -p /dst/uploads && cp -r /src/. /dst/uploads/" ``` Take a backup before upgrading. ## Development ```bash npm install npx prisma generate # strong dev secrets are already in .env (gitignored) npm run dev ``` ## License MIT