This document tracks the current backend implementation for auth, RBAC, announcements, notification fan-out, and read tracking.
Primary files:
- convex/schema.ts
- convex/users.ts
- convex/lib/auth.ts
- convex/lib/rbac.ts
- convex/announcements.ts
- tests/convex/*.test.ts
- Fields: clerkId, firstName, lastName, email, role, department, enrollmentNumber, employeeId, updatedAt
- Indexes: by_clerk_id, by_email, by_department, by_enrollmentNumber, by_employeeId
- Fields: title, content, authorId, targetRoles, updatedAt
- Indexes: by_authorId, by_updatedAt
- Fields: userId, type, content, isRead, relatedId
- Indexes: by_userId, by_type, by_user_unread
- Fields: userId, announcementId, readAt
- Indexes: by_user, by_announcement, by_user_announcement
The by_user_announcement index is used for fast idempotent read lookup and conflict recovery.
File: convex/lib/auth.ts
Behavior:
- Reads Convex identity from ctx.auth.getUserIdentity()
- Throws Unauthorized when no identity exists
- Loads user doc using users.by_clerk_id
- Throws User not found if identity has no matching user row
File: convex/lib/rbac.ts
Behavior:
- APP_ROLES: student, faculty, admin
- requireRole(user, allowedRoles) throws Forbidden for disallowed roles
- normalizeTargetRoles(targetRoles) enforces non-empty and deduplicates values
File: convex/announcements.ts
- Auth required via requireUser
- Role gate: admin or faculty
- Validates title/content are non-empty after trim
- Validates and deduplicates targetRoles
- Inserts announcement with authorId set from authenticated user only
- Generates notifications for users whose role is in targetRoles (excluding author)
- Auth required
- Loads announcements by by_updatedAt descending
- Filters to rows where targetRoles includes current user role
- Enriches each row with read metadata from announcementReads
- Auth required
- Rejects if announcement does not exist
- Rejects if announcement is not visible to user role
- Uses by_user_announcement and canonicalization to ensure one stable read row per user-announcement pair
- If concurrent inserts happen, duplicates are cleaned up and a canonical id is returned
- Auth required
- Role gate: admin only
- Rejects if announcement does not exist
- Deletes related rows in announcementReads
- Deletes related announcement notifications
- Deletes announcement row
Concurrency target: multiple near-simultaneous markAnnouncementRead calls for the same user and announcement should converge to one final read row.
Current strategy:
- Query by by_user_announcement.
- If existing rows are found, pick canonical first row and remove duplicates.
- If not found, insert a row, then re-query and canonicalize again.
This provides deterministic return behavior and cleanup under concurrent calls.
Test framework: Vitest
Location:
- tests/convex/auth.test.ts
- tests/convex/rbac.test.ts
- tests/convex/announcements.test.ts
Covered scenarios:
- Auth helper: missing identity, missing user, success
- RBAC helper: allow, deny, role normalization validation
- createAnnouncement: admin success, student forbidden
- getAnnouncements: role filtering and read state mapping
- markAnnouncementRead: sequential idempotency, role rejection, concurrent race scenario
- deleteAnnouncement: admin cleanup path, non-admin rejection
Run:
pnpm test
pnpm test:watch
pnpm -s tsc --noEmit- pnpm install
- pnpm convex dev
- pnpm dev
- pnpm test
Keep pnpm convex dev running while editing Convex schema or functions so generated types stay in sync.