Self-hosted, privacy-friendly video calls. Your team creates and schedules meetings from a small dashboard; guests join by link with no account. Audio and video flow peer-to-peer (WebRTC mesh) — your server only relays the connection setup and never sees the media.
Built with Nuxt 4. Self-contained: MySQL for auth + meetings, optional SMTP for invites. No third-party media servers, no tracking.
Reference deployment: localcalls.de
- Peer-to-peer video calls (mesh) — great for 2–4 participants; the server is signaling-only, so media never touches it.
- Screen sharing with a presentation stage + camera strip.
- Background blur and an adaptive low-light boost — on-device (MediaPipe), self-hosted model files, nothing leaves the browser.
- Automatic reconnection with backoff and ghost-peer eviction.
- Team auth — email/password login, sessions, roles (user/admin).
- Meeting management — create a titled room, schedule date/time + duration, optionally repeat (daily/weekly/monthly/yearly, every N, ending on a date or after N times), invite recipients by email.
- Email invites with an inline calendar entry (native "Add to calendar",
no
.icsattachment). The organizer gets a copy. - Admin user management — create/edit/delete team members, optional welcome email with credentials.
- Profiles — display name, password change, drag-and-drop avatar upload.
- 7 languages — English, German, French, Spanish, Italian, Dutch, Polish.
Set instance-wide via
NUXT_LANGUAGE; also localizes the emails. - Light/dark that follows the OS, brand-themeable.
- Guests open a meeting link and join immediately — no account required.
- Team members log in to a dashboard to create and manage meetings.
- Media path: browsers connect directly to each other (
RTCPeerConnection). The Nuxt server is a WebSocket signaling relay only (offers/answers/ICE). - Storage: a single MySQL database holds users, sessions and meetings.
- Scale note: a full mesh sends your stream to every other participant, so it's ideal up to ~4 people. Beyond that you'd want an SFU (out of scope here).
- Node.js 20+ (developed on 22)
- MySQL 8+ (a local socket install or any managed instance)
- Optional: an SMTP account for meeting invites / welcome emails
- Optional: a STUN/TURN server for calls across the open internet
# 1. Install
npm install
# 2. Configure
cp .env.example .env
# then edit .env — at minimum set your MySQL connection and the first admin email
# 3. Run
npm run devOn first start the app creates the database if missing, runs migrations, and
bootstraps the first admin from NUXT_INITIAL_ADMIN_EMAIL and
NUXT_INITIAL_ADMIN_PASSWORD. Both are required — if either is missing, no admin
is created and a warning is logged. Passwords are never written to the log.
Then open http://localhost:3000, log in at /login, and create your first
meeting. Share the link; guests join without signing in.
A typical local install exposes a unix socket. Point .env at it:
NUXT_MYSQL_SOCKET=/tmp/mysql.sock
NUXT_MYSQL_USER=root
NUXT_MYSQL_PASSWORD=your-local-root-password
NUXT_MYSQL_DATABASE=localcalls
NUXT_INITIAL_ADMIN_EMAIL=you@example.comThe localcalls database is created automatically — you don't need to create it
by hand.
All configuration is via environment variables (see .env.example).
| Variable | Default | Description |
|---|---|---|
NUXT_APP_NAME |
LocalCalls |
Brand name shown in the UI, page titles and emails. |
NUXT_APP_URL |
(request origin) | Public base URL used to build links in emails, e.g. https://localcalls.de. |
NUXT_LANGUAGE |
en |
Instance-wide UI language and the language of outgoing emails. One of en, de, fr, es, it, nl, pl. |
| Variable | Default | Description |
|---|---|---|
NUXT_MYSQL_SOCKET |
(empty) | Unix socket path. Takes precedence over host/port when set. |
NUXT_MYSQL_HOST |
127.0.0.1 |
Host (when not using a socket). |
NUXT_MYSQL_PORT |
3306 |
Port. |
NUXT_MYSQL_USER |
root |
User. |
NUXT_MYSQL_PASSWORD |
(empty) | Password. |
NUXT_MYSQL_DATABASE |
localcalls |
Database name (auto-created if missing). |
NUXT_MYSQL_SSL |
false |
Set true for managed DBs requiring TLS. |
NUXT_MYSQL_SSL_REJECT_UNAUTHORIZED |
true |
Set false only for an unverifiable cert. |
| Variable | Default | Description |
|---|---|---|
NUXT_SESSION_MAX_AGE_DAYS |
7 |
Session cookie lifetime. |
NUXT_INITIAL_ADMIN_EMAIL |
(empty) | Bootstraps the first admin on first run. Required to create an admin. |
NUXT_INITIAL_ADMIN_NAME |
Admin |
Display name for the bootstrapped admin. |
NUXT_INITIAL_ADMIN_PASSWORD |
(empty) | Required to create the admin. Never logged. |
| Variable | Default | Description |
|---|---|---|
NUXT_EMAIL_HOST |
(empty) | SMTP host. Without it, meetings still work but no mail is sent. |
NUXT_EMAIL_PORT |
465 |
SMTP port. |
NUXT_EMAIL_SECURE |
true |
true for port 465, false for STARTTLS ports. |
NUXT_EMAIL_USER |
(empty) | SMTP user / from-address. |
NUXT_EMAIL_PASS |
(empty) | SMTP password. |
| Variable | Default | Description |
|---|---|---|
NUXT_PUBLIC_ICE_SERVERS |
German public STUN | JSON array of RTCIceServer entries. Add a TURN entry for strict NATs. |
Example:
NUXT_PUBLIC_ICE_SERVERS='[{"urls":"stun:turn.example.com:3478"},{"urls":"turn:turn.example.com:3478","username":"user","credential":"pass"}]'Out of the box the UI is a neutral slate theme with a blue accent, in light and dark (following the OS). To match your brand, set the accent colour — it re-themes the whole app (buttons, links, nav, avatars, badges, spinners) and the invite/welcome emails:
| Variable | Default | Description |
|---|---|---|
NUXT_PUBLIC_COLOR_PRIMARY |
#2563eb |
Accent colour (light mode). |
NUXT_PUBLIC_COLOR_PRIMARY_DARK |
#3b82f6 |
Accent colour used in dark mode (usually a touch lighter). |
NUXT_PUBLIC_COLOR_PRIMARY="#7c3aed"
NUXT_PUBLIC_COLOR_PRIMARY_DARK="#a78bfa"The accent is injected as the --brand-accent CSS variable at runtime (see
app.vue); the neutral surface/text colours live in app/assets/css/main.css
as --call-* variables if you want to go further. Use NUXT_APP_NAME to change
the brand name shown in the UI, page titles and emails.
The UI ships in English, German, French, Spanish, Italian, Dutch and Polish.
Language is an instance-wide setting, not a per-user choice: NUXT_LANGUAGE
picks the one the whole frontend is delivered in and the language of
outgoing emails (invites, welcome mails). Every locale is bundled into the
build, so switching is just an env change and a restart — no rebuild.
Translations live in i18n/locales/<code>.json — copy en.json to add or
adjust a language, then register it in the i18n.locales array in
nuxt.config.ts.
Build and run as a Node server (the app is stateful — it holds live WebSocket rooms in memory, so a static/edge export won't work):
npm run build
node .output/server/index.mjsProvide the same environment variables to the running process (systemd
Environment=, a Docker environment: block, PM2 env, etc.). Nuxt does not
read .env in production.
Signaling runs at /ws/:roomId. Your proxy must forward the WebSocket
upgrade or calls won't connect.
-
Caddy does this automatically.
-
nginx needs:
location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
Browsers only grant camera/microphone/screen access on a secure origin
(localhost or HTTPS). Serve the deployed app over TLS.
On the same LAN, no ICE servers are needed. Across different networks you need
STUN (address discovery, cheap) and, for the ~10–20% behind strict/symmetric
NAT, TURN (a media relay). Self-host coturn
(one daemon does both) or use a managed provider, then set
NUXT_PUBLIC_ICE_SERVERS. STUN never sees media; TURN only relays encrypted media.
Profile pictures are resized to a small square and stored as base64 on the user row (~15–40 KB each) — no external file storage needed. Fine at team scale; for hundreds of users, moving avatars to object storage would be the upgrade path.
- Media is peer-to-peer and encrypted (DTLS-SRTP). It never passes through or is stored on your server.
- Background blur / low-light run entirely in the browser with self-hosted model files — no third-party requests.
- STUN servers only learn a participant's public IP (needed for connectivity), never call content. Prefer EU-hosted or self-hosted STUN/TURN if that matters to you.
- Nuxt 4 + Nuxt UI + Tailwind CSS
- WebRTC (
RTCPeerConnection) mesh, Nitro WebSocket signaling - MySQL via
mysql2, bcrypt password hashing - nodemailer for SMTP
- MediaPipe Tasks Vision for on-device
segmentation (self-hosted assets under
public/mediapipe/)
MIT — see LICENSE.
