Anaya Care Docs

Content & Community (Posts, Blog, Trainings, Skills, Ratings, Feature Requests)

Part of the Anaya Care product wiki. See 00-overview.md.

Purpose

This module groups the "content" and "community" features of the platform:

  • Posts — a per-business social feed (text, image/video attachments, hashtags, mentions) with likes, comments, threaded replies, and bookmarks, plus automated birthday celebration posts (apps/backend/src/posts/).
  • Blog — a platform-global, publicly readable marketing/resources blog with AI-assisted bulk authoring (OpenAI content + Gemini cover images) (apps/backend/src/blog/).
  • Trainings — a full LMS: training courses with sections, videos, AI-generated slides/videos/assessments, learner progress, assignments with due dates, certificates with public verification, and a platform catalog that businesses can fork ("avail") (apps/backend/src/trainings/).
  • Skills — a flat global skill catalog referenced by care provider profiles and job postings (apps/backend/src/skills/).
  • Ratings — star ratings (0–5) of care providers by other users, feeding an average-rating field on the care provider profile (apps/backend/src/ratings/).
  • Feature Requests — business users submit product ideas; SuperAdmin triages them through a status pipeline (apps/backend/src/feature-requests/).

Entities & Data Model

Posts module

EntityCollectionSource
Postpostsapps/backend/src/posts/entities/post.entity.ts
Commentcommentsapps/backend/src/posts/entities/comment.entity.ts
Likelikesapps/backend/src/posts/entities/like.entity.ts
Bookmarkbookmarksapps/backend/src/posts/entities/bookmark.entity.ts

Post key fields (post.entity.ts):

FieldTypeNotes
businessObjectId → Businessrequired, indexed — feed is tenant-scoped
contentstringrequired
authorObjectId → Userrequired
attachments{type: 'image'|'video', url, blurhash?, metadata?}[]embedded
hashtagsstring[]indexed
mentionsObjectId[] → Userindexed; triggers mention notifications
likesCount / commentsCount / bookmarksCountnumberdenormalized counters
typePostType (regular | birthday)default regular
celebrantObjectId → Useronly on birthday posts
isDeleted / deletedAt / deletedBysoft delete

Comment key fields: business, content, author, post, parentComment? (one nesting level), replies[], repliesCount (max 500, enforced in a pre-save hook and again in createCommentcomment.entity.ts, posts.service.ts:840-846), likesCount, soft-delete fields.

Like: polymorphic via target + targetType (Post|Comment, Mongoose refPath). Unique index on {user, target, targetType}.

Bookmark: {user, post} unique index; posts only.

Blog module

EntityCollectionSource
BlogPostblog_postsapps/backend/src/blog/entities/blog-post.entity.ts

Key fields: title (≤200), slug (unique, lowercase), content (Markdown), excerpt (≤500), coverImage {url, alt?, blurhash?}, author → User, status (BlogPostStatus: draft/published/archivedpackages/shared/src/enums/domain-status.ts:648), tags[], publishedAt, isFeatured, isDeleted, seoMeta {title?, description?}, coverImagePrompt, readingTimeMinutes (computed at ~200 wpm from markdown-stripped content — blog.service.ts:14-23), viewCount. No business field — blog is platform-global and the controller is @SkipBusinessScope() (blog.controller.ts:38).

Trainings module

EntityCollectionSource
Trainingtrainingsapps/backend/src/trainings/entities/training.entity.ts
TrainingProgresstraining_progresstraining-progress.entity.ts
TrainingAssignmenttraining_assignmentstraining-assignment.entity.ts
TrainingCertificatetraining_certificatestraining-certificate.entity.ts
BusinessTrainingbusiness_trainingsbusiness-training.entity.ts (audit-only fork log)
TrainingGenerationJobtraining_generation_jobstraining-generation-job.entity.ts

Training key fields: title, description, coverPhoto(+Blurhash, +ImagePrompt), photoBank[] (reference images for AI slide image generation, max 10 per a comment — not enforced in schema), sections[] (embedded; each with id (uuid), title, content, order, optional assessment {questions (multiple_choice/yes_no), passingScore, maxAttempts?, required}, contentType (none/video/slides), unified videoType/videoKey/videoBlurhash (S3 keys only; signed URLs generated on demand), slides[] with speechScript, video-generation tracking + captionKey), status (TrainingStatus), business (ObjectId or null = platform training), sourceTrainingId (provenance for catalog forks), isPublic, isDeleted, review fields (submittedForReviewAt, reviewedBy, reviewedAt, rejectionReason), createdBy, generation progress fields.

TrainingProgress: {training, user} unique; sectionProgress[] with per-section isCompleted, assessmentAttempts[] (responses, score 0–100, passed), best assessmentScore, assessmentPassed; overallProgress (0–100); startedAt/lastAccessedAt/completedAt.

TrainingAssignment: business (required), training, user, assignedBy, assignedAt, dueDate?, currentStatus (TrainingAssignmentStatus: ASSIGNED/IN_PROGRESS/COMPLETED/OVERDUE), append-only statusHistory[] (with isSystemAction), links to trainingProgress and certificate, soft delete. Unique {training, user} where isDeleted: false.

TrainingCertificate: certificateNumber unique, format CERT-YYYYMM-XXXXXX from a confusion-free charset (training-certificate.service.ts:33-66), pdfKey (S3), rich metadata (overall score, per-section assessment results, time spent), verificationCount/lastVerifiedAt for public verification.

BusinessTraining: audit row per catalog fork — {business, sourceTraining, clonedTraining, availedBy, availedAt}. Explicitly "no longer the source of truth for visibility" (entity header comment); no uniqueness, re-avail produces a fresh fork each time.

TrainingGenerationJob: KB-driven AI generation job — selectedDocuments[] → KnowledgeBaseDocument, promptOptions (audience/tone/objectives/focusAreas, 3–12 sections, 200–1500 words/section), status (TrainingGenerationStatus, 12 values incl. searching/generating/self_checking/refiningdomain-status.ts:500), currentIteration/maxIterations (default 5), currentDraft, iterationLogs[], accumulatedContext[], autoGenerationOptions/Results (cover photo, slides, videos, assessments), requestedBy.

Skills, Ratings, Feature Requests

Skill (skills collection, apps/backend/src/skills/entities/skill.entity.ts): just name (unique) + description?. No business field — one global catalog shared by all tenants.

Rating (ratings collection, apps/backend/src/ratings/entities/rating.entity.ts): business (required, auto-injected by the global business-scope Mongoose plugin since the service never sets it — apps/backend/src/common/plugins/business-scope.plugin.ts), user (the rated care provider), rater, score (0–5), comment?, isVerified (default false), verifiedAt?, verifiedBy?.

FeatureRequest (feature_requests collection, apps/backend/src/feature-requests/entities/feature-request.entity.ts): business, submittedBy, title (≤200), description (≤5000), category (FeatureRequestCategory, 9 values — domain-status.ts:1059), status (FeatureRequestStatus: New / Under Review / Planned / In Progress / Completed / Declined — domain-status.ts:1047), platformNotes (≤5000, SuperAdmin-only).

Workflows & State Machines

Social feed posts

  • Create/read: any authenticated user in a business hits POST /posts / GET /posts (posts.controller.ts). The feed is hard-scoped to the caller's business and uses cursor pagination (_id < cursor, newest first) with $lookups for author, mentions, per-viewer isLiked/isBookmarked (posts.service.ts:69+).
  • Update/delete: only the author can update or soft-delete their own post — the query filters author: userId (posts.service.ts:437-447, 468-477). There is no admin/moderator override path in code.
  • Reactions: single "like" toggle on posts and comments (no reaction types). Unique like per user/target; denormalized likesCount via $inc. Liking a post notifies the author (posts.service.ts:541-546PostNotificationService.handlePostLike).
  • Comments: one level of nesting (parentCommentId); replies increment repliesCount on the parent (cap 500) instead of the post's commentsCount (posts.service.ts:860-885). Top-level comments notify the post author; replies notify the parent-comment author; post mentions notify mentioned users (posts.service.ts:57-65).
  • Bookmarks: per-user toggle + GET /posts/bookmarks list.
  • Birthday celebration posts (the "celebrations" feature): a cron at 9:00 AM America/Los_Angeles daily (posts/tasks/birthday.tasks.ts, @Cron('0 9 * * *')) finds active users whose birthday is today (Feb 29 celebrated Feb 28 in non-leap years — birthday.service.ts:55-80), skips users with no business, dedupes one birthday post per celebrant per day, creates a type: birthday post authored by the celebrant with a random canned message and hashtags ['birthday','celebration'] (birthday.service.ts:createBirthdayPost), then sends a BirthdayToday unified (in-app + push) notification to all active users in the same business with link /dashboard/posts/{postId} (birthday.tasks.ts:sendBirthdayNotificationToAll).
  • Read endpoints: GET /posts/birthdays/today (today's birthday posts), /upcoming?days=, /range?pastDays=&futureDays= (default −15/+30, bucketed into recent/today/upcoming), and a diagnostic GET /posts/birthdays/debug/:username (posts.controller.ts:80-115).
  • The showBirthdayInFeed user privacy flag exists on User (users/entities/user.entity.ts:207) and is toggleable in mobile (apps/mobile/components/ui/birthday-privacy-toggle.tsx), but all three birthday queries have the filter commented out with // TODO: Re-enable when UI toggle is added (birthday.service.ts:88, 137, 215) — the toggle currently has no effect.

There is no post status machine — posts are live immediately; no draft/approval/flagging flow exists in the backend (despite POSTS_FLAG/POSTS_MODERATE permissions existing, see Gaps).

Blog authoring & publishing

  • Admin endpoints (POST /blogs, GET /blogs/admin/list, GET /blogs/admin/:id, PATCH /blogs/:id, DELETE /blogs/:id, DELETE /blogs/all) require POSTS_MANAGE with a real controller-level PermissionsGuard (blog.controller.ts:48-...). Note blog reuses the posts permission; there is no dedicated blog permission.
  • Public endpoints (@Public()): GET /blogs/posts (published only), /posts/featured (default 3), /posts/tags, /posts/:slug (increments viewCount on every fetch — blog.controller.ts:findBySlug).
  • AI bulk generation: POST /blogs/bulk-generate enqueues a BullMQ blog-generation job; per topic it skips existing slugs, generates structured content via the OpenAI Responses API (model gpt-4o) with a strict JSON schema (blog/services/blog-ai.service.ts:generateBlogContent), optionally generates a 16:9 cover photo via Gemini uploaded to S3 with blurhash (generateCoverImage), retries slug collisions with a timestamp suffix, optionally auto-publishes, and optionally marks 4 random created posts as featured (blog/processors/blog-generation.processor.ts). Job listing/status endpoints expose queue state (blog.controller.ts:97-160). POST /blogs/generate-cover generates a single cover on demand (503 if Gemini unavailable).
  • Web editor: platform admins author posts at apps/web/app/(app)/(platform)/platform/blog (list, new, [id]) using a Plate rich-text editor. apps/web/app/editor/page.tsx is an unauthenticated test page for the Plate editor (its own comment: "This test page uses components that require QueryClientProvider at runtime") — it is not part of the blog flow.
  • Public reading happens at apps/web/app/(app)/(public)/blogs (/blogs, /blogs/[slug]) with SEO metadata/JSON-LD. (apps/web/app/(app)/(public)/features is a static marketing page, unrelated to feature requests.)

Trainings

Lifecycle state machine (TrainingStatus, domain-status.ts:478; transitions in trainings.service.ts:517-713):

  • Ownership/visibility (trainings.service.ts:findAll, isTrainingVisibleToUser, enforceTrainingOwnership): platform trainings have business = null and only SuperAdmin/System can modify them; business trainings are only modifiable by users of that business with TRAININGS_MANAGE. Non-managers see only published trainings of their business plus public+published platform trainings. A learner with existing progress keeps access to a training that was since unpublished (allowInProgressAccess).
  • Catalog ("avail") flow (trainings/services/business-training.service.ts): the catalog is non-public, published platform trainings (getCatalog filter {business: null, isPublic: false, status: published}). POST /trainings/business-trainings/:id/avail (permission TRAININGS_CATALOG) deep-forks the training into a business-owned, immediately-published copy with new section IDs but reused S3 keys (no asset duplication; hard-deleting the source breaks forks — entity comment in training.entity.ts), plus a BusinessTraining audit row. Re-avail is intentionally unconditional and creates parallel forks.
  • Learner progress (trainings.service.ts:1073-1338): POST /trainings/:id/progress/start requires the training to be published; creates/touches a TrainingProgress doc and emits training.started. POST /trainings/:id/progress/sections/:sectionId records section completion and/or scores an assessment attempt server-side (percentage of exact-match answers vs correctAnswer; sections without questions auto-score 100/pass). Required assessments gate completion; maxAttempts blocks further failed attempts. overallProgress = completed sections / total sections; reaching 100% sets completedAt and emits training.completed (and can regress below 100% if sections are added).
  • Completion side-effects (trainings/listeners/training-completion.listener.ts): on training.completed → generate certificate + mark any assignment COMPLETED; on training.started → mark assignment IN_PROGRESS and link the progress doc.
  • Certificates (trainings/services/training-certificate.service.ts:generateCertificate): idempotent per {training, user}; validates 100% progress and all required assessments passed; generates a unique CERT-YYYYMM-XXXXXX number; renders a PDF via Puppeteer against the frontend certificate page (PdfService.generateTrainingCertificatePdf) and uploads to S3 (failure is non-fatal — PDF can be regenerated). Public, unauthenticated verification at GET /trainings/certificates/verify/:certificateNumber increments verificationCount (training-certificate.controller.ts).
  • Assignments (trainings/services/training-assignment.service.ts): admins with TRAININGS_ASSIGN create single or bulk assignments of published trainings; if the user already has progress the initial status is back-filled to IN_PROGRESS/COMPLETED (createAssignment:96-104). A daily midnight cron bulk-marks past-due ASSIGNED/IN_PROGRESS assignments OVERDUE with a system status-history entry (trainings/tasks/training-assignment.tasks.ts, markOverdueAssignments). No notification is sent on assignment or overdue (none found in the service).
  • AI content pipeline (all behind TRAININGS_MANAGE):
    • POST /trainings/generate runs an agentic BullMQ training-generation job over selected knowledge-base documents: iterative search → generate → self-check → refine loop (max 5 iterations), accumulating search context and confidence, then creates the Training and optionally auto-generates cover photo, slides, videos, and assessments (training-generation-job.entity.ts, processors/training-generation.processor.ts).
    • Per-section slide CRUD/generation, slide-image regeneration (Gemini, with photoBank images as reference context), AI assessment generation, and a slides-video-merge BullMQ job that renders slides + TTS speech scripts into a section video with captions (SlideMergeStatus: pending/processing/completed/failed) (trainings.controller.ts:300-540, processors/slides-video-merge.processor.ts).

Skills

No workflow. CRUD on the global catalog plus POST /skills/populate, which idempotently seeds 20 hardcoded caregiving skills (skills.service.ts:populateSkills). findOrCreateSkillsByNames (case-insensitive find-or-create) exists but has no callers anywhere in the backend — dead code. Skills attach by ObjectId reference to:

  • Care provider profiles — top-level skills[] and per-WorkExperience skills[] (apps/backend/src/users/entities/care-provider-profile.entity.ts:49,164), populated during mobile profile setup (apps/mobile/app/setup-profile/careprovider/skills.tsx, add-work-experience.tsx).
  • Job postings — JobPosting.skills[] (apps/backend/src/job-postings/entities/job-posting.entity.ts:94), selected in the web job-posting form and populated (id name) throughout job-postings.service.ts. No automated skill-matching algorithm between providers and postings was found — matching is human/visual.

Ratings

  • POST /ratings: the authenticated caller (rater) rates a target user (userId, called "care provider" in the service but any existing user passes — there is no role check on the target, ratings.service.ts:create). Score 0–5, optional comment. After save, CareProviderService.recalculateAverageRating(userId) runs fire-and-forget (failures logged, non-blocking).
  • GET /ratings?userId=&isVerified=&sortOrder= lists with pagination and population; sort by recency or score.
  • Verification: POST /ratings/:id/verify sets isVerified/verifiedAt/verifiedBy; DELETE /ratings/:id/reject simply deletes the rating (same handler as remove). There is no role/permission restriction on any ratings endpoint beyond JWT auth (ratings.controller.ts uses only AuthGuard('jwt')).
  • No state machine beyond unverified → verified (or deletion). No duplicate-rating prevention (one user can rate the same person repeatedly).

Feature requests

  • Business side (REQUESTS_MANAGE permission — shared with the operational Requests module, see 13-requests.md): POST /feature-requests creates; GET /feature-requests lists own business's requests with status, platformNotes, business, submittedBy stripped — submitters deliberately cannot see triage state (feature-requests.service.ts:findAllForBusiness).
  • Platform side (SuperAdmin/System via PLATFORM_ROLESuser-role.ts:90): GET /feature-requests/platform (with per-status tab counts scoped to category/search filters), GET/PATCH /feature-requests/platform/:id to set status/platformNotes.
  • No voting, no comments, no de-duplication — it is a plain submit-and-triage pipeline.

Business Rules & Constraints

  • Posts permission decorators are not enforced. PostsController applies only JwtAuthGuard (posts.controller.ts:36); its @RequirePermissions(...) decorators rely on the globally registered PermissionsGuard (app.module.ts:292-294), which intentionally defers (returns true) when req.user is not yet populated — and global guards run before the controller-level JwtAuthGuard (auth/guards/permissions.guard.ts:64-72). Unlike blog/trainings/feature-requests controllers, posts never re-applies PermissionsGuard at controller level, so every authenticated user can create posts, like, comment, and bookmark regardless of POSTS_MANAGE. This is also why CareProviders (whose defaults grant only POSTS_VIEW_ASSIGNED + POSTS_FLAGpackages/shared/src/constants/default-role-permissions.ts:238-239) can interact with the feed from mobile.
  • Posts are tenant-isolated by explicit business match in every aggregation (posts.service.ts:75-78) plus the global business-scope Mongoose plugin (common/plugins/business-scope.plugin.ts).
  • Only the post/comment author may edit or delete their content (posts.service.ts:437, 468, 1092+, 1124+); no moderator override exists.
  • Max 500 replies per comment — double-enforced (comment.entity.ts pre-save; posts.service.ts:840-846). One like per user per target (unique index). One bookmark per user per post (unique index).
  • Birthday posts: one per celebrant per calendar day (birthday.service.ts:checkIfBirthdayPostExists); skipped for users without a business; showBirthdayInFeed is currently ignored (commented out).
  • Blog slugs are globally unique; conflict → 409 (blog.service.ts:create/update). publishedAt is set only on the first transition to published. Deletes are soft (isDeleted), including the bulk DELETE /blogs/all.
  • Blog admin endpoints reuse POSTS_MANAGE; any business Admin holding it can manage the platform-global blog (blog has no business scoping) (blog.controller.ts).
  • Trainings: business-authored trainings must pass SuperAdmin review (pending_review) before publishing; platform trainings publish directly (trainings.service.ts:publish:570-607). Only draft/rejected can be (re)submitted; only pending_review can be approved/rejected; unpublish allowed from published/pending_review; void has no status precondition.
  • Only published trainings can be started (startTraining:1102-1104) or assigned (createAssignment:66-68).
  • Catalog avail requires: platform training (business === null), not public, published (business-training.service.ts:54-68). Forks are published immediately and never receive upstream updates (permanent snapshot).
  • Assessment scoring is server-side; required assessments block section completion; maxAttempts is only enforced when the new attempt fails (trainings.service.ts:1245-1253).
  • One certificate per {training, user} (idempotent); requires 100% progress + all required assessments passed; certificate verification endpoint is public/unauthenticated (training-certificate.controller.ts:verifyCertificate).
  • Assignments: unique per {training, user} among non-deleted (partial index); overdue flip is system-only via the midnight cron.
  • Skills/Ratings: no permission checks at all beyond JWT — any authenticated user of any role can create/update/delete global skills (including cross-tenant impact, since the catalog is shared) and create/verify/delete ratings (skills.controller.ts, ratings.controller.ts).
  • Rating business is auto-set from CLS by the business-scope plugin, and reads are auto-scoped to the caller's business; SuperAdmin (null business context) sees all (business-scope.plugin.ts).
  • Feature requests: business users never see status/platformNotes (field-stripped in findAllForBusiness); only PLATFORM_ROLES (SuperAdmin, System) can read cross-tenant or update; search input is regex-escaped (escapeRegExp from care-proposals/utils/security.util).

Surfaces (Web & Mobile)

Web (apps/web)

SurfaceRouteWhoWhat
Feedapp/(app)/(admin)/dashboard/posts (+[id])Admin dashboard usersBusiness feed: list, detail, create/comment/like (uses /posts API)
Celebrationsapp/(app)/(admin)/dashboard/celebrationsAdmin dashboard usersTabs of today/upcoming/recent birthdays from GET /posts/birthdays/range; "send greeting" dialog posts to POST /posts/birthdays/:userId/greetingsan endpoint that does not exist in the backend (celebrations/page.tsx:194, 214; no such route in posts.controller.ts)
Blog adminapp/(app)/(platform)/platform/blog (+new, [id])SuperAdminAuthoring with Plate editor, bulk AI generation, publish/feature
Blog publicapp/(app)/(public)/blogs (+[slug])AnonymousPublished posts, tags, featured, SEO/JSON-LD
Editor test pageapp/editor/page.tsxAnyone (no auth wrapper)Standalone Plate editor playground; self-described test page
Trainings (business)app/(app)/(admin)/dashboard/trainings (+add, [id])Admin (TRAININGS_MANAGE)Author/edit sections, slides, assessments, publish→review
Training catalogapp/(app)/(admin)/dashboard/training-catalogAdmin (TRAININGS_CATALOG)Browse platform catalog, avail (fork), see availed list
Training centerapp/(app)/(admin)/dashboard/training-centerRedirect-only stub to /dashboard/trainings (training-center/page.tsx)
Trainings (platform)app/(app)/(platform)/platform/trainings (+add, [id])SuperAdminAuthor platform/catalog trainings; approve/reject business submissions
Skillsapp/(app)/(platform)/platform/skillsSuperAdmin UI (API unrestricted)Global skill catalog CRUD
Ratingscomponents/user-reviews/* used in dashboard/clients/[id]/edit/care-team/_components/caregiver-card.tsx and components/user-profile/*Admin dashboard usersCreate/edit/list reviews of care providers; verify
Feature requests (business)app/(app)/(admin)/dashboard/feature-requestsAdmin (REQUESTS_MANAGE)Submit + list own (no status visible)
Feature requests (platform)app/(app)/(platform)/platform/feature-requestsSuperAdminTriage: status tabs, update status/notes

Mobile (apps/mobile)

SurfaceRouteWhat
Feedapp/(app)/(feed)/index.tsx, bookmarks.tsx, [id].tsx + components/post/create-post-bottom-sheet.tsxFull feed for care providers/families: create, like, comment, reply, bookmark (lib/posts-api.ts covers all /posts endpoints)
Post deep linkapp/(post)/[id].tsxStandalone post detail screen (notification deep-link target)
Trainingsapp/(trainings)/index.tsx, [id]/index.tsx, [id]/[sectionId].tsxLearner view: published trainings list (status=published), my progress, my certificates; section player + assessments
Celebrationsapp/(celebrations)/_layout.tsxLayout only — the index.tsx screen its own doc comment describes does not exist. The route group is effectively dead; only the birthday privacy toggle (components/ui/birthday-privacy-toggle.tsx) ships, and the backend ignores that flag anyway
Skillsapp/setup-profile/careprovider/skills.tsx, add-work-experience.tsx (lib/skills-api.ts)Care provider onboarding picks skills from the global catalog

Frontend-only rules of note: the mobile trainings list filters to status=published client-side via query param; web celebrations greeting flow is broken at the API layer (above); no mobile UI exists for ratings or feature requests.

Cross-Module Dependencies

  • Posts → Notifications: PostNotificationService (in apps/backend/src/notification/post-notification.service.ts) sends PostLiked, PostCommented, CommentLiked, CommentReplied, PostMentioned (packages/shared/src/enums/notification-types.ts:30-34); birthday cron sends BirthdayToday via NotificationService.sendUnifiedNotification (posts/tasks/birthday.tasks.ts). See 12-communication.md.
  • Posts ← Users: birthday detection reads User.birthday/isActive/business (posts/birthday.service.ts).
  • Trainings → Knowledge Base: AI generation searches selected KnowledgeBaseDocuments (trainings.service.ts ctor injects KnowledgeBaseService; training-generation-job.entity.ts).
  • Trainings → Files/PDF: signed URLs for section videos/captions, certificate PDFs via PdfService (Puppeteer) + FilesService S3 upload (training-certificate.service.ts).
  • Trainings → AI services: TrainingAIContentService, SlidesVideoService, Gemini image generation; BullMQ queues training-generation and slides-video-merge (trainings.module.ts, processors/).
  • Blog → AI/Files: OpenAI (gpt-4o) + GeminiService + FilesService (blog/services/blog-ai.service.ts); BullMQ queue blog-generation.
  • Skills → Job Postings: JobPosting.skills refs (job-postings/entities/job-posting.entity.ts:94); see 06-job-postings.md.
  • Skills → Users: CareProviderProfile.skills and work-experience skills (users/entities/care-provider-profile.entity.ts:49,164); also referenced in shift generation UI (apps/web/.../shifts/_components/shift-generation-form.tsx).
  • Ratings → Users: CareProviderService.recalculateAverageRating updates the care provider profile's average (ratings.service.ts); displayed on user profiles/care-team cards. Ratings are not linked to shifts/visits — they are free-floating user-to-user ratings.
  • Feature Requests ↔ Requests module: gated by the same REQUESTS_MANAGE permission as operational requests (13-requests.md) and reuses escapeRegExp from care-proposals utils.
  • Permissions: all module permissions live in packages/shared/src/enums/business-permission.ts with role defaults in packages/shared/src/constants/default-role-permissions.ts; Owner/SuperAdmin/System bypass all checks (user-role.ts:254).

Open Questions & Gaps

  1. Posts permissions are decorative. @RequirePermissions on every PostsController route is never evaluated because the controller lacks a controller-level PermissionsGuard and the global instance defers pre-auth (posts.controller.ts:36; permissions.guard.ts:64-72). Either this is intentional ("everyone in the business may use the feed") or a missing guard — cannot determine from code. If POSTS_MANAGE was meant to gate creation, it is currently bypassed.
  2. Web celebrations "send greeting" calls a nonexistent endpoint. dashboard/celebrations/page.tsx:214 POSTs to /posts/birthdays/:userId/greetings; no such route exists in posts.controller.ts. Every greeting submission should 404. Broken feature.
  3. Mobile celebrations route group is an empty shellapp/(celebrations)/ contains only _layout.tsx, whose comment documents an index.tsx that does not exist. Dead/dormant surface.
  4. showBirthdayInFeed privacy toggle is a no-op. Mobile lets users toggle it, but all birthday queries have the filter commented out with a TODO (birthday.service.ts:88, 137, 215). Users who opt out still get public birthday posts and business-wide notifications — a privacy gap.
  5. Skills and Ratings endpoints have no authorization beyond JWT: any authenticated user (e.g. a Family member) can delete global skills shared by all tenants, create ratings about anyone, verify ratings, or delete others' ratings (skills.controller.ts, ratings.controller.ts). Also no duplicate-rating prevention and no role check that the rated user is actually a care provider.
  6. POSTS_FLAG, POSTS_MODERATE, POSTS_CONFIGURE, POSTS_VIEW_ALL are defined and granted in role defaults but referenced nowhere in the backend — no flagging/moderation endpoints exist. Planned-but-unbuilt moderation.
  7. Blog admin reuses POSTS_MANAGE even though the blog is platform-global: a business Admin with the default permission set can, per the guard logic, create/edit/delete the public marketing blog (blog.controller.ts). Whether non-SuperAdmins are blocked elsewhere cannot be determined from code (web only exposes the UI under (platform)).
  8. DELETE /blogs/all soft-deletes the entire blog in one authenticated call — dangerous surface for a POSTS_MANAGE holder.
  9. Birthday timezone: cron runs at 9 AM Pacific and "today" is computed from server-local new Date() (birthday.service.ts:findUsersWithBirthdayToday), despite a comment claiming "uses the user's timezone". Users in other timezones may be celebrated on the wrong local day.
  10. Posts debug endpoint in production: GET /posts/birthdays/debug/:username exposes another user's birthday, activity status, and privacy flag to any feed-viewing user (posts.controller.ts:108-115).
  11. Training void status is terminal and unguarded — any status (including generating) can be voided, and no endpoint restores a voided training (trainings.service.ts:694). Intent unclear.
  12. Catalog fork asset coupling: forks reuse S3 keys; hard-deleting a source training (the code only soft-deletes via API, but ops-level deletes would) breaks all forks — acknowledged in entity comments (training.entity.ts sourceTrainingId comment) but unmitigated.
  13. Feature requests have no notification loop: submitters cannot see status by design and are never notified of outcomes; no events/notifications are emitted by feature-requests.service.ts. Whether this is intended fire-and-forget feedback cannot be determined from code.
  14. findOrCreateSkillsByNames is dead code (skills.service.ts) — no callers; likely a leftover from an AI flow.
  15. training-center web route is a redirect stub to /dashboard/trainings (training-center/page.tsx) — vestigial.
  16. Module activity: trainings (last commit 2026-05-06), posts/blog (2026-04-24), feature-requests (2026-04-14) are active; ratings (2026-03-29) and especially skills (2026-01-23) are near-dormant. The /editor test page and mobile (celebrations) group look abandoned.
  17. Ratings isVerified query filter is typed @IsBoolean() on a query string (filter-rating.dto.ts); it filters only when truthy, and isVerified=false cannot be used to list unverified-only ratings.

On this page