Real-Time Collaboration
TruePPM uses Django Channels 4 to push project changes to connected clients over WebSocket.
Connecting
Section titled “Connecting”ws://localhost:8000/ws/projects/{project_id}/?token=<jwt>Authentication via ?token= JWT. Requires Member role or above (ordinal ≥ 1). Viewers are rejected with close code 4003.
Event format
Section titled “Event format”{ "type": "board.event", "event": "<event_name>", "payload": { ... }}Events
Section titled “Events”Schedule
Section titled “Schedule”| Event | Payload |
|---|---|
schedule_updated | {"project_id": "..."} |
| Event | Payload |
|---|---|
task_created | {"task_id": "..."} |
task_updated | {"task_id": "..."} |
task_deleted | {"task_id": "..."} |
Dependencies
Section titled “Dependencies”| Event | Payload |
|---|---|
dependency_created | {"dependency_id": "..."} |
dependency_deleted | {"dependency_id": "..."} |
Members
Section titled “Members”| Event | Payload |
|---|---|
member_added | {"membership_id": "...", "user_id": "...", "role": 1} |
member_role_changed | {"membership_id": "...", "user_id": "...", "role": 2} |
member_removed | {"membership_id": "...", "user_id": "..."} |
Broadcast safety
Section titled “Broadcast safety”All broadcasts are deferred inside transaction.on_commit() — events only fire if the database write committed successfully. No phantom events for rolled-back transactions.
Channel layer
Section titled “Channel layer”Uses Valkey (the BSD-licensed Linux Foundation fork of Redis; wire-compatible) configured via REDIS_URL. All api and celery containers share the same Valkey instance, so Celery-originated broadcasts (e.g. schedule_updated) reach WebSocket clients connected to any API container — safe for horizontal scaling. Existing Redis-compatible managed services (ElastiCache, Memorystore, Azure Cache for Redis) work as drop-in alternatives.
JavaScript example
Section titled “JavaScript example”const ws = new WebSocket( `ws://localhost:8000/ws/projects/${projectId}/?token=${jwt}`);
ws.onmessage = (event) => { const { event: name, payload } = JSON.parse(event.data); if (name === 'schedule_updated') fetchSchedule(payload.project_id); if (name === 'task_updated') fetchTask(payload.task_id);};