These keys were referenced in SettingsForm but absent from both locales, so they rendered as raw keys instead of the intended text.
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
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:
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
docker run --rm \
-v web-app-launcher_launcher-data:/data \
-v "$PWD":/backup \
alpine tar czf /backup/launcher-backup.tar.gz -C /data .
Upgrade
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:
-
INTEGRATION_ENCRYPTION_KEYis required and must differ fromJWT_SECRET. The launcher will refuse to start without it. Previously the integration key was derived fromJWT_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. -
All users will be logged out and all API tokens / invites will be revoked. The hardening migration drops the
Session,Invite, andApiTokentables 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. -
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 previousstatic/uploads/mount into thelauncher-datavolume:# 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
npm install
npx prisma generate
# strong dev secrets are already in .env (gitignored)
npm run dev
License
MIT