-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfolder-structure.txt
More file actions
1070 lines (1041 loc) · 77 KB
/
Copy pathfolder-structure.txt
File metadata and controls
1070 lines (1041 loc) · 77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
VIRTUAL STUDY GROUP - FOLDER STRUCTURE
=======================================
virtual_study_group/
|
|-- README.md # Project documentation (features, setup, routes, API)
|-- folder-structure.txt # This file - explains the project structure
|-- LICENSE # GNU General Public License v3.0
|-- video-call-architecture.txt # Breakdown of what's in-house vs Agora in the
| video call feature (~80% in-house). Covers:
| Agora's role (WebRTC transport, TURN/STUN,
| encoding, channel routing, publish/subscribe)
| vs in-house code (room sessions, invite links,
| call UI, track lifecycle, voice activity
| detection, usage limits, remote user presence,
| companion name, chat, AI, whiteboard)
|-- whiteboard-architecture.txt # A-Z breakdown of the collaborative whiteboard.
| Excalidraw's role (rendering engine only) vs
| in-house features (live diagram sync, late-joiner
| sync, persistent state via MongoDB, collaborator
| cursors, presence pills, follow mode via viewport
| mirroring, clear sync, AI assist, summaries,
| download button). Socket events reference,
| MongoDB WhiteboardState model, frontend
| architecture (useEffect split, version
| fingerprinting, CSS overrides)
|
|-- backend/ # Node.js + Express + TypeScript backend
| |-- .env # Environment variables (DATABASE_URL, MONGODB_URI,
| | UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN,
| | JWT_SECRET, PORT, OTP_SECRET, OTP_EXPIRY_MINUTES,
| | AI_PROVIDER, GEMINI_API_KEY, GEMINI_EMBEDDING_MODEL,
| | GROK_API_KEY, OPENROUTER_API_KEY,
| | R2_*, AGORA_APP_ID,
| | RESEND_API_KEY, RESEND_FROM_EMAIL,
| | LISTEN_NOTES_API_KEY,
| | + optional rate limit overrides)
| |-- .gitignore # Git ignore rules for backend
| |-- package.json # Backend dependencies and scripts
| |-- tsconfig.json # TypeScript configuration for backend
| |
| |-- drizzle.config.ts # Drizzle Kit config — points to schema.ts,
| | output dir (src/db/migrations/), dialect
| | postgresql, reads DATABASE_URL from .env
| |
| |-- src/ # All backend source code
| |
| |-- server.ts # Main entry point - Express app setup, triple-layer
| | data init (NeonDB PostgreSQL + MongoDB + Upstash
| | Redis), CORS config, route mounting. Eagerly
| | initializes getNeonDb() and getRedis() at startup
| | (exits on failure). MongoDB for summaries. Redis
| | for OTP cache, podcast cache, rate limiting.
| | Registers all 11 route groups (includes
| | roomSessionRoutes at /room). Port via .env (PORT).
| | Applies global rate limiter (200 req/IP/15min,
| | Redis-backed). Sets trust proxy for correct IP
| | behind proxies. On listen: starts
| | startPodcastCacheJob() (Tue & Sat 02:00 UTC) and
| | startNotificationCleanupJob() (daily 03:00 UTC)
| |
| |-- socketServer.ts # Socket.IO server with JWT auth middleware.
| | Handles: room chat, invites (companion-gated),
| | companion events, DM rooms, presence tracking,
| | whiteboard collaboration.
| | Uses Drizzle queries for companions, DMs,
| | notifications (no Mongoose imports for these).
| | Per-user event throttling via socketThrottles map:
| | dm:send (200ms), companion:sendRequest (5s),
| | sendInvite (3s), whiteboard:update (100ms),
| | whiteboard:pointer (50ms). Throttle intervals
| | read from RATE_LIMIT_CONFIG.
| | serverMessage handler: broadcasts to room and
| | persists each message via insertChat (Drizzle).
| | DM events: dm:send, dm:markRead, dm:join.
| | Whiteboard events:
| | - whiteboard:update (caches in whiteboardCache
| | Map, broadcasts whiteboard:sync to room)
| | - whiteboard:clear (clears cache + MongoDB
| | WhiteboardState, broadcasts whiteboard:cleared)
| | - whiteboard:join (socket.join(roomId), adds to
| | whiteboardUsers, broadcasts whiteboard:users,
| | sends cached/MongoDB state via whiteboard:state)
| | - whiteboard:leave (removes from whiteboardUsers,
| | broadcasts updated whiteboard:users)
| | - whiteboard:save (upserts whiteboardCache to
| | MongoDB WhiteboardState collection)
| | - whiteboard:pointer (throttled 50ms, broadcasts
| | whiteboard:pointer-update with userId, userName,
| | pointer {x,y,tool}, button, viewport
| | {scrollX,scrollY,zoom})
| | In-memory Maps: userSocketMap (userId->socketId),
| | roomParticipants, whiteboardCache (roomId->
| | elements[]), whiteboardUsers (roomId->Map of
| | {userId,userName}). Imports WhiteboardState
| | Mongoose model for persistent state.
| | Notification events: notification:new (pushed to
| | recipient on companion_request, companion_accepted,
| | room_invite). saveAndEmitNotification() via
| | createNotification() Drizzle query.
| | Exports: getIO(), getSocketIdForUser()
| |
| |-- socketServer2.ts # Socket.IO server for live streaming.
| | Spawns FFmpeg child process for RTMP output.
| | Handles binary stream data from frontend.
| |
| |-- utils/ # Backend utility modules
| | |-- sendEmail.ts # Resend email service — sends emails via Resend
| | | REST API (replaced Nodemailer + Gmail SMTP).
| | | Singleton pattern: getResend() initializes once.
| | | Uses RESEND_API_KEY, RESEND_FROM_EMAIL env vars.
| | | Exports sendEmail(to, subject, html)
| | |-- otp.ts # Redis-backed OTP utility — HMAC-SHA256 based.
| | generateOtp(email) → { otp } (async, stores
| | hash in Redis at otp:{email} with TTL).
| | verifyOtp(email, otp) → boolean (async,
| | retrieves hash from Redis, deletes on success
| | for one-time use). Uses OTP_SECRET +
| | OTP_EXPIRY_MINUTES env vars. Redis TTL handles
| | auto-expiry. crypto.timingSafeEqual for
| | timing-attack prevention
| |
| |-- jobs/ # Scheduled background jobs
| | |-- podcastCacheJob.ts # node-cron job: refreshes all 5 podcast topic caches
| | | in Upstash Redis at 02:00 UTC every Tuesday (2)
| | | and Saturday (6). Sequential topic fetching with
| | | 1.5s gap between API calls to avoid burst rate
| | | limiting. Imports refreshTopicCache() + VALID_TOPICS
| | | from PodcastController. Exports startPodcastCacheJob()
| | | called from server.ts on startup. Logs per-topic
| | | success/failure; keeps stale cache on API failure
| | |-- notificationCleanupJob.ts # node-cron job: deletes expired notifications
| | from PostgreSQL. Runs daily at 03:00 UTC ('0 3 * * *').
| | Calls deleteExpiredNotifications() from Drizzle queries
| | (removes notifications older than 10 days). Replaces
| | MongoDB TTL index. Exports startNotificationCleanupJob()
| | called from server.ts on startup
| |
| |-- helpers/ # Backend helper modules
| | |-- callUsage.ts # Daily call time tracking via Upstash Redis.
| | MAX_DAILY_CALL_SECONDS = 3600 (1 hour).
| | Redis key: callUsage:{userId}:{YYYY-MM-DD}
| | with 24h TTL. Exports: getRemainingSeconds(),
| | incrementCallUsage(deltaSeconds)
| |
| |-- controllers/ # Request handlers (business logic)
| | |-- AuthController.ts # OTP-based auth + Google OAuth. Uses Drizzle
| | | queries (findByEmail, createUser, updateUser).
| | | sendOtp (generates OTP via Redis-backed hash,
| | | emails it via Resend, returns { success: true }).
| | | Register (verify OTP from Redis, create user,
| | | JWT sign), Login (verify OTP from Redis, find
| | | user, JWT sign). Frontend sends only { email,
| | | otp } — no hash/expires needed.
| | | googleLogin: validates Google access_token
| | | via googleapis userinfo endpoint, creates or
| | | finds user by email, stores googleId, issues JWT.
| | | Passwordless — no bcrypt, no password storage
| | |-- RoomController.ts # Create room, join room, get room users.
| | | Uses Drizzle queries (dbCreateRoom, findRoomById,
| | | addUserToRoom, getUsersInRoom)
| | |-- ChatController.ts # Add chat message, get chat history, get user name.
| | | bulkSaveChats: POST /chat/bulk-save — bulk-saves
| | | room chat messages to PostgreSQL. Accepts messages
| | | array (max 500), filters bot messages, generates
| | | sessionId (UUID) for grouping, uses Drizzle
| | | bulkInsertChats
| | |-- CompanionController.ts # Send/accept/decline companion requests,
| | | get companion list, get pending requests.
| | | Uses Drizzle queries. Emits real-time socket
| | | notifications. sendCompanionRequest handles
| | | unique constraint violation (23505) gracefully.
| | | acceptCompanionRequest emits companion:online
| | | to both parties, creates companion_accepted
| | | notification, deletes old companion_request
| | | notification via deleteByTypeAndSender
| | |-- RoomSessionController.ts # createOrResumeSession: creates or resumes
| | | a 12h TTL room session (POST /room/session).
| | | listSessions: returns all sessions for current
| | | user (GET /room/sessions). getSessionChats:
| | | returns chat history for a session room
| | | (GET /room/sessions/:sessionRoomId/chats,
| | | owner-only auth check). Uses Drizzle queries
| | | from db/queries/roomSessions
| | |-- UserController.ts # getProfile: returns name, email, bio, avatar,
| | | education, projects, workExperience,
| | | companionCount for authenticated user.
| | | updateProfile: updates name/bio/avatar/
| | | education/projects/workExperience, re-issues JWT.
| | | searchUsers: search by name/email (case-insensitive,
| | | excludes self, limit 10). All via Drizzle queries
| | |-- AiController.ts # Switchable AI provider (Gemini/Grok/OpenRouter)
| | | via AI_PROVIDER env var. Uses OpenAI SDK with
| | | provider config object + lazy-initialized
| | | singleton. Default: Gemini 2.5 Flash.
| | | OpenRouter provider adds middle-out context
| | | compression (auto-trims long conversations).
| | | Separate getEmbeddingClient() always uses
| | | Gemini directly for embeddings (OpenRouter
| | | doesn't support embedding models).
| | | POST /ai/ask (question -> answer)
| | | POST /ai/summary (messages -> summary)
| | | POST /ai/whiteboard-explain (elements +
| | | optional question -> explanation)
| | | POST /ai/whiteboard-summary (elements ->
| | | summary). POST /ai/dm-summary (fetches
| | | up to 100 DMs via Drizzle getDmTranscript,
| | | formats as transcript, generates summary).
| | | describeWhiteboardElements() helper
| | | converts elements to compact text.
| | | RAG Q&A: generateEmbedding() uses Gemini
| | | text-embedding-004 (768-dim vectors) with
| | | global daily cap via Drizzle incrementEmbeddingCount.
| | | cosineSimilarity() for in-memory vector
| | | comparison. querySummaries() controller:
| | | embeds question -> cosine similarity search
| | | across user's summaries -> top 5 as context
| | | -> Gemini chat completion -> answer with
| | | source citations
| | |-- SummaryController.ts # Save AI summary to Cloudflare R2 + MongoDB.
| | | Monthly upload quota per user via Drizzle
| | | getUploadCount/incrementUploadCount (PostgreSQL
| | | upload_counters table). R2_MAX_UPLOADS_PER_MONTH
| | | env var (def 10). Builds styled HTML document
| | | with purple gradient header, metadata (user,
| | | room, date), and formatted bullet-point content.
| | | Uploads via @aws-sdk/client-s3. Returns presigned
| | | download URL (7-day expiry). Broadcasts VSG Bot
| | | message with link to room chat via Socket.IO
| | | (skipped for DM summaries). Generates vector
| | | embedding at save time via generateEmbedding()
| | | (non-blocking on failure).
| | | POST /ai/save-summary (summary + roomId -> url)
| | | GET /ai/summaries (list by type, 50 per page)
| | | GET /ai/summaries/:id/download (generates fresh
| | | presigned URL from R2 key, 1-hour expiry)
| | | DELETE /ai/summaries/:id (ownership check)
| | |-- DmController.ts # getRecentChats: SQL CTE + window function
| | | (ROW_NUMBER OVER PARTITION BY companion)
| | | returning last message per conversation
| | | partner, sorted by most recent, with unread
| | | counts. Replaces MongoDB aggregation pipeline.
| | | getDmHistory: last 50 messages via Drizzle.
| | | getUnreadCounts: unread count per sender
| | | (for restoring green rings on refresh).
| | | markDmRead: marks sender's messages as read +
| | | emits dm:readUpdate to sender via socket.
| | | All queries via Drizzle (db/queries/directMessages)
| | |-- NotificationController.ts # getNotifications (last 50), markRead (single),
| | | markAllRead, deleteNotification. All scoped to
| | | authenticated user via verifyToken middleware.
| | | All queries via Drizzle (db/queries/notifications)
| | |-- NewsController.ts # Returns 12 mock news articles across
| | | AI, Tech, Productivity categories
| | |-- CallUsageController.ts # Daily call time rate limiting.
| | | getCallTimeRemaining: GET /room/call-usage
| | | — returns { remainingSeconds } for current
| | | user. reportCallUsage: POST /room/call-usage
| | | — accepts { deltaSeconds }, increments daily
| | | usage. Uses helpers/callUsage (Redis-backed,
| | | MAX_DAILY_CALL_SECONDS = 3600s = 1 hour)
| | |-- PodcastController.ts # Listen Notes API integration with Upstash Redis
| | cache. Topics: trending, ai, tech, business,
| | productivity. Cache: Redis key podcast:{topic}
| | with 4-day TTL (345,600s). Replaced former MongoDB
| | Podcast model. Cache valid if fetchedAt >= last
| | Tue/Sat midnight (getLastRefreshBoundary() walks
| | back up to 7 days). Request flow: fresh cache ->
| | Listen Notes API -> stale cache -> mock fallback
| | (3 items/topic). Response: { data, fetchedAt,
| | source }. Audio enrichment pipeline:
| | findRssUrl() queries iTunes Search API (free,
| | no key) to get RSS feed URL by podcast title +
| | publisher; parseRssForAudio() fetches RSS XML
| | and extracts <enclosure> MP3 URL, <title>,
| | <itunes:duration> from first <item>; enrichWithAudio()
| | runs both in parallel for all 12 podcasts (5s
| | timeout each). PodcastItem includes: audio (MP3
| | URL | null), audioLengthSec, latestEpisodeTitle.
| | Exports: getPodcasts (controller),
| | refreshTopicCache (used by cron job), VALID_TOPICS,
| | Topic type. Env: LISTEN_NOTES_API_KEY
| |
| |-- routes/ # Express route definitions
| | |-- authRoutes.ts # /auth/send-otp (OTP email+IP rate limited),
| | | /auth/register, /auth/login,
| | | /auth/google (Google OAuth, IP rate limited).
| | | Dual-layer rate limiting: per-email + per-IP
| | |-- roomRoutes.ts # /room/create, /room/join/:room_id, /room/users/:room_id,
| | | GET /room/call-usage (verifyToken,
| | | getCallTimeRemaining), POST /room/call-usage
| | | (verifyToken, reportCallUsage)
| | |-- chatRoutes.ts # /chat/join, /chat/add, /chat/view/:room_id,
| | | POST /chat/bulk-save (verifyToken, bulkSaveChats)
| | |-- companionRoutes.ts # /companion/request, /companion/accept,
| | | /companion/decline, /companion/list, /companion/pending
| | |-- userRoutes.ts # GET /user/profile, PUT /user/profile,
| | | GET /user/search?q= (per-user rate limited)
| | |-- aiRoutes.ts # /ai/ask, /ai/summary (per-user rate limited),
| | | /ai/whiteboard-explain, /ai/whiteboard-summary
| | | (per-user rate limited),
| | | /ai/save-summary (auth required, saves to R2+MongoDB),
| | | /ai/dm-summary (generate DM summary),
| | | POST /ai/summary-qa (RAG Q&A, summaryQaLimiter),
| | | GET /ai/summaries (list saved summaries),
| | | GET /ai/summaries/:id/download (fresh presigned URL),
| | | DELETE /ai/summaries/:id (delete summary)
| | |-- dmRoutes.ts # GET /dm/recent (recent chats aggregation),
| | | GET /dm/unread-counts (before /:companionId to
| | | avoid shadowing), GET /dm/:companionId,
| | | PATCH /dm/:companionId/read
| | |-- notificationRoutes.ts # GET /notifications, PATCH /:id/read,
| | | PATCH /read-all, DELETE /:id
| | |-- newsRoutes.ts # /news
| | |-- roomSessionRoutes.ts # POST /room/session (create or resume session),
| | | GET /room/sessions (list all user sessions),
| | | GET /room/sessions/:sessionRoomId/chats
| | | (get chat history for a session, owner-only).
| | | All routes require verifyToken
| | |-- podcastRoutes.ts # GET /podcasts/:topic -> getPodcasts.
| | No auth required (public content, same as /news)
| |
| |-- models/ # Mongoose schemas (MongoDB only — unstructured data)
| | | All structured models (User, Room, Companion,
| | | Notification, DirectMessage, Chat, UploadCounter,
| | | EmbeddingCounter) have been migrated to PostgreSQL
| | | via Drizzle ORM (see db/ directory below).
| | | Podcast cache migrated to Upstash Redis
| | | (Podcast.ts model deleted)
| | |-- Summary.ts # Persistent summary model (MongoDB): userId, type
| | | ('room'|'dm'|'whiteboard'), contextId, contextLabel,
| | | title, content, r2Key, r2Url, embedding ([Number],
| | | 768-dim vector for RAG). Index on { userId, type,
| | | createdAt: -1 }. Stays in MongoDB because: variable-
| | | length HTML (5KB-50KB+), 768-dim float arrays stored
| | | natively, schema varies by type, no joins needed
| | |-- WhiteboardState.ts # Persistent whiteboard state (MongoDB): roomId
| | | (unique index), elements (Mixed — Excalidraw JSON
| | | array), timestamps. Upserted via whiteboard:save
| | | socket event (save-on-exit). Loaded on whiteboard:join
| | | when in-memory cache is empty (server restart).
| | | Deleted on whiteboard:clear
| |
| |-- db/ # PostgreSQL (NeonDB) via Drizzle ORM + Upstash Redis
| | | All structured relational data lives here
| | | (PostgreSQL). Redis connection also managed
| | | from this directory. Uses UUID v4 primary keys
| | | to preserve the existing string-based auth
| | | contract (JWTs, socket IDs, DM room keys)
| | |-- schema.ts # All Drizzle table definitions + enums + relations.
| | | Tables: users, rooms, roomMembers, companions,
| | | notifications, uploadCounters, embeddingCounters,
| | | directMessages, chats, roomSessions. Enums: companionStatusEnum
| | | ('pending'|'accepted'), notificationTypeEnum
| | | ('companion_request'|'companion_accepted'|
| | | 'room_invite'). Exports inferred types: User,
| | | NewUser, Companion, Notification, DirectMessage,
| | | Chat, UploadCounter, EmbeddingCounter.
| | | users: jsonb for education/projects/workExperience.
| | | companions: unique index on (requester_id,
| | | recipient_id). direct_messages: 3 indexes for
| | | unread badge queries. upload_counters /
| | | embedding_counters: unique constraints for
| | | ON CONFLICT DO UPDATE upserts
| | |-- neon.ts # Lazy singleton NeonDB/Drizzle connection.
| | | getNeonDb() initializes on first call using
| | | DATABASE_URL env var. Uses @neondatabase/serverless
| | | + drizzle-orm/neon-http. Same pattern as
| | | AiController.getClient()
| | |-- redis.ts # Upstash Redis singleton connection. getRedis()
| | | initializes on first call using
| | | UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN
| | | env vars. REST-based client (no TCP pools,
| | | serverless-friendly). Used by: otp.ts (OTP
| | | hashes), PodcastController (cache),
| | | rateLimiter.ts (@upstash/ratelimit)
| | |-- index.ts # Re-exports: getNeonDb from neon.ts, * from schema.ts
| | |-- migrations/ # Drizzle-kit generated SQL migration files.
| | | | Generated by `npx drizzle-kit generate`.
| | | | Applied by `npx drizzle-kit migrate`.
| | | | Drizzle tracks applied migrations via
| | | | __drizzle_migrations table in PostgreSQL
| | | |-- 0001_initial.sql # Initial schema: all tables, indexes, enums
| | |-- queries/ # Thin query helpers, one file per domain.
| | | Each file imports getNeonDb() and schema tables,
| | | exports async functions used by controllers
| | |-- users.ts # findByEmail, findById, createUser, updateUser,
| | | searchUsers
| | |-- rooms.ts # dbCreateRoom, findRoomById, addUserToRoom,
| | | getUsersInRoom
| | |-- companions.ts # findCompanionPair, createCompanion,
| | | acceptCompanionRequest, deleteCompanion,
| | | listAcceptedCompanions, getPendingRequests,
| | | countAcceptedCompanions, getAcceptedCompanionIds,
| | | checkCompanionship
| | |-- notifications.ts # createNotification, listByRecipient, markRead,
| | | markAllRead, deleteById, deleteByTypeAndSender,
| | | deleteExpiredNotifications
| | | (deletes rows older than 10 days — used by cron)
| | |-- directMessages.ts # createDm, getDmHistory, getUnreadCounts,
| | | markDmRead, getDmTranscript, getRecentChats
| | | (SQL CTE + ROW_NUMBER window function —
| | | replaces MongoDB aggregation pipeline)
| | |-- chats.ts # bulkInsertChats, getChatsByRoom, insertChat
| | | (single message insert, used by socket handler)
| | |-- roomSessions.ts # getActiveSession, createSession,
| | | getOrCreateSession, getAllSessions,
| | | getSessionByRoomId. 12h TTL sessions
| | |-- uploadCounters.ts # getUploadCount, incrementUploadCount
| | | (ON CONFLICT DO UPDATE — atomic upsert)
| | |-- embeddingCounters.ts # incrementEmbeddingCount
| | (ON CONFLICT DO UPDATE — atomic upsert)
| |
| |-- scripts/ # One-time migration scripts
| | |-- backfillEmbeddings.ts # Generates vector embeddings for existing
| | summaries that don't have them. Connects
| | to MongoDB, finds summaries with empty
| | embedding arrays, calls generateEmbedding()
| | for each with 1-second delay between calls.
| | Respects daily embedding cap. Run with:
| | npx ts-node src/scripts/backfillEmbeddings.ts
| |
| |-- middlewares/ # Express middleware
| |-- middleware.ts # JWT token verification - extracts userId from
| | Authorization header. Exports AuthenticatedRequest
| |-- rateLimiter.ts # Rate limiting middleware (@upstash/ratelimit,
| Redis-backed, replaced express-rate-limit).
| Sliding window algorithm via Ratelimit.
| slidingWindow(). createLimiter() factory
| builds middleware with prefix, window, max,
| key function. Fail-open: allows requests if
| Redis is down (logged error). Sets standard
| RateLimit-* headers on all responses.
| RATE_LIMIT_CONFIG: every threshold reads from
| env vars via envInt() helper with defaults.
| Exports 8 limiters + config object:
| authEmailLimiter (AUTH_MAX_PER_EMAIL, def 7),
| authIpLimiter (AUTH_MAX_PER_IP, def 15),
| otpEmailLimiter (OTP_MAX_PER_EMAIL, def 5),
| otpIpLimiter (OTP_MAX_PER_IP, def 7),
| aiLimiter (AI_MAX_PER_USER, def 20),
| summaryQaLimiter (SUMMARY_QA_MAX_PER_USER,
| def 10, tighter for RAG Q&A: each call =
| embedding + chat completion),
| searchLimiter (SEARCH_MAX_PER_USER, def 30),
| globalLimiter (GLOBAL_MAX_PER_IP, def 200).
| Also exports EMBEDDING_DAILY_MAX (def 400),
| R2_MAX_UPLOADS_PER_MONTH (def 10),
| and socket throttle intervals (SOCKET_*_MS)
| used by socketServer.ts + SummaryController.ts
|
|-- frontend/ # React 18 + TypeScript + Vite frontend
|-- .eslintrc.cjs # ESLint configuration
|-- .gitignore # Git ignore rules for frontend
|-- package.json # Frontend dependencies and scripts
|-- tsconfig.json # TypeScript configuration
|-- tsconfig.node.json # TypeScript config for Vite/Node tooling
|-- vite.config.ts # Vite bundler configuration
|-- postcss.config.js # PostCSS config (used by Tailwind)
|-- tailwind.config.js # Tailwind CSS config (ESM default export)
|-- components.json # shadcn/ui component configuration
|-- index.html # HTML entry point for Vite
|
|-- public/ # Static public assets
| |-- vite.svg # Vite logo
|
|-- lib/ # Shared utilities (shadcn/ui)
| |-- utils.ts # cn() helper for merging Tailwind classes
|
|-- components/ # shadcn/ui base components (root-level)
| |-- ui/
| |-- button.tsx # Reusable Button component with variants
| |-- card.tsx # Card, CardHeader, CardContent, CardFooter
| |-- input.tsx # Styled Input component
| |-- label.tsx # Form Label component
| |-- select.tsx # Dropdown Select component
|
|-- src/ # Main application source code
|
|-- main.tsx # React app entry point, renders App into DOM.
| Wraps App with GoogleOAuthProvider using
| VITE_GOOGLE_CLIENT_ID env var
|-- App.tsx # Root component - split into AppInner + App.
| JWT rehydration on refresh, mounts overlays,
| Redux Provider + Router wrapping.
| Routes include /sessions (SessionsPage, auth-guarded)
|-- App.css # Global app styles
|-- index.css # Tailwind directives and base styles
|-- vite-env.d.ts # Vite TypeScript type declarations
|
|-- assets/ # Static images and media
| |-- logo.svg # App logo
| |-- group-img1.svg # Landing page illustration
| |-- login-back.jpg # Auth page background image
| |-- active.gif # Recording active indicator animation
| |-- notactive.png # Recording inactive indicator
| |-- react.svg # React logo
|
|-- pages/ # Top-level page components (one per route)
| |-- LandingPage.tsx # Hero page with animated CTAs
| |-- AuthPage.tsx # Auth layout with Login/Register tabs.
| | Two auth methods: OTP email flow and
| | single-click Google OAuth ("Continue with
| | Google" button via useGoogleLogin hook).
| | OTP flow: Step 1 (email + name for register)
| | → Step 2 (6-digit OTP input). Resend OTP
| | with 30s countdown, back button. Sends
| | only { email, otp } to backend (no hash/
| | expires — Redis handles OTP state server-side).
| | Google flow: gets access_token → POST
| | /auth/google → JWT + login. Calls
| | /auth/send-otp then /auth/login or
| | /auth/register with OTP verification
| |-- RoomPage.tsx # Home dashboard with:
| | - Global people search bar (blurred dummy
| | preview when logged out, live search when
| | logged in with Add companion buttons,
| | optimistic UI on Add/Accept with loaders)
| | - Companion requests section (accept/decline
| | cards, fetched from /companion/pending,
| | real-time via companion:requestReceived,
| | success toast with sound on accept)
| | - Study companions bar with online/offline
| | dots and green ring for unread DMs.
| | unreadDmFrom restored from GET /dm/unread-counts
| | on mount (survives refresh)
| | - "Ready to study?" CTA card: enterMyRoom is
| | async, calls POST /room/session to create
| | or resume a 12h session
| | - News feed with category filters, controlled
| | by VITE_NEWS_IMAGES_ONLY env var
| | - Add Companion modal with user search
| | - DM panel integration
| | - inviteCompanion uses session roomId from Redux
| |-- ProfilePage.tsx # User profile page - gradient banner, large
| | avatar circle (emoji or initials), editable
| | name/bio, email display, companion count.
| | Default avatar picker: 5 themed avatars
| | (camera icon opens modal, instant save to
| | backend + Redux updateAvatar). Uses shared
| | DEFAULT_AVATARS from utils/avatars.ts.
| | Education section (GraduationCap icon):
| | degree, institution, year with inline edit.
| | Projects section (FolderGit2 icon): up to
| | 2 projects with title, description, link.
| | Add/remove in edit mode. View shows mini
| | cards with "View" link pill (ExternalLink).
| | Work Experience section (Briefcase icon):
| | company, role (indigo badge), duration,
| | description. All sections share global
| | editing toggle with staggered Framer Motion
| | animations. Calls GET/PUT /user/profile,
| | re-issues JWT
| |-- SettingsPage.tsx # Settings page at /settings. Sections:
| | Appearance (dark mode toggle), Notifications
| | (push + sound toggles), General (language,
| | privacy), About (version). Account actions:
| | Edit Profile + Log Out. Premium design with
| | staggered Framer Motion animations, decorative
| | blobs, grouped cards. Auth-guarded with JWT
| | rehydration race condition handling
| |-- ChatsPage.tsx # WhatsApp-style recent chats list. Fetches
| | GET /dm/recent on mount. Each row: companion
| | avatar, name, last message preview, timestamp,
| | unread badge. Click opens DmPanel. Real-time
| | updates via dm:receive socket event
| |-- RoomCallPage.tsx # Room lobby + video call page. Agora opt-in:
| | video call only starts on "Start Video
| | Call" button click (saves Agora hours).
| | Tab panel: Chat / AI Doubt / Whiteboard.
| | Whiteboard tab navigates to full-page
| | /whiteboard/:roomId. Bottom action bar
| | with Invite + Summary buttons (Summary
| | only on Chat tab, disabled when no user
| | messages). Agora App ID from env var.
| | RoomId resolution: router state >
| | Redux > user's personal roomId.
| | Channel name: roomId.slice(0, 64)
| | (Agora's 64-byte channel name limit).
| | Auto-publish: useEffect sequentially
| | publishes video then audio when isInCall
| | becomes true (avoids concurrent
| | client.publish conflicts).
| | Track lifecycle: module-level videoTrack
| | and audioTrack vars. leaveChannelInternal
| | calls .close() on both and nulls them
| | out to prevent stale track reuse.
| | turnOnCamera re-plays track to DOM
| | element on enable (rebinds after
| | component remount).
| | Remote user: hasRemoteUser state via
| | user-joined/user-left events.
| | user-unpublished resets isVideoSubed
| | (shows avatar when camera turns off).
| | Companion name from chatMessages
| | (first non-bot, non-self sentby).
| | Call usage: fetches remainingSeconds
| | from GET /room/call-usage before join,
| | syncs deltaSeconds via POST during call
| | and on page unload (keepalive fetch).
| | Themed toast on daily limit reached.
| | Chats persisted in real-time via
| | insertChat in socket handler.
| | Safari fix: absolute positioning for
| | tab content + bottom bar
| |-- WhiteboardPage.tsx # Full-page collaborative whiteboard at
| | /whiteboard/:roomId. Lazy-loads
| | @excalidraw/excalidraw via React.lazy().
| | Real-time sync via Socket.IO (debounced
| | 200ms). Built-in AI Assist sidebar (360px,
| | Framer Motion spring) with "Explain This"
| | + custom questions. Toolbar: Back to Room,
| | Clear, Summary (generates + saves whiteboard
| | summary), AI Assist toggle. Simplifies
| | elements for AI payloads (type + text +
| | dimensions)
| |-- SummariesPage.tsx # Dedicated summaries page at /summaries.
| | RAG Q&A panel (collapsible, above tabs):
| | text input + suggestion chips, calls
| | POST /ai/summary-qa, displays AI answer
| | with source citation badges. Error/rate
| | limit handling with amber warning style.
| | 3 sub-tabs: Room Chat, DM Chat, Whiteboard.
| | Fetches GET /ai/summaries?type= on mount
| | and tab switch. Summary cards with type
| | badge, title, date, expandable content,
| | delete button. Follows ChatsPage layout
| |-- SessionsPage.tsx # Dedicated session history page at /sessions.
| | Lists all past room sessions with collapsible
| | chat history. Fetches GET /room/sessions and
| | GET /room/sessions/:sessionRoomId/chats.
| | Owner-only access. Auth-guarded
| |-- RadioPage.tsx # Study Radio — full-page radio player.
| | Displays 8 SomaFM channel cards with
| | play/pause, volume slider, and canvas-based
| | audio visualizer. Uses RadioContext for
| | global playback state
| |-- PodcastsPage.tsx # Podcast discovery + in-house playback at /podcasts.
| | 5 topic tabs (Trending, AI, Tech, Business,
| | Productivity & Tools). Lazy-fetches per tab
| | via GET /podcasts/:topic; component-state cache
| | prevents re-fetch on tab revisit. Framer Motion
| | AnimatePresence + layoutId sliding tab highlight.
| | Refresh banner with pulsing dot: "Fresh drops
| | every Tue & Sat — stay ahead of the curve."
| | PodcastCard: 3px accent strip, thumbnail (Mic2
| | fallback), title (line-clamp-2), publisher,
| | description (line-clamp-3), listen score badge
| | (Star, amber), episode count, "Play" button
| | (teal highlight when active, Pause icon) that
| | starts in-house playback via PodcastPlayerContext;
| | falls back to external link when no audio URL.
| | handlePlay: toggle play/pause if same track,
| | else playTrack() with audio URL + metadata.
| | SkeletonGrid (8 ghost cards, animate-pulse).
| | Amber notice badge for stale-cache/mock source.
| | No Redux — podcast state via usePodcastPlayer()
| |-- JoinRoomPage.tsx # Auth-gated join redirect at /join/:roomId.
| | If authenticated: dispatches enterRoom and
| | navigates to /room/call, passes roomId in
| | navigation state. If not: stores roomId in
| | sessionStorage, redirects to /login,
| | auto-joins after auth. Invite links are
| | session-based (roomId includes session UUID)
| |-- Streampage.tsx # Live streaming page - camera preview, YouTube
| | stream key input, start/stop controls
| |-- AskAiPage.tsx # AI voice input page - mic component with
| | P5.js audio visualization
| |-- DemoAiPage.tsx # AI demo page - audio recording and upload
| interface with message display
|
|-- components/ # Reusable React components
| |-- Login.tsx # Login form (legacy, unused — AuthPage handles
| | OTP-based login directly)
| |-- Register.tsx # Register form (legacy, unused — AuthPage handles
| | OTP-based registration directly)
| |-- Navbar.tsx # Collapsible sidebar (not top bar) - hamburger
| | toggle, framer-motion spring animation.
| | Nav items: Home, Chats, Summaries, Sessions
| | (Clock icon, /sessions), My Room, Ask AI,
| | Study Radio, Podcasts, Contact us.
| | Active route highlighting.
| | Profile avatar (top-right): shows emoji avatar
| | with matching gradient when set, initials when
| | not. Uses getAvatarById() from utils/avatars.ts.
| | Dropdown: My Profile, Settings, My Room, Ask AI,
| | Logout. Login icon shown when logged out.
| | Logout: clears localStorage token, disconnects
| | socket, resets Redux, redirects to /login
| |-- NavbarCall.tsx # Minimal navbar for in-call view. Exit button
| | uses onExitClick callback (no <Link>)
| |-- SaveChatPrompt.tsx # [No longer used — import removed from RoomCallPage]
| | Previously: animated modal overlay for "Save your
| | chats?" prompt. Chats are now always persisted in
| | real-time via insertChat in the socket handler
| |-- ChatComponent.tsx # Real-time chat widget - uses getSocket()
| | singleton, room-based messaging, lifts messages
| | to parent via onMessagesChange prop.
| | Socket join retry mechanism and reconnect
| | handler for reliability. Fetches chat history
| | on mount. Uses sentById field for correct
| | sender/receiver styling.
| | Linkify helper renders URLs in bot messages as
| | clickable links. Summary bot messages get special
| | violet styling distinct from regular bot messages.
| | Chats persist in real-time (no manual save needed)
| |-- DmPanel.tsx # Slide-in DM panel from right. Optimistic send
| | (tempId), delivery states: sending/delivered/read.
| | Tick icons: clock=sending, ✓=delivered, ✓✓=read.
| | Emits dm:markRead on open (REST + socket).
| | Filters dm:receive to current conversation only.
| | Listens for dm:readUpdate to upgrade ticks.
| | Summary icon button (FileText) in header
| | generates DM summary via generateAndSaveSummary
| |-- NotificationBell.tsx # Bell icon with unread badge in Navbar (top-right).
| | Fetches GET /notifications on mount.
| | Listens for notification:new socket events
| | (socket listener dependency fix for reconnects).
| | Dropdown: type icons, relative timestamps, mark
| | read on click, bulk "Mark all read", inline delete.
| |-- AiPanel.tsx # AI doubt solver panel (text + voice via
| | Web Speech API), Q&A history cards with
| | generic "AI Assistant" branding. Full
| | dark mode support: input field, Q&A
| | bubbles, loading states, mic button,
| | borders all have dark: variants
| |-- ChatTabPanel.tsx # Extracted chat tab for RoomCallPage. Wraps
| | ChatComponent with room member list header
| | (fetches companion list), DM panel integration,
| | and inline save callback passthrough.
| | Safari fix: replaced h-full with flex-1
| | min-h-0 for correct layout
| |-- WhiteboardPanel.tsx # Reusable whiteboard wrapper component.
| | Imports @excalidraw/excalidraw directly.
| | Real-time sync via Socket.IO: debounced
| | onChange (150ms) emits whiteboard:update,
| | listens for whiteboard:sync. isRemoteUpdate
| | ref prevents echo loops. Lifts simplified
| | elements to parent via onSceneChange.
| | Accepts initialData for state restoration
| |-- RadioVisualizer.tsx # Canvas-based audio frequency bar visualizer.
| | Uses Web Audio API AnalyserNode for real-time
| | FFT data. Two variants: "full" (tall bars on
| | RadioPage) and "mini" (compact for MiniPlayer).
| | Falls back to CSS animated bars when analyser
| | returns zero data
| |-- MiniPlayer.tsx # Floating bottom-right mini radio player.
| | Visible on all pages except /radio when a
| | channel is playing. Play/pause, volume,
| | mute, expand to full page, close controls.
| | Uses RadioContext for shared audio state
| |-- PodcastMiniPlayer.tsx # Fixed bottom podcast player (z-50, above
| | radio z-40). Teal/emerald gradient theme.
| | Thumbnail, track title + publisher, clickable
| | progress bar with hover seek thumb, time
| | display (mm:ss / mm:ss), play/pause with
| | loading spinner, volume + mute, close/stop.
| | Framer Motion spring entry/exit. Uses
| | PodcastPlayerContext for shared audio state
| |-- InviteNotificationOverlay.tsx # Animated banner for incoming room
| | invites. Accept navigates to /room/call,
| | Decline dismisses. Socket: receiveInvite
| |-- CompanionRequestOverlay.tsx # Queue-based overlay for companion
| | requests. Accept calls POST /companion/accept
| | + emits companion:acceptNotify
| |-- Messages.tsx # Placeholder message component for AI demo page
| |-- Stream.tsx # Video call UI — 2-up grid (local + remote).
| | Local: #camera-video element, hidden when
| | isVideoOn=false, "You" label with speaking
| | indicator. Remote: #remote-video element,
| | hidden when isVideoSubed=false. When remote
| | user connected but camera off: shows initials
| | avatar (gradient circle) + name + "Camera
| | off" label. When no remote user: gray
| | "Waiting..." placeholder. Name label overlay
| | on remote tile (visible with/without video).
| | Controls: camera toggle, mic toggle, publish
| | (MonitorUp), end call (PhoneOff).
| | useVoiceActivity hook: Web Audio API
| | AnalyserNode for real-time speaking detection
| | (green ring + pulsing dot). Props:
| | hasRemoteUser, remoteUserName, onEndCall
| |
| |-- shared/ # Shared/utility components
| |-- BackgroundImage.tsx # Background image wrapper component
| |-- Emoji.tsx # Emoji renderer component
| |-- MicComponent.tsx # P5.js microphone visualization - real-time
| | audio bars, toggle mic on/off
| |-- Recorder.tsx # MediaRecorder component - records audio as
| | webm blob, upload callback, status indicator
| |-- SteamElement.tsx # Stream element placeholder (empty)
| |-- sound-bar.tsx # Visual sound level indicator
| |-- roomUtils.ts # Room utility functions
|
|-- store/ # Redux Toolkit state management
| |-- authStore/
| | |-- store.ts # Combined store config - 5 reducers: auth, room,
| | | invite, companion, notification. Exports RootState,
| | | AppDispatch, AuthState (backwards-compat alias)
| | |-- authSlice.ts # Auth slice: login/logout/updateName/updateAvatar.
| | AuthUser: { name, userId, roomId, avatar }
| |-- RoomStore/
| | |-- store.ts # Room store (legacy, unused - store is combined)
| | |-- roomSlice.ts # Room slice: { currentRoomId, isOwner }.
| | Actions: enterRoom, leaveRoom
| |-- inviteStore/
| | |-- inviteSlice.ts # Invite slice: { pendingInvite }.
| | Actions: receiveInvite, clearInvite
| |-- companionStore/
| | |-- companionSlice.ts # Companion slice: { companions, pendingRequests }.
| | Actions: setCompanions (preserves isOnline),
| | setOnline, setOffline, setPendingRequests,
| | addPendingRequest, removePendingRequest,
| | addCompanion
| |-- notificationStore/
| |-- notificationSlice.ts # Notification slice: { items: AppNotification[] }.
| Actions: setNotifications, addNotification,
| markOneRead, markAllReadLocal, removeNotification.
| AppNotification: { _id, type, fromUserId,
| fromUserName, data, read, createdAt }
|
|-- context/ # React context providers
| |-- RadioContext.tsx # Global radio playback context. useReducer
| | for state (currentChannel, isPlaying,
| | volume, isMiniPlayerEnabled). Manages
| | HTMLAudioElement + Web Audio API AnalyserNode.
| | State persisted to sessionStorage. Exports
| | RadioProvider + useRadio() hook
| |-- PodcastPlayerContext.tsx # Global podcast audio context. useReducer
| for state (currentTrack, isPlaying, volume,
| currentTime, duration, isLoading). Native
| HTMLAudioElement with timeupdate/loadedmetadata/
| ended/waiting/canplay event listeners. Supports
| seeking (seekTo), progress tracking, volume.
| Stops radio (calls radio.stop()) when a podcast
| starts — mutual exclusion. Volume persisted to
| localStorage (key: study-podcast-player). Exports
| PodcastPlayerProvider + usePodcastPlayer() hook
|
|-- data/ # Static data files
| |-- radioChannels.ts # 8 curated SomaFM radio channels (Groove Salad,
| Drone Zone, Lush, Space Station Soma, DEF CON,
| Boot Liquor, Fluid, Groove Salad Classic).
| Each: id, name, genre, description, streamUrl,
| emoji, gradient color
|
|-- utils/ # Utility functions
| |-- socketInstance.ts # Socket.IO singleton - connectSocket(token),
| | getSocket(), disconnectSocket(). All components
| | share one connection with JWT auth
| |-- avatars.ts # Shared default avatar constants. 5 themed
| | avatars (Cool Guy, Scholar, Scientist, Artist,
| | Astronaut) with id, label, gradient, emoji, bg.
| | Exports DEFAULT_AVATARS array + getAvatarById()
| | helper. Used by ProfilePage + Navbar
| |-- summaryApi.ts # Shared generate+save summary utility.
| | generateAndSaveSummary(genEndpoint, genPayload,
| | saveParams) — calls AI endpoint, then saves
| | to R2+MongoDB via /ai/save-summary. Used by
| | RoomCallPage, WhiteboardPage, and DmPanel
| |-- useDarkMode.ts # Dark mode hook - toggles 'dark' class on
| | document, persists preference
| |-- roomCallUtils.ts # Room call utilities (empty, placeholder)
|
|-- helpers/ # Helper scripts
|-- p5sound_fix.js # P5.js sound library compatibility fix
ENV CONFIGURATION
=================
backend/.env -> DATABASE_URL (NeonDB PostgreSQL connection string),
MONGODB_URI (MongoDB — summaries only),
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
(Upstash Redis — OTP cache, podcast cache, rate limiting),
PORT, JWT_SECRET, OTP_SECRET, OTP_EXPIRY_MINUTES,
AI_PROVIDER, GEMINI_API_KEY, GEMINI_EMBEDDING_MODEL, GROK_API_KEY, OPENROUTER_API_KEY,
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME,
R2_MAX_UPLOADS_PER_MONTH, AGORA_APP_ID,
LISTEN_NOTES_API_KEY,
RESEND_API_KEY, RESEND_FROM_EMAIL (Resend — replaced SMTP_*),
+ Rate limiting env vars (all optional with defaults):
AUTH_WINDOW_MIN, AUTH_MAX_PER_EMAIL, AUTH_MAX_PER_IP,
OTP_WINDOW_MIN, OTP_MAX_PER_EMAIL, OTP_MAX_PER_IP,
AI_WINDOW_MIN, AI_MAX_PER_USER,
SUMMARY_QA_WINDOW_MIN, SUMMARY_QA_MAX_PER_USER,
EMBEDDING_DAILY_MAX, SEARCH_WINDOW_MIN,
SEARCH_MAX_PER_USER, GLOBAL_WINDOW_MIN, GLOBAL_MAX_PER_IP,
SOCKET_DM_INTERVAL_MS, SOCKET_COMPANION_REQ_INTERVAL_MS,
SOCKET_INVITE_INTERVAL_MS
frontend/.env -> VITE_API_URL, VITE_STREAM_SOCKET_URL, VITE_NEWS_IMAGES_ONLY, VITE_AGORA_APP_ID,
VITE_GOOGLE_CLIENT_ID
API ROUTES SUMMARY
==================
Auth (OTP-based + Google OAuth, passwordless):
POST /auth/send-otp Send 6-digit OTP to email (HMAC hash + expiry returned)
POST /auth/register Verify OTP + register new user (returns token, name, userId)
POST /auth/login Verify OTP + login (returns token, name, userId)
POST /auth/google Google OAuth sign-in/register (validates access_token, returns token)
Rooms:
POST /room/create Create room or add user to existing room
POST /room/join/:room_id Join a room by ID
GET /room/users/:room_id Get all users in a room
Room Sessions (12h TTL):
POST /room/session Create or resume a session (returns session roomId)
GET /room/sessions List all sessions for current user
GET /room/sessions/:sessionRoomId/chats Get chat history for a session (owner-only)
Chat:
GET /chat/join Get logged-in user's name from JWT
POST /chat/add Send a chat message (emits via Socket.IO)
GET /chat/view/:room_id Get chat history for a room
POST /chat/bulk-save Bulk-save room chat messages (auth, max 500, sessionId grouping)
Companions:
POST /companion/request Send companion request
POST /companion/accept Accept companion request
POST /companion/decline Decline companion request
GET /companion/list Get accepted companions
GET /companion/pending Get pending requests
Users:
GET /user/profile Get authenticated user's profile (name, email, bio, avatar, education, projects, workExperience, companionCount)
PUT /user/profile Update profile (name, bio, avatar, education, projects, workExperience) — re-issues JWT
GET /user/search?q= Search users by name/email
AI (Gemini / Grok / OpenRouter — switchable via AI_PROVIDER env var):
POST /ai/ask Ask AI a study question
POST /ai/summary Generate AI summary of chat messages
POST /ai/whiteboard-explain AI analysis of whiteboard drawing (+ optional question)
POST /ai/whiteboard-summary Generate AI summary of whiteboard content
POST /ai/save-summary Save summary to Cloudflare R2 + MongoDB, broadcast link
POST /ai/dm-summary Generate AI summary of DM conversation
POST /ai/summary-qa RAG Q&A: embed question, cosine similarity search, AI answer with sources
GET /ai/summaries List saved summaries (filter by ?type=room|dm|whiteboard)
DELETE /ai/summaries/:id Delete a saved summary (ownership check)
DMs:
GET /dm/recent Get recent chats (last message per companion, sorted by time)
GET /dm/unread-counts Get unread message count per companion (for badge restore)
GET /dm/:companionId Get DM history (last 50, includes _id + read state)
PATCH /dm/:companionId/read Mark all messages from companion as read
Notifications:
GET /notifications Get all notifications for current user (last 50)
PATCH /notifications/:id/read Mark a single notification as read
PATCH /notifications/read-all Mark all notifications as read
DELETE /notifications/:id Delete a notification
News:
GET /news Get mock news feed articles
Podcasts:
GET /podcasts/:topic Get podcasts for topic (trending|ai|tech|business|productivity).
No auth. Upstash Redis cache per topic (key podcast:{topic},
4-day TTL). Cache valid until last Tue or Sat midnight
(lazy refresh). node-cron pre-warms cache at 02:00 UTC
every Tue & Sat. Audio enrichment: iTunes Search API →
RSS feed parsing → direct MP3 URLs from podcast CDN.
Response: { data: PodcastItem[], fetchedAt, source }.
PodcastItem includes audio (MP3 URL | null),
audioLengthSec, latestEpisodeTitle for in-house playback
SOCKET EVENTS
=============
Room Chat:
joinRoom Client joins a room (emits bot welcome)
serverMessage Client sends message (broadcast to room)
message:{roomId} Server sends message to room
Invites (companion-gated, throttled 3s):
sendInvite Client invites a companion to their room
receiveInvite Server notifies target of invite
acceptInvite Client accepts invite (joins room)
declineInvite Client declines invite
inviteError Server reports invite failure or throttle
Companions (sendRequest throttled 5s):
companion:sendRequest Client sends companion request
companion:error Server reports companion request throttle
companion:requestReceived Server notifies target of request
companion:acceptNotify Client notifies requester of acceptance
companion:accepted Server confirms acceptance to requester
companion:online Server broadcasts user online
companion:offline Server broadcasts user offline
companion:getOnlineCompanions Client requests online companion IDs
companion:onlineList Server responds with list of online companion IDs
Whiteboard (whiteboard:update throttled 100ms):
whiteboard:update Client broadcasts drawing changes to room (roomId + elements)
whiteboard:sync Server relays whiteboard changes to other room participants
whiteboard:clear Client clears the whiteboard for all room participants
whiteboard:cleared Server notifies room that whiteboard was cleared
DMs (dm:send throttled 200ms):