Phase 4: Activity Log, Card Comments with @Mentions, Live Presence & In-App Notifications
TaskFlow is a collaborative Kanban task board built with Python 3.12+ and Django 5.x on top of PostgreSQL, with live real-time sync powered by Django Channels and Redis.
In Phases 1, 2, 3 & 4, the project establishes:
- Secure authentication with a custom User model (equipped with
avatar_colorfor live presence). - Relational schema with role-based access control (RBAC):
Board,BoardMembership,BoardList,Card,Activity,Comment, andNotification. - Centralized access control returning
404 Not Foundfor non-members to prevent workspace enumeration. - Board workspace CRUD, member invitations, role promotions, and member removals restricted to workspace
OWNER. -
Dedicated business logic service layer (
boards/services.py) encapsulating all mutations withselect_for_update()concurrency locks. - Contiguous integer sequence ordering with automatic gap closure and renumbering.
- Dynamic drag-and-drop with vendored SortableJS for horizontal column reordering and cross-column card moves.
-
Live real-time multiplayer synchronization via Django Channels WebSockets (
ws/boards/<id>/). - One validated write path (CQRS pattern): Mutations flow via HTTP POST endpoints; WebSockets push real-time event deltas.
-
transaction.on_commitbroadcast guarantees: Clients never receive ghost events for rolled-back transactions. -
Client-side echo cancellation & collision handling via ephemeral tab
client_idand active drag queueing. - Automatic reconnect with exponential backoff & snapshot state reconciliation.
- Real-time membership lifecycle updates: Role demotions and member removals update or kick clients live.
- Immutable audit activity log recorded in same-transaction writes with frozen write-time summaries and cursor-based pagination.
-
Card comment discussion threads with chronological ordering, card badge counters, and strict XSS prevention (
textContent). -
Regex @mention extraction & in-app notifications delivered over private user WebSockets (
ws/notifications/) to an interactive navbar bell. - Live board presence tracking powered by Redis sorted sets with 60s stale pruning, 20s client heartbeats, and multi-tab deduplication.
-
Zero N+1 database queries: Board detail page optimized to exactly 7 total queries (
$O(1)$ query scaling). - Redis channel layer with zero-setup in-memory fallback and broker outage resilience.
- Django Admin with inline models for convenient entity inspection.
- A comprehensive
seed_demomanagement command for instant local testing.
- Python 3.12+
- PostgreSQL running locally on port 5432 (or use SQLite fallback if desired)
# Navigate to project directory
cd taskflow
# Create and activate virtual environment
python -m venv venv
.\venv\Scripts\activate # On Windows PowerShell / CMD
# or source venv/bin/activate on macOS/Linux
# Install pinned dependencies
pip install -r requirements.txtCopy .env.example to .env and verify your PostgreSQL credentials:
cp .env.example .envYour .env file contains:
SECRET_KEY=django-insecure-taskflow-dev-secret-key-phase1-super-secure
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost
# Database Configuration (PostgreSQL)
DB_ENGINE=django.db.backends.postgresql
DB_NAME=taskflow
DB_USER=postgres
DB_PASSWORD=root
DB_HOST=localhost
DB_PORT=5432
# Or toggle SQLite for instant zero-dependency testing:
USE_SQLITE=FalseEnsure the taskflow database exists in PostgreSQL (or create it: CREATE DATABASE taskflow;), then apply migrations:
python manage.py migratePopulate the database with 3 demo users, 1 board workspace, 3 columns, and 8 sample task cards:
python manage.py seed_demoSeeded Demo Credentials (all use password: password123):
alice— Role: OWNER (full workspace management, invites, deletes)bob— Role: EDITOR (member with edit privileges)charlie— Role: VIEWER (read-only member)
(An admin superuser admin / admin123 is also available for accessing /admin/).
python manage.py runserverVisit: http://127.0.0.1:8000/
taskflow/
├── manage.py
├── requirements.txt # Pinned Django 5.x, psycopg 3, python-decouple
├── .env.example # Example environment file
├── .env # Local environment variables
├── .gitignore # Python, Django, venv, and DB ignore rules
├── taskflow/ # Core project configuration
│ ├── __init__.py
│ ├── settings.py # Decouple-driven settings, custom AUTH_USER_MODEL
│ ├── urls.py # Top-level routing: accounts/, boards/, admin/
│ ├── wsgi.py
│ └── asgi.py
├── accounts/ # Authentication & user profile app
│ ├── models.py # Custom User extending AbstractUser (avatar_color)
│ ├── forms.py # SignUpForm with color picker widget
│ ├── views.py # SignUpView with automatic post-registration login
│ ├── urls.py # /accounts/login/, /accounts/signup/, /accounts/logout/
│ ├── admin.py # UserAdmin with avatar_color in fieldsets
│ └── tests.py # User creation and auth flow tests
├── boards/ # Workspace, membership & Kanban models
│ ├── models.py # Board, BoardMembership, BoardList, Card
│ ├── permissions.py # Centralized get_board_for_member & BoardAccessMixin
│ ├── forms.py # BoardForm, AddMemberForm, UpdateMemberRoleForm
│ ├── views.py # MyBoards, Create, Detail, Edit, Delete, ManageMembers
│ ├── urls.py # Workspace routes
│ ├── admin.py # Inlines: BoardMembershipInline, BoardListInline, CardInline
│ ├── tests.py # Access control & CRUD unit tests
│ └── management/
│ └── commands/
│ └── seed_demo.py # Seed demo users, boards, lists, and cards
├── static/
│ └── css/
│ └── style.css # Vanilla CSS modern design system
└── templates/
├── base.html # Base layout with navbar, avatar badges, alerts
├── accounts/
│ ├── login.html # Login card with demo credential quick-hints
│ └── signup.html # Signup card with avatar color picker
└── boards/
├── my_boards.html # User's boards dashboard showing roles
├── board_create.html # Board creation form
├── board_detail.html # Workspace columns & cards read-only view
├── board_edit.html # Owner edit board form
├── board_confirm_delete.html # Owner delete confirmation
└── manage_members.html # Owner member roster, invites, role changes & removal
Each model was designed specifically to support interview discussions regarding scalability, clean relational design, and security:
- Why extend
AbstractUseron migration 0001? Django's default auth model is notoriously difficult to substitute once foreign keys have been registered across a project. Defining a custom user model upfront avoids migration deadlocks and provides flexibility to add features likeavatar_colorand future profile or OAuth fields without database surgery. avatar_color(CharField(max_length=7)): Stores a hex color string (e.g.#4F46E5). In Phase 1, it provides a distinctive colored avatar bubble in the navbar and on cards. In Phase 3 (WebSockets), it will power real-time cursor/presence indicators.
- Role: High-level collaborative workspace entity.
- Fields:
title: Name of the board workspace.description: Context and workspace objectives.owner: Foreign key toUser. The board creator who retains final authority (board deletion, owner cannot be demoted).created_at&updated_at: Timestamps for auditability and sorting.
- Optimization: Indexed on
ownerandcreated_at. Ordered by['-updated_at']so recently active boards surface first.
-
Role: Explicit join table modeling the Many-to-Many relationship between
BoardandUser. -
Why an explicit through model instead of
ManyToManyField? An explicit through model allows storing critical relational metadata on the edge itself: the member'srole(OWNER,EDITOR,VIEWER) andjoined_attimestamp. -
Constraints & Indexes:
-
UniqueConstraint(fields=['board', 'user']): Prevents duplicate memberships at the database level. - Composite index on
(board, user): Makes permission lookups on every request$O(1)$ fast. - Index on
user: Enables instant querying of all boards a user belongs to for the "My Boards" dashboard.
-
- Role: Workflow columns within a board (e.g. "To Do", "In Progress", "Done").
- Why named
BoardList? Prevents shadowing Python's built-inlistprimitive. position(PositiveIntegerField): Enables explicit arbitrary sequencing from left to right. This decouples visual column order from insertion primary keys, making drag-and-drop column reordering trivial in later phases.- Optimization: Indexed on
(board, position)and ordered by['position', 'created_at'].
- Role: Discrete task items nested inside a
BoardList. position(PositiveIntegerField): Enables vertical ordering within a column.created_by(ForeignKey(User)): Preserves authorship and task provenance regardless of who edits or drags the card across columns.- Optimization: Indexed on
(board_list, position)and ordered by['position', 'created_at'].
All authorization checks are consolidated in get_board_for_member and BoardAccessMixin:
- Preventing Workspace Enumeration (404 vs 403):
When a user requests a board they are not a member of (e.g.,
/boards/999/), TaskFlow raisesHttp404rather thanHttp403. Returning403 Forbiddenwould confirm to unauthorized users that a board ID exists, exposing private organizational boards to ID enumeration attacks. - Owner-Only Enforcement:
Views modifying workspace metadata (
BoardUpdateView,BoardDeleteView,BoardMembersView) inherit fromBoardOwnerRequiredMixin. Non-members receive404, and non-owner members receive403 PermissionDenied. - Owner Invariant Protection: The board owner cannot be removed or demoted from the workspace, preventing orphaned boards.
To maintain separation of concerns and prepare for WebSocket broadcasts in Phase 3, all mutation operations are isolated into pure service functions:
| Service Function | Responsibility | Return Value |
|---|---|---|
create_list(board, title, user) |
Appends column at end (position = count) |
Created BoardList |
rename_list(board_list, new_title, user) |
Updates column header title | Updated BoardList |
delete_list(board_list, user) |
Cascades cards and renumbers remaining columns | None |
move_list(board_list, new_position, user) |
Reorders column within board; renumbers siblings | Updated BoardList |
create_card(board_list, title, description, user) |
Appends card at bottom of column (position = count) |
Created Card |
update_card(card, title, description, user) |
Modifies task card title/description | Updated Card |
delete_card(card, user) |
Deletes card and renumbers list to close gap | None |
move_card(card, target_list, new_position, user) |
Moves card within list or across columns with renumbering | Updated Card |
Views act purely as thin HTTP/JSON adapters, delegating all domain logic and validation to this module.
TaskFlow uses Contiguous Integer Sequence Positioning (0, 1, 2, ... N-1):
-
How It Works:
- Moving an item removes it from its current position, closing the sequence gap.
- The item is inserted at the desired destination index.
- Neighboring rows are renumbered so indices remain contiguous zero-indexed integers without holes or duplicates.
-
Tradeoff Comparison:
| Strategy | Pros | Cons / Tradeoffs | Decision in TaskFlow |
|---|---|---|---|
| Contiguous Integers (Selected) | Deterministic sorting, human-readable, zero risk of precision exhaustion, zero database bloat | Requires |
Adopted: Columns rarely exceed 50–100 cards; indexed updates execute in <5ms inside transactions |
| Fractional / Float Indexing | Single-row update ($O(1)$) by taking midpoint |
Rapidly collapses due to IEEE 754 precision exhaustion; requires background rebalancing jobs | Rejected: High complexity and precision failure risks |
| LexoRank / String Keys | Single-row update using alphanumeric midpoint strings | String storage overhead, complex boundary generation, eventual string bloat | Rejected: Overkill for Kanban workload |
- Race Condition Protection:
Multi-row renumbering is locked using
transaction.atomic()andselect_for_update(). This serializes concurrent drag-and-drop operations, preventing sequence duplication or interleaving.
- Library: SortableJS vendored directly into
static/vendor/sortable.min.js. - Why Vendored? Eliminates CDN latency, provides offline resilience, and prevents external CDN outages from breaking internal workflows.
- Optimistic UI with Rollback: On drop, the DOM changes immediately. If the server rejects the request (e.g. permission error, session timeout), the script catches the failure, reverts the DOM element back to its original parent and position, and displays an informative toast notification.
All endpoints require CSRF authentication and accept HTTP POST:
| Method | Endpoint URL | Input Payload | Status Codes | Description |
|---|---|---|---|---|
POST |
/boards/<b_id>/lists/create/ |
{"title": "..."} |
200, 400, 403, 404 | Appends a column |
POST |
/boards/<b_id>/lists/<l_id>/rename/ |
{"title": "..."} |
200, 400, 403, 404 | Renames a column |
POST |
/boards/<b_id>/lists/<l_id>/delete/ |
{} |
200, 403, 404 | Deletes column and its cards |
POST |
/boards/<b_id>/lists/<l_id>/move/ |
{"new_position": 1} |
200, 400, 403, 404 | Repositions a column |
POST |
/boards/<b_id>/lists/<l_id>/cards/create/ |
{"title": "...", "description": "..."} |
200, 400, 403, 404 | Appends a card |
POST |
/boards/<b_id>/cards/<c_id>/update/ |
{"title": "...", "description": "..."} |
200, 400, 403, 404 | Updates card title/desc |
POST |
/boards/<b_id>/cards/<c_id>/delete/ |
{} |
200, 403, 404 | Deletes card |
POST |
/boards/<b_id>/cards/<c_id>/move/ |
{"target_list_id": 2, "new_position": 0} |
200, 400, 403, 404 | Moves card within or across lists |
GET |
/boards/<b_id>/snapshot/ |
N/A | 200, 404 | Returns full board state for reconnect reconciliation |
| Action | Workspace OWNER |
EDITOR |
VIEWER |
Non-Member / Anonymous |
|---|---|---|---|---|
| View Board & Cards | ✅ Yes | ✅ Yes | ✅ Yes (Read-Only) | ❌ 404 Not Found |
| Create / Rename / Delete Lists | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Reorder Lists (Drag & Drop) | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Create / Update / Delete Cards | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Move Cards (Drag & Drop) | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Connect to WebSocket Live Sync | ✅ Yes | ✅ Yes | ✅ Yes | ❌ 4404 / 4401 Closed |
| Manage Members (Add / Change Roles) | ✅ Yes | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 404 Not Found |
| Edit Board Metadata / Delete Board | ✅ Yes | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 404 Not Found |
- The Design Decision:
All board state mutations (create, update, delete, move) flow strictly through the existing HTTP POST endpoints and
boards/services.py. WebSockets are used exclusively to push real-time event notifications from server to clients. - Why this pattern?
- Centralized Command Validation: Centralizes permissions, input sanitization, database transactions, row locking (
select_for_update), and contiguous renumbering inservices.py. - Security & Session Simplicity: Leverages Django's mature CSRF protection, session management, and HTTP status codes (
400,403,404) without re-inventing error protocols over WebSocket frames. - Guaranteed Delivery on Commit: Broadcasters are executed within
transaction.on_commit(). If an operation fails and rolls back in PostgreSQL, clients will never receive ghost events for state that never persisted.
- Centralized Command Validation: Centralizes permissions, input sanitization, database transactions, row locking (
sequenceDiagram
autonumber
actor Alice as Alice (Editor)
participant ClientA as Alice's Browser
participant Django as Django HTTP Views
participant Service as services.py
participant DB as PostgreSQL
participant Redis as Redis Channel Layer
participant Daphne as Daphne / Channels (WebSocket)
participant ClientB as Bob's Browser (Collaborator)
ClientA->>Django: POST /boards/1/cards/10/move/ (Header: X-Client-ID: "tab_xyz")
Django->>Service: move_card(card, target_list, new_position, user, client_id="tab_xyz")
Service->>DB: Atomic Update with select_for_update()
DB-->>Service: Database Transaction Committed
Service->>Redis: transaction.on_commit -> group_send("board_1", event)
Django-->>ClientA: 200 OK (Optimistic Confirmation)
Redis-->>Daphne: Broadcast event to channel layer group
Daphne-->>ClientA: WebSocket push (Ignored: matches tab_xyz)
Daphne-->>ClientB: WebSocket push (card.moved)
ClientB->>ClientB: Reorder DOM element & update count badges
All messages published to the board_<board_id> channel layer group adhere to a consistent envelope:
{
"event": "<event_type>",
"payload": { ... },
"board_id": 1,
"actor_id": 1,
"client_id": "tab_xyz"
}| Event Type | Trigger | Payload Schema | Client Action |
|---|---|---|---|
list.created |
New column added | {"list": {"id", "title", "position"}} |
Renders new column DOM element |
list.renamed |
Column title updated | {"list": {"id", "title"}} |
Updates header title text |
list.deleted |
Column deleted | {"list_id": int} |
Removes column DOM element |
list.moved |
Column repositioned | {"list_id": int, "new_position": int} |
Repositions column in board track |
card.created |
New task card created | {"card": {"id", "title", "description", "position", "list_id", "author", "author_initials", "author_color"}} |
Renders card in target list; updates counts |
card.updated |
Card title/description edited | {"card": {"id", "title", "description"}} |
Updates card in DOM; live-updates edit modal if currently open |
card.deleted |
Task card deleted | {"card_id": int, "list_id": int} |
Removes card DOM; closes edit modal if open with warning |
card.moved |
Card dragged/reordered | {"card_id": int, "source_list_id": int, "target_list_id": int, "new_position": int} |
Repositions card in target list; recalculates indices |
card.assigned |
Card assignee updated | {"card_id": int, "assignee": {"id", "username", "avatar_color", "initials"}} |
Updates card assignee badge and modal select |
comment.added |
New comment posted | {"comment": {"id", "card_id", "author", "body", "created_at"}, "comments_count": int} |
Appends comment to modal thread; updates card badge |
comment.edited |
Comment body edited | {"comment": {"id", "card_id", "body", "edited_at"}} |
Updates comment text and edited timestamp |
comment.deleted |
Comment removed | {"comment_id": int, "card_id": int, "comments_count": int} |
Removes comment from thread; updates card badge |
activity.created |
Board mutation logged | {"activity": {"id", "summary", "created_at", "actor": {"username", "avatar_color"}}} |
Prepends new activity item to slide-out drawer |
presence.changed |
Member joins/leaves/heartbeats | {"users": [{"id", "username", "avatar_color", "initials"}]} |
Updates live presence avatar stack in board header |
member.role_changed |
Member role modified | {"user_id": int, "new_role": str} |
Updates role badge; toggles drag/edit controls live |
member.removed |
Member kicked from board | {"user_id": int} |
Closes WebSocket with code 4403; redirects user to /boards/ |
board.deleted |
Board workspace deleted | {"board_id": int} |
Closes WebSocket with code 4403; redirects all users to /boards/ |
- Same-Transaction Audit Writes:
Every state-changing mutation in
boards/services.pylogs anActivityrecord within the exact sametransaction.atomic()block viaboards/activity.py: log_activity(). If any mutation fails or rolls back, zero orphan activity records are ever committed. - Write-Time Frozen Summaries:
The
summaryfield is pre-rendered at the exact moment of the mutation (e.g., "bob created card 'Write Docs'"). If the card is subsequently renamed, moved, or deleted, past activity logs remain historically accurate and immutable. - Database Indexing:
The
Activitymodel features a composite index on(board, -created_at)guaranteeing sub-millisecond retrieval without table scans. - Cursor-Based Pagination (
before_id): The activity drawer endpoint (GET /boards/<id>/activity/?before_id=<id>&limit=20) uses strict cursor-based pagination on primary keys (id < before_id). Unlike offset-based pagination (OFFSET N), cursor pagination is immune to duplicate or skipped entries when new events are continuously prepended. - Live Push Dispatch:
When a transaction commits,
log_activitytriggersbroadcast(board.id, 'activity.created', ...)viatransaction.on_commit(), streaming newly logged activities directly into all active drawer views.
- Modal Discussion Thread: Opening any card reveals its comment thread sorted in chronological order (oldest first).
- Regex @Mention Extraction:
When a comment is created via
services.add_comment(), a regular expression parses all@usernamementions:pattern = r"(?<!\w)@([a-zA-Z0-9_.+-]+)"
- Mentions are filtered against active board members only.
- Non-members mentioned in text are silently ignored.
- Self-mentions by the comment author do not generate notifications.
- Valid mentions generate a
Notificationrecord and immediately push a live WebSocket notification to the mentioned user.
- Comment Permissions:
OWNER&EDITOR: Can create comments on any card.VIEWER: Read-only access; commenting endpoints reject viewers with403 Forbidden.- Editing: Strictly restricted to the original comment author (
comment.author == request.user). - Deletion: Allowed for either the comment author OR the workspace
OWNER.
- Zero XSS Guarantee:
All comment bodies, activity summaries, and usernames are inserted into the DOM strictly via
textContentor text node instantiation, entirely eliminating XSS vectors.
- Data Model & Performance:
The
Notificationmodel tracksrecipient,actor,board,card,kind(mention/assigned), andis_read. It is indexed on(recipient, is_read, -created_at)for instant unread query execution. - User-Dedicated WebSocket Channel:
Clients establish a dedicated notification connection to
ws/notifications/.NotificationConsumerauthenticates the user and joins the personal groupuser_<user_id>. When a notification is generated anywhere across any board, it is delivered exclusively to that user's private channel. - Navbar Bell & Live Counter: The navbar features an interactive bell icon with a live unread badge. Clicking the bell displays a dropdown with unread and recent notifications, with actions to mark individual notifications read or mark all read.
- Card Assignment Flow:
Assigning a member to a card via
services.assign_card()updatesCard.assignee, logs an activity record, dispatches acard.assignedevent to the board, and dispatches anassignednotification to the assignee's personal channel.
- High-Throughput In-Memory Storage:
Presence is stored in Redis sorted sets rather than PostgreSQL, preventing unnecessary database write amplification:
- Key Format:
presence:board:<board_id> - Member Format:
<user_id>:<channel_name>(uniquely identifies a specific WebSocket connection) - Score: Current Unix timestamp (
time.time())
- Key Format:
- Lifecycle & Stale Pruning:
- Connect: Adds the connection member and score; prunes scores older than 60 seconds (
ZREMRANGEBYSCORE). - Heartbeat: Clients send keepalive pings every 20 seconds, updating the score in Redis.
- Disconnect: Explicitly removes the connection member on socket teardown.
- Key TTL: A 2-hour TTL is maintained on presence keys (
EXPIRE) to prevent orphaned keys if a board is abandoned.
- Connect: Adds the connection member and score; prunes scores older than 60 seconds (
- Multi-Tab Deduplication:
If a user opens the same board across 3 tabs, all 3 channel names are tracked in Redis, but
get_board_presence()groups them byuser_id, rendering a single avatar in the board header. - Broker Fault Tolerance & Test Isolation:
If Redis is down or unreachable, presence calls gracefully degrade, returning an empty presence list without crashing the board. When running automated test suites or using
USE_IN_MEMORY_CHANNEL_LAYER=True, presence seamlessly uses a thread-safe in-memory dictionary.
| Action | Workspace OWNER |
EDITOR |
VIEWER |
Non-Member / Anonymous |
|---|---|---|---|---|
| View Board, Lists & Cards | ✅ Yes | ✅ Yes | ✅ Yes (Read-Only) | ❌ 404 Not Found |
| Create / Rename / Delete Lists | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Reorder Lists (Drag & Drop) | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Create / Update / Delete Cards | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Move Cards (Drag & Drop) | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Assign Member to Card | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Post Comments & @Mentions | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Edit Own Comment | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Edit Others' Comments | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 404 Not Found |
| Delete Own Comment | ✅ Yes | ✅ Yes | ❌ 403 Forbidden | ❌ 404 Not Found |
| Delete Others' Comments | ✅ Yes | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 404 Not Found |
| View Activity Log & Presence | ✅ Yes | ✅ Yes | ✅ Yes | ❌ 404 Not Found |
| Manage Members (Add / Change Roles) | ✅ Yes | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 404 Not Found |
| Edit Metadata / Delete Board | ✅ Yes | ❌ 403 Forbidden | ❌ 403 Forbidden | ❌ 404 Not Found |
| Connect to Board WebSocket | ✅ Yes | ✅ Yes | ✅ Yes | ❌ 4404 / 4401 Closed |
| Connect to Notifications WebSocket | ✅ (Own Room) | ✅ (Own Room) | ✅ (Own Room) | ❌ 4401 Closed |
-
Board Detail Query Audit:
Loading the full board workspace (
BoardDetailView) performs exactly 7 total database queries:- Session retrieval (Authentication)
- Authenticated user lookup
- Board entity lookup (
BoardAccessMixin) - Active membership role verification (
BoardMembership) - Lists with prefetched Cards, Card authors, Card assignees, and comment count annotations
- Board memberships with pre-fetched User models
- Board owner lookup
-
Verification Method:
Benchmarked using
django.db.connection.queriesin authenticated test runs. Regardless of whether a board contains 5 cards or 500 cards, the query count remains strictly constant ($O(1)$ query scaling).
- Echo Cancellation: The client generates an ephemeral
client_id(e.g.tab_abc123_171000000) per browser tab and sends it via theX-Client-IDHTTP header. When Daphne pushes the event back to the board group, the originating tab detectsevent.client_id === myTabIdand skips the DOM update, preventing visual jitter. - Drag Collision Avoidance: If a remote user moves a card while the local user is actively dragging an item, incoming events are placed into a FIFO event queue. As soon as the local drop completes (
onEnd), queued events are flushed in order.
TaskFlow uses Redis for multi-worker channel communication. Run Redis via Docker or WSL:
# Option A: Run Redis in Docker
docker run -d --name taskflow-redis -p 6379:6379 redis:alpine
# Option B: Run Redis in WSL / Linux
sudo service redis-server startNote: For lightweight testing without Redis, set USE_IN_MEMORY_CHANNEL_LAYER=True in .env.
With 'daphne' registered at the top of INSTALLED_APPS, the standard Django runserver command serves both HTTP and WebSockets:
python manage.py runserver(Visit http://127.0.0.1:8000/)
daphne -b 0.0.0.0 -p 8000 taskflow.asgi:applicationTo scale TaskFlow horizontally across multiple worker nodes:
- Run multiple Daphne or ASGI server instances behind an Application Load Balancer (ALB) or Nginx with WebSocket proxying enabled (
Upgrade $http_upgrade,Connection "Upgrade"). - All Daphne nodes connect to the same centralized Redis cluster (or Redis Sentinel).
- Any worker node processing an HTTP mutation publishes to Redis; Redis distributes the event to all Daphne nodes; each Daphne node fans out the message to its connected client WebSockets.