nodemeet
Embed live video meetings and scheduling in your own Python app. Self-hosted and free for personal, educational and nonprofit use. No paid services, no vendor accounts.
- Meetings: an aiohttp server you mount into your app or run on its own. Signed HMAC join tokens with roles (host / participant / viewer). No participant cap. Topology
autoruns peer-to-peer up to 4 people, then switches to an aiortc SFU. There's also a webinar mode. - Hooks:
before_join,on_join,on_leave,on_chat(moderate or rewrite messages), plus room, booking and reminder events. - Scheduling: weekly availability per host with time zones (DST-safe), buffers, minimum notice, booking horizon, blackout dates and per-date overrides. Also slot finding, book / cancel / reschedule,
.icsinvites and reminders. - Integrations: SMTP email (stdlib), HMAC-signed webhooks, SQLite and in-memory storage, a pluggable
Storageinterface for your database, a REST API, and Google/Outlook "add to calendar" links. - Frontend:
nodemeet.js(vanilla client), a ready-made meeting UI (grid, mute, camera, screen share, chat, raise hand, host moderation), a booking widget, and<nodemeet-room>/<nodemeet-booking>embed tags. - CLI:
nodemeet init,nodemeet serve,nodemeet link,nodemeet doctor,nodemeet cheatsheet,nodemeet token.
Install
pip install "nodemeet @ git+https://github.com/dhyanamgit/nodemeet" # core + aiohttp server
pip install "nodemeet[sfu] @ git+https://github.com/dhyanamgit/nodemeet" # + aiortc SFU for rooms larger than 4
pip install "nodemeet[redis] @ git+https://github.com/dhyanamgit/nodemeet" # + several servers behind a load balancer
pip install "nodemeet[postgres] @ git+https://github.com/dhyanamgit/nodemeet" # + SQLAlchemy storage (also [mysql], [sql])
pip install "nodemeet[asgi] @ git+https://github.com/dhyanamgit/nodemeet" # + uvicorn, to serve the ASGI app
pip install "nodemeet[all] @ git+https://github.com/dhyanamgit/nodemeet" # everything
Or download the wheel from the latest release and pip install it.
Quickstart: 3 lines
from nodemeet import NodeMeet
meet = NodeMeet() # every setting has a default
print(meet.room("standup").host_link("Ada")) # open this link, share room.guest_link() with others
meet.run()
Or, without writing any code: nodemeet init myapp && cd myapp && python app.py.
Camera and mic need https:// (or localhost). In production, put a TLS proxy such as Caddy or nginx in front.
The whole API on one page
Everything is meet.<something>(...) and takes plain values. The full list is in CHEATSHEET.md, or run nodemeet.help() / nodemeet cheatsheet.
meet = NodeMeet(db="postgres://u:p@host/app", email="smtp://u:p@smtp.host:587", secret="long-random")
room = meet.room("class-7", preset="classroom", join_list=["ravi@x.com"]) # presets: open, webinar, 1on1...
room.host_link("Ms Rao") room.guest_link("Ravi") meet.link("class-7", "Sam", "viewer")
page = meet.hours("rao", "mon-fri 9am-12pm, 2pm-5pm", timezone="Asia/Kolkata", minutes=30, buffer=10)
page.url # booking page · page.embed() for your HTML
meet.role("teacher", can="moderate, record", badge="T", rank=50) # permissions in plain words
meet.brand(name="Acme", color="#ff5a00", logo="https://acme.com/logo.png", font="Inter")
@meet.on("join")
def joined(room, person): print(person.name, "joined", room.id)
meet.add_payments("stripe") meet.add_calendars(google=True) meet.add_sso(github=True) meet.add_sms("twilio")
Integrations, one line each:
- Chat: Slack, Discord, Teams, Google Chat, Telegram, Mattermost.
- CRM: HubSpot, Salesforce, Pipedrive, Zoho.
- Payments: Stripe, Razorpay, PayPal.
- Push: Web Push, FCM, OneSignal, ntfy.
- Meetings handed off to: Zoom, Teams, Google Meet, Webex, Jitsi.
- Also: calendars, SSO, SMS/WhatsApp, webhooks, S3 recordings, local captions.
See the table in CHEATSHEET.md.
Webhooks in and out:
- 112 outgoing events with patterns, a delivery log, redelivery and a live feed.
- Inbound webhooks from Zoom, Webex, Microsoft, Google, Twilio, WhatsApp, Slack, Telegram, Discord, HubSpot, Pipedrive, Salesforce, Zoho and payment providers.
- An actions API for Zapier / n8n.
See WEBHOOKS.md.
Settings from a file or env vars. Use NodeMeet.from_config("nodemeet.toml") (written for you by nodemeet init) or NodeMeet.from_env() (reads NODEMEET_* vars and .env). In both, ${VAR} keeps secrets out of the file.
Databases are just URLs: meet.db, postgres://, mysql://, mssql://, oracle://, duckdb:///, snowflake://, clickhouse://, redis://, mongodb://, couchdb://, elasticsearch://, neo4j://, arangodb://, firestore://, dynamodb://, s3://, cassandra://, etcd://, consul://, lmdb:///, dir:/// and more. If a driver is missing, the error tells you the exact pip install line.
Typos get suggestions. For example: unknown room preset 'webnar'. Did you mean 'webinar'?. Run nodemeet doctor to check your setup.
Declarations are safe to keep in code. Rooms, roles and hours declared in code are applied on startup. If an admin edits one later through the API, that edit survives restarts, until you change the declaration in code.
Plug into your stack
| Your app | How |
|---|---|
| FastAPI / Starlette | app.mount("/meet", meet.asgi()) (see examples/fastapi_app.py) |
| Django | application = route({"/meet": meet.asgi()}, default=get_asgi_application()) (examples/django_asgi.py) |
| aiohttp | meet.mount(app, "/meet") |
| Quart, Litestar, any ASGI | mount meet.asgi(); run with uvicorn/hypercorn/daphne |
| Flask / anything else | run nodemeet serve next to it and mint tokens with nodemeet.TokenSigner |
from nodemeet import NodeMeet, JoinRejected
meet = NodeMeet(secret="long-random-secret", base_url="https://myapp.com/meet")
@meet.before_join
async def gate(ctx): # your auth/billing rules
if not ctx.claims.meta.get("paid"):
raise JoinRejected("Upgrade to join")
token = meet.create_token("project-7", "u42", "host", name="Ada", meta={"paid": True})
html = meet.embed_room("project-7", token) # drop into any template
Your users never log in to nodemeet: your backend authenticates them and mints a signed token.
LMS (Moodle, Canvas, Blackboard, Brightspace...) over LTI 1.3
lti = meet.add_lti(grade_attendance=True) # or moodle=(URL, CLIENT_ID, DEPLOYMENT_ID)
print(lti.registration_url()) # paste into the LMS once: done, permanently
Students arrive already signed in and teachers become hosts. Each course link is its own room. The roster syncs to the join list, and attendance goes to the gradebook. See LTI.md.
Scheduling
from nodemeet import Availability
await meet.bookings.set_availability(Availability.from_hours(
"dr-ada", "Asia/Kolkata", {"mon-fri": "09:00-13:00, 14:00-18:00", "sat": "10:00-12:00"},
duration_minutes=30, buffer_before=5, buffer_after=10, min_notice_minutes=120,
max_days_ahead=30, blackout_dates=["2026-12-25"], host_email="ada@example.com"))
slots = await meet.bookings.find_slots("dr-ada", date(2026, 10, 5), date(2026, 10, 9))
b = await meet.bookings.book("dr-ada", slots[0].start, attendee_name="Sam", attendee_email="sam@x.com")
await meet.bookings.reschedule(b.id, slots[3].start)
await meet.bookings.cancel(b.id, reason="sick")
BookingService also works without the HTTP server. Feed busy times from your own systems with busy_provider=async def f(host_id, start, end) -> [(start, end), ...]. Reminders run in the background (default 24h and 15 min before). In multi-process deploys, call meet.reminders.run_once() from your own scheduler instead.
Databases
| Storage | Use it for |
|---|---|
MemoryStorage |
tests, demos |
SQLiteStorage("file.db") |
single server; stdlib only; versioned schema; safe with several worker processes |
SQLAlchemyStorage(url or engine=...) |
Postgres, MySQL/MariaDB, your existing DB; share your app's engine; Alembic-friendly (nodemeet.storage.sql.metadata) |
SyncStorageAdapter(obj) |
wrap a synchronous implementation (Django ORM, sync SQLAlchemy) |
subclass Storage |
anything else (see examples/custom_storage.py) |
nodemeet db upgrade --db-url postgresql+asyncpg://... creates/upgrades tables ahead of deploys.
No double bookings, no duplicate reminders, with any number of workers/servers: bookings go through Storage.insert_booking_if_free() (SQLite BEGIN IMMEDIATE; SQL backends lock a per-host row), and reminders are claimed with mark_reminder_sent() (unique key) before sending. Custom backends should implement both atomically.
Scaling out (several servers)
meet = NodeMeet(secret, broker="redis://redis:6379/0", storage=SQLAlchemyStorage(pg_url))
or nodemeet serve --redis redis://... --db-url .... Redis (or Valkey/KeyDB/Dragonfly) syncs presence, chat, WebRTC signaling, moderation, topology and rate limits across servers, and cleans up after a crashed server. Peer-to-peer rooms work with plain round-robin. The SFU relays media inside one server, so for SFU/webinar rooms route by room: the JS client connects to /ws?room=<id>, so nginx hash $arg_room consistent; pins a room to one server. Full setup: examples/cluster/ (docker-compose + nginx).
Multi-tenant API keys
For SaaS platforms: give each of your customers (tenants) their own keys.
nodemeet keys create --tenant acme --scopes rooms:write,tokens:create --rate-limit 600 --db app.db
# or: POST /api/keys {"tenant_id":"acme","scopes":[...]} with the master key
- The key is shown once; only its SHA-256 hash is stored. Keys can be revoked, expire, and carry per-minute rate limits.
- Everything a tenant key creates (rooms, hosts, bookings, webhooks) is stamped with its tenant. Other tenants get 404s, and join tokens minted for a tenant only work in that tenant's rooms.
- Scopes:
rooms:read,rooms:write,tokens:create,availability:write,bookings:read,bookings:write,webhooks:manageor*. - Tenants register their own webhooks (
POST /api/webhooks) and receive only their own events, signed with their own secret. - The
api_keyyou giveNodeMeetis the master key: it sees everything and manages keys.
Email and webhooks
from nodemeet import SMTPMailer, WebhookDispatcher
meet = NodeMeet(secret, mailer=SMTPMailer("smtp.example.com", 587, username="u", password="p", sender="no-reply@example.com"),
webhooks=WebhookDispatcher())
meet.webhooks.add("https://myapp.com/hooks/nodemeet", secret="whsec", events=["booking.created", "participant.joined"])
Webhooks carry X-NodeMeet-Signature: t=<ts>,v1=<hmac>. Check them with nodemeet.verify_signature(secret, body, header). To rebrand emails, subclass EmailTemplates.
REST API
Authorization: Bearer <master key or tenant key>. Public endpoints power the booking widget; booking management also accepts the per-booking manage_token (?t=).
| Method | Path | Auth |
|---|---|---|
| GET | /api/health, /api/me |
public / any key |
| POST/GET | /api/rooms; GET/PATCH/DELETE /api/rooms/{id} |
rooms:write / rooms:read |
| POST | /api/tokens {room, user_id, role, name, ttl, meta} |
tokens:create |
| PUT / GET | /api/hosts/{host}/availability |
availability:write / public (redacted) |
| GET | /api/hosts/{host}/slots?start=YYYY-MM-DD&end=...&tz=... |
public |
| POST | /api/bookings {host_id, start, name, email, timezone, notes} |
public |
| GET | /api/bookings |
bookings:read |
| GET | /api/bookings/{id}, /invite.ics |
bookings:read or manage token |
| POST | /api/bookings/{id}/cancel, /reschedule |
bookings:write or manage token |
| POST/GET/DELETE | /api/keys, /api/keys/{id} |
master (tenants can list/revoke their own) |
| POST/GET/DELETE | /api/webhooks, /api/webhooks/{id} |
webhooks:manage |
Pages: /r/{room}#token=..., /book/{host}, /book/manage/{id}#t=.... WebSocket: /ws.
Frontend
<script src="https://myapp.com/meet/static/embed.js" async></script>
<nodemeet-room token="nm1...." height="600px"></nodemeet-room> <!-- iframe; add `inline` for no iframe -->
<nodemeet-booking host="dr-ada"></nodemeet-booking>
Or build your own UI with new NodeMeet.Client({base, token}): events are stream, stream-removed, chat, participants, topology, and more. Methods include join(), toggleAudio(), toggleVideo(), startScreenShare(), sendChat(), raiseHand(), mute(peer) and kick(peer). See examples/html/custom_client.html. To theme the UI, override the CSS variables in nodemeet.css.
Media topology notes
auto: mesh up top2p_max(4). Above that it switches to the SFU and drops back when the room shrinks to half.webinar: only hosts, or people a host "invites to stage", publish.- The aiortc SFU relays decoded frames, so it's CPU-bound. It's good for roughly 5 to 25 people per server. For huge rooms, implement
nodemeet.sfu.MediaBackendon top of an OSS SFU (mediasoup, Janus, LiveKit OSS) and passNodeMeet(sfu=MyBackend()). - The default STUN server is Google's free public one. Behind strict NATs, run coturn and pass
ice_servers=[{"urls": "turn:turn.example.com", "username": ..., "credential": ...}].
Development
pip install -e ".[dev]"
pytest # ASGI, cluster, tenancy and concurrency tests need nothing extra
python -m build # sdist + wheel in dist/
Licence
nodemeet is free for noncommercial use under the PolyForm Noncommercial License 1.0.0: personal
projects, study, research, hobby use, and any school, college, charity, public research body or government
office. Keep the Required Notice: lines from LICENSE in every copy.
Selling nodemeet, or using it in a product or service that makes money, needs a commercial licence from the author, Dhyanam Shah. Open an issue titled "Commercial licence" or write through the GitHub profile.
Copyright (c) 2026 Dhyanam Shah. "nodemeet" is the author's project name; forks must use a different name.
Contributing
Bug fixes, docs, tests and new integrations are welcome. See CONTRIBUTING.md and look for issues
labelled good first issue.
New in 0.3: roles, branding, settings, every database
from nodemeet import NodeMeet
from nodemeet.storage import DBAPIStorage, KeyValueStorage, DocumentStorage, MemoryKV
nm = NodeMeet(secret="change-me",
branding={"name": "Acme Meet", "colors": {"primary": "#7c3aed"}, "font_family": "Inter",
"strings": {"join_now": "Hop in"}, "toolbar": ["mic", "camera", "chat", "leave"]},
roles=[{"name": "moderator",
"permissions": ["chat.*", "moderate.mute", "moderate.lobby"],
"attributes": {"label": "Moderator", "badge": "🛡", "rank": 50}}])
Roles/permissions: GET /api/permissions, GET/POST /api/roles, PUT/PATCH/DELETE /api/roles/{name}, POST /api/roles/{name}/reset.
Branding: GET/PUT/PATCH /api/branding, public GET /api/branding/resolve?room=&host=.
Storage: pick SQL (DBAPIStorage(connect_fn, dialect="postgres")), key-value (KeyValueStorage(RedisKV(...))) or document (DocumentStorage(MongoDocs(...))) and plug in your own driver.
Note: in peer-to-peer mode, media permissions are enforced by the client. In SFU mode the server enforces them.
Waiting room (on by default) and blocked people
Every new room has a waiting room. Hosts, people on the room's join list, people who
booked the meeting and links made with skip_waiting_room=True go straight in; everyone
else waits until a moderator admits them.
await meet.create_room(room_id="class-1", join_list=["ravi@x.com", "u_42"]) # these skip it
await meet.add_to_join_list("class-1", "neha@x.com")
link = meet.invite("class-1", "guest-9", skip_waiting_room=True) # one-off pass
await meet.create_room(room_id="open-hall", waiting_room=False) # off for one room
meet = NodeMeet(secret, waiting_room=False) # off everywhere
Blocking (ban) someone, either in the meeting or straight from the waiting room:
- removes them and records who blocked them and when; they also drop off the join list;
- any later join attempt with that user id (even with a fresh token) is refused with a clear "you can't rejoin" message, never reaching the waiting room;
- moderators in the meeting get a notice ("X tried to rejoin") with an Unblock button
(at most once a minute per person);
on_blocked_attempthook andparticipant.blocked_attemptwebhook fire too; - unblocking (
meet.unban(room, user), UI, orDELETE /api/rooms/{id}/bans/{user}) lets them come back through the waiting room.
REST: GET|POST /api/rooms/{id}/join-list, POST /api/rooms/{id}/join-list/remove,
GET /api/rooms/{id}/bans, DELETE /api/rooms/{id}/bans/{user_id}; POST /api/tokens
accepts "skip_waiting_room": true. CLI: nodemeet serve --no-waiting-room,
nodemeet token ROOM USER --role teacher --grant chat.pin --skip-waiting-room.
See RECIPES.md for copy-paste snippets for every use case.
Metadata
Release files for nodemeet 0.9.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nodemeet-0.9.5.tar.gz | 367.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nodemeet-0.9.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 692.6 kB
Release files / nodemeet-0.9.5.tar.gz
| Download URL | nodemeet-0.9.5.tar.gz |
|---|---|
| Size | 367.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c49854d0c14268b7522994d7c58b894e76bbcf00c34ceb511cc6389a6fbbee47
|
|
BLAKE2b-256 checksum How to use checksums |
a9ec94a7dc2c39bc40ddd2ff19327459c1947ec3bcbd0035f0130a44308cd774
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / nodemeet-0.9.5-py3-none-any.whl
| Download URL | nodemeet-0.9.5-py3-none-any.whl |
|---|---|
| Size | 325.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
34d3d6504d4d65caa8f572ff9f21369cf169eb00b02dd464354bdbc56ea19dfd
|
|
BLAKE2b-256 checksum How to use checksums |
4d8fafed28d4f3376c84f1e18c1e19510337336c5d4416588c22916ac257df94
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|