Platform Operations & Supporting Services
Part of the Anaya Care product wiki. See 00-overview.md.
Purpose
This module covers the horizontal infrastructure that every domain module consumes: S3 file storage and signed URLs (files/), bundled fonts/images (assets/), generated initials avatars (avatar/), the LockCare document vault (lock-care/), PDF generation (pdf/), Google Maps geocoding/timezone lookup (google-maps/), super-admin statistics (statistics/), platform health checks (platform-health/), the activity/audit log including HIPAA PHI-access auditing (platform-logs/), global platform settings (platform-settings/), mobile force-update checks (app-version/), the BullMQ dashboard (bull-board/), super-admin maintenance endpoints (internal/), dev tooling (dev/), one-off database migrations (migrations/), and the cross-cutting interceptors/decorators/plugins in common/.
All paths in this document are relative to apps/backend/src/ unless prefixed with apps/web/, apps/mobile/, or packages/shared/.
Entities & Data Model
Only four submodules persist their own data; the rest are stateless services over S3, external APIs, or other modules' collections.
LockCare (lock-care/lock-care.entity.ts) — collection lock_cares
| Field | Type | Notes |
|---|---|---|
key | string, required | S3 object key |
name | string | Display filename (original upload name; renameable) |
size | number | Bytes |
mimeType | string | From the uploaded file |
description | string, optional | |
ownerType | enum FileOwnerType, required | USER | CLIENT | CARE_PROPOSAL (packages/shared/src/enums/lock-care.ts) |
ownerId | ObjectId, required | Untyped ref — resolved per ownerType |
uploadedBy | ObjectId ref User, required | Populated in list queries |
tags | string[] | Default [] |
uploadedAt | Date | Default Date.now |
metadata | Mixed | Free-form; metadata.folder carries the folder enum value |
blurhash | string | null | Generated for image uploads |
url | virtual | Signed URL injected at read time, never stored |
Indexes: { ownerType: 1, ownerId: 1 } and { tags: 1 } (lock-care/lock-care.entity.ts:68-69). Note: no business field — LockCare documents are not tenant-scoped by the business-scope plugin (see Open Questions).
PlatformLog (platform-logs/entities/platform-log.entity.ts) — collection platform_logs
| Field | Type | Notes |
|---|---|---|
actor | ObjectId ref User, required | Who performed the action |
entityType | enum PlatformLogEntityType, required | Shared enum (packages/shared/src/enums/domain-status.ts) |
entityId | string, required | Id of the affected entity |
action | enum PlatformLogAction, required | ~40 values: created, updated, deleted, user_logged_in, shift_assigned, care_proposal_status_changed, phi_viewed, training/announcement/rate actions, etc. (packages/shared/src/enums/domain-status.ts:941+) |
business | ObjectId ref Business | null | Tenant; null for platform-level events |
description | string | Auto-generated if not supplied (platform-logs/platform-logs.service.ts:53-55) |
metadata | object | Free-form |
ipAddress | string, optional |
Indexes: { business, createdAt }, { entityType, action, createdAt }, { actor, createdAt }, plus a TTL index { createdAt: 1 } with expireAfterSeconds: 189216000 (6 years) — commented as HIPAA 45 CFR 164.530(j) retention (platform-logs/entities/platform-log.entity.ts:58-62).
PlatformSettings (platform-settings/entities/platform-settings.entity.ts) — collection platform_settings
Singleton document (key: 'global', unique). Fields: minVersionIos, minVersionAndroid, latestVersionIos, latestVersionAndroid (all default '1.0.0'), iosStoreUrl, androidStoreUrl, OTA controls latestOtaUpdateIdIos, latestOtaUpdateIdAndroid, deprecated latestOtaUpdateId, and isOtaUpdateRequired (default false). get() upserts the singleton on first read (platform-settings/platform-settings.service.ts:23-31).
AppVersion
No entity of its own — app-version/app-version.service.ts reads PlatformSettings. Statistics are also not persisted: statistics/statistics.service.ts aggregates live over shift_assignments, care_proposals, and clients.
Workflows & Key Behaviors
files/ — S3 storage & signed URLs
FilesService(files/files.service.ts) wraps an S3 client built fromS3_BUCKET_NAME,S3_REGION,S3_ACCESS_KEY,S3_SECRET_ACCESS_KEY; constructor fails fast if bucket/region are missing and pushes a CORS config to the bucket on boot (files/files.service.ts:49-77).- Upload (
uploadFile, :79): key ={normalizedFolder}/{uuid}{ext}— the folder is caller-supplied in the request body (files/schema/file.zod.ts), normalized only by trimming slashes, defaulting touploads/(:252-260). Objects are stored withServerSideEncryption: AES256and ACLpublic-readorprivateper theisPublicflag (:111-118). Images optionally get a blurhash and resized "alternative dimension" variants stored as{uuid}-{WxH}{ext}(:135-190). - Type/size limits enforced in the controller per
FILE_LIMITS(files/interfaces/file-types.ts): image 15 MB (jpg/jpeg/png/webp), video 100 MB, document 100 MB (pdf/office), audio 50 MB. - URLs: public files get the raw bucket URL
https://{bucket}.s3.amazonaws.com/{key}(:353-355); private files get a presigned GET URL with 1-hour expiry, commented "HIPAA: 1-hour expiry for PHI document access" (:362-375). - Endpoints (
files/files.controller.ts, controller-levelJwtAuthGuard):POST /files/:typeandPOST /files/:type/batch(multipart upload),POST /files/download(bundle images aspdf-print/pdf-standard/png-zipvia pdfkit/archiver),GET /files/file/signed-url?key=,GET /files/file/watermark?key=(tiled "Geriatric Care Solution" watermark via sharp),POST /files/file/copy-private-to-public,GET /files/file/:key(@Public()— no auth, streams any object by key,files/files.controller.ts:176-195),GET /files/file/:key/url,GET /files/file/:key/signed-url,DELETE /files/file/:key,PUT /files/file/:key/update-to-private,POST /files/file/combine-images-to-pdf,POST /files/image/rgb-channels(uploads original + per-channel red/green/blue renditions; used for visual-assessment imagery). - Upload error handling maps S3 error codes (ACL unsupported, AccessDenied, NoSuchBucket, bad credentials, region mismatch, unsupported image format) to actionable messages (
files/files.service.ts:262-318, hardened in commit46e2daa3).
assets/ — bundled static assets
assets/fonts/ (DM Sans, Inter) and assets/images/ (anayacare-logo.png, gcs-logo-banner.png, sample.jpg). Consumed by the avatar generator (font registration, avatar/avatar.service.ts:20-30) and PDF layouts via pdf/resolve-asset-path.ts, which resolves src/assets/... whether cwd is the backend package or the monorepo root.
avatar/ — generated initials avatars
AvatarService.createAvatarImagedraws a 1000×1000 PNG on a@napi-rs/canvascanvas: linear gradient from the role's palette (getRoleGradientColorsin@anaya/shared, plus localClient/GCSpalettes) with up to two white initials in DM Sans (avatar/avatar.service.ts:42-100).createAndUploadAvataruploads the PNG viaFilesServiceas a public file under folderavatars/(or caller-supplied folder) and returns the URL (:102-130);generateAvatarUrlIfMissingis the helper other modules call to backfill missing profile images (:132-141).- Endpoints (
avatar/avatar.controller.ts,JwtAuthGuard):GET /avatar?name=&role=streams a PNG;POST /avatar/upload?name=&role=&folder=generates and uploads.
lock-care/ — the document vault
- Wraps
FilesService:uploadFileToS3AndSaveToLockCareuploads the file private (isPublic: false), generates a blurhash for images, then creates theLockCarerecord (lock-care/lock-care.service.ts:404-447). - Every read path re-signs a fresh 1-hour URL per document (
findById,findByOwner,findByMetadata,renameFile— e.g.:75,:200). - Querying is metadata-driven: filters are built dynamically as
metadata.{key} = value/$in/$nin(:156-170), with the folder category stored atmetadata.folder(controller mergesbody.folderinto metadata,lock-care/lock-care.controller.ts:80-84). Folder taxonomies live in@anaya/shared:ClientFolderType(MEDICAL_RECORD, DOCTOR_APPOINTMENT, MEDICATION, ADVANCE_DIRECTIVE, insurance/ID/legal docs, PERSONAL_PHOTO, …),CareProviderFolderType,UserFolderType(packages/shared/src/enums/lock-care.ts). getStatisticsByMetadataKeyaggregates document counts grouped by a metadata key for an owner (:356-402) — powers folder counts in the UI.- Access control: all endpoints require
JwtAuthGuard+BusinessPermission.LOCK_CARE_VIEW_ASSIGNED(reads) orLOCK_CARE_MANAGE(upload/delete/rename) (lock-care/lock-care.controller.ts). Permission semantics are documented in 11-identity-and-access.md. - hasAdvanceDirective sync:
createLockCareanddeleteemitlockcare.document.changed(lock-care/lock-care.service.ts:50-54,:319-323).DnrSyncListener(clients/listeners/dnr-sync.listener.ts) reacts whenownerType === CLIENTandmetadata.folder === ADVANCE_DIRECTIVE: it counts remaining advance-directive documents and updates the client to{ hasDnrOrder: false, hasAdvanceDirective: count > 0 }.hasDnrOrderis always forced tofalsehere (a legacy flag retired byscripts/migrate-dnr-orders-to-advance-directives.ts). These flags drive DNR acknowledgment requirements for care providers (clients/services/dnr-acknowledgment.service.ts).
pdf/ — PDF generation
- Two engines (
pdf/pdf.service.tsx):- @react-pdf/renderer (v4, ESM-only) for everything except certificates. It is loaded via a plain-JS dynamic
import()shim (pdf/load-react-pdf.js) because TypeScript's CJS transform would break the ESM package. Layout components live inpdf/pdf-layout/: care-plan, care-proposal, health-metrics-report, markdown, period-report, four report variants (care-manager, clinical-snapshot, family-summary, formal-welfare, transition-care), shift-report, shift-summary. - Puppeteer only for training certificates:
generateTrainingCertificatePdflaunches headless Chromium (--no-sandbox), navigates to the web frontend page/certificates/{number}/print, waits for fonts/content, and prints landscape US-Letter (pdf/pdf.service.tsx:544-633).
- @react-pdf/renderer (v4, ESM-only) for everything except certificates. It is loaded via a plain-JS dynamic
convertMarkdownToPdfStreamrenders AI-generated markdown (used by care proposals/plans) with special tags[PAGE_BREAK]and[LANDSCAPE]...[/LANDSCAPE]for landscape calendar pages (pdf/pdf.service.tsx:106-192); text is sanitized to avoid@react-pdf/textkitglyph crashes (:73-105).PdfControlleris empty — no routes; the service is consumed internally by other modules (pdf/pdf.controller.ts).
google-maps/ — geocoding & timezone
GoogleMapsController(google-maps/google-maps.controller.ts, controller-levelJwtAuthGuard):GET /google-maps/places/search(@Public()),GET /google-maps/timezone(@Public()),GET /google-maps/places/details(@Public()),GET /google-maps/places/nearby(auth),POST /google-maps/places/format-address,PATCH /google-maps/places/address.- Autocomplete calls the Places v1 REST API with a session token, US/PH region restriction and optional 10 km location bias (
google-maps/google-maps.service.ts:82-156). Hardened error handling (commit46e2daa3): non-OK responses log Google's real error body and surface as503 ServiceUnavailableException("Address lookup is temporarily unavailable") instead of unhandled 500s (:126-156). getPlaceDatafetches place details, parses address components (with fallbacks toadrFormatAddressspan scraping and the user's typedselectedText), and geocodes route-level places to a street address when needed (:228-318).getTimezonewraps the legacy@googlemaps/google-maps-services-jstimezone API and returns thetimeZoneId(:54-72) — consumed by user/client timezone resolution (internal/migration endpoints, signup flows).
statistics/ — super-admin dashboards
StatisticsControllerrequiresJwtAuthGuard+PermissionsGuard+BusinessPermission.STATISTICS_VIEW_ALLon the whole controller (statistics/statistics.controller.ts:9-12). Endpoints:/statistics/shifts,/care-proposals,/clients,/overview.- The service aggregates live: shift status distribution, 6-month monthly trend, completion and assignment-method breakdown over
shift_assignments; status/monthly distributions overcare_proposals; status distribution and totals overclients(statistics/statistics.service.ts:48-360)./overviewcomposes all three plus summary counts (:362-400). - The aggregations carry no explicit business filter — tenancy comes from the global business-scope Mongoose plugin's
pre('aggregate')hook (all three entities have abusinesspath), so an Admin sees their business and a SuperAdmin (null CLS business) sees platform-wide numbers (common/plugins/business-scope.plugin.ts:70-85). - Powers
apps/web/app/(app)/(admin)/dashboard/statistics/page.tsx(super-admin statistics dashboard).
platform-health/
GET /platform-health(JwtAuthGuard+PermissionsGuard+BusinessPermission.HEALTH_METRICS_VIEW_ASSIGNED;?refresh=truebypasses a 30-second cache) returns four categories (platform-health/platform-health.service.ts):- Infrastructure: MongoDB, Redis, app server (always "Operational" with node/memory details).
- External services: Twilio, Resend, Expo push, OpenAI, S3, Google Maps — one indicator class each in
platform-health/indicators/. - Background jobs:
QueueIndicatorchecks 9 hard-coded BullMQ queues (waiting/active/completed/failed/delayed/paused counts) (platform-health/indicators/queue.indicator.ts:14-24). - Real-time: chat and notification Socket.IO gateways (
websocket.indicator.ts).
- Status rolls up worst-of per category (MajorOutage > PartialOutage > Degraded > Operational) and then overall (
platform-health.service.ts:209+). - Rendered on the web super-admin dashboard (
apps/web/app/(app)/(admin)/dashboard/_components/platform-health/platform-health-card.tsx).
platform-logs/ — activity & PHI audit log
- Write path is event-driven: any module emits
PLATFORM_LOG_ACTIVITY_EVENT(platform-logs/events/platform-log.events.ts) with actor/entityType/entityId/action/business/metadata/ip;PlatformLogListenerconsumes it and persists viaPlatformLogsService.log()(platform-logs/listeners/platform-log.listener.ts). Writes are fire-and-forget —log()swallows errors and returns null so audit failures never break the request (platform-logs/platform-logs.service.ts:43-78). - PHI access auditing (
@AuditPhiAccess): the decorator (common/decorators/audit-phi-access.decorator.ts) tags a handler with an entity type; the globally-registeredPhiAccessAuditInterceptor(app.module.ts:307-310) reads the metadata and, on successful response, emits a platform-log event with actionPHI_VIEWED, entity id fromparams.id/params.clientId(else'unknown'), the route path/method, and the requester IP (common/interceptors/phi-access-audit.interceptor.ts:55-84). Currently applied in:clients/controllers/clients.controller.ts,clients/controllers/shift-summary.controller.ts,health-metrics/health-metrics.controller.ts,incident-reports/incident-reports.controller.ts,wound-care/wound-care.controller.ts. - ~30 services emit ordinary activity events (auth, business, care-proposals, clients sub-services, shifts, requests, trainings, job applications, etc. — see Cross-Module Dependencies).
- Read path:
GET /platform-logsandGET /platform-logs/my-activity(PLATFORM_LOGS_VIEWpermission) with filters for actor/entityType/action/date-range/search, scoped to the caller'sbusinessIdwhen present (platform-logs/platform-logs.controller.ts:24-37). Web UI:apps/web/app/(app)/(admin)/dashboard/platform-logs/page.tsx.
platform-settings/
GET/PATCH /platform-settings, guarded by JwtAuthGuard + RolesGuard + @Roles(UserRole.SuperAdmin) (platform-settings/platform-settings.controller.ts:10-12). Edited via apps/web/app/(app)/(platform)/platform/settings/page.tsx (semver-validated form for min/latest versions and store URLs).
app-version/ — mobile force-update
GET /app-version/check?platform=¤tVersion=¤tUpdateId=is@Public()(intentional — runs before login) (app-version/app-version.controller.ts:10-12).- Returns
{ minimumVersion, latestVersion, isUpdateRequired, isUpdateAvailable, storeUrl, latestOtaUpdateId, isOtaUpdateAvailable, isOtaUpdateRequired }.isUpdateRequired= installed semver strictly below the platform's min version; store URL falls back to hard-coded App Store / Play Store URLs; OTA is "available" when the device's ExpoupdateIddiffers from the configured latest, and "required" only whenisOtaUpdateRequiredis also set (app-version/app-version.service.ts:17-60). - Mobile:
apps/mobile/hooks/use-force-update.tspolls every 5 minutes (and on app foreground), skips in__DEV__, detects TestFlight installs via theitms-beta://scheme, and blocks/redirects the user when an update is required.
bull-board/ — queue dashboard
BullBoardConfigModule (bull-board/bull-board.module.ts) mounts the Bull Board Express UI at /admin/queues and registers 9 queues: notification, shifts, shift-reminders, care-readiness-assessment, care-provider-tasks-generation, care-plan-generation, client-meals, client-engagement-activities, slides-video-merge.
The app registers 14 distinct BullMQ queues in total; 5 are not visible in Bull Board (or in the platform-health queue indicator):
| Queue | Registered in | In Bull Board? |
|---|---|---|
notification | many modules (e.g. clients/clients.module.ts:244) | yes |
shifts, shift-reminders, care-readiness-assessment | shifts/shifts.module.ts:78-81 | yes |
care-provider-tasks-generation, care-plan-generation | clients/clients.module.ts:245-246 | yes |
client-meals | client-meals/client-meals.module.ts:40 | yes |
client-engagement-activities | clients/clients.module.ts:247 | yes |
slides-video-merge | trainings/trainings.module.ts:56-58 | yes |
appointment-history-generation | clients/clients.module.ts:248 | no |
shift-engagement-assignments | client-engagements/client-engagements.module.ts:44 | no |
shift-meal-assignments | client-meals/client-meals.module.ts:41 | no |
training-generation | trainings/trainings.module.ts:68-70 | no |
wound-analysis | wound-care/wound-care.module.ts | no |
No authentication is configured on the Bull Board route — the only reference to it elsewhere is the request logger skipping it (common/middlewares/request-logger.middleware.ts:9). See Open Questions.
internal/ — super-admin maintenance endpoints
InternalController (internal/internal.controller.ts) is guarded by JwtAuthGuard + RolesGuard + @Roles(UserRole.SuperAdmin). Endpoints are labelled [INTERNAL TESTING] / [MIGRATION] in Swagger: remove a user's profile by email, force-verify email/phone (assigns test phone numbers from a +1 555-01xx pool), reset all isOnboarded flags, set default timezone/preferences, re-resolve timezones from addresses via Google Maps (users and clients), generate/regenerate avatars, create/update the "Anaya" system user, cleanup orphaned task submissions (dry-run default), and delete all shifts for a client (dry-run default).
dev/ — dev-only endpoints (NOT actually dev-only)
DevController (dev/dev.controller.ts) has no guards at all — the file comment says "Dev-only controller… No auth required — intended for dev scripts and seed tooling". Endpoints: GET /dev/users-by-role (returns real user emails by role, used by get-token.sh), GET /dev/seed-templates, and POST /dev/clients/:clientId/seed-template (overwrites a client's medical record and all 6 assessments with template data). DevModule is imported unconditionally in app.module.ts:265 with no NODE_ENV gate anywhere in dev/ — so these unauthenticated endpoints are live in production. Flagged in Open Questions.
migrations/ — one-off data migrations
- Migrations are standalone ts-node scripts, not a framework: each connects directly with
mongoose.connect(process.env.MONGO_URI)and is run manually, e.g.npx ts-node -r tsconfig-paths/register src/migrations/025-family-to-representative-rename.ts(header of each file). There is no migrations-state collection and no automatic runner — execution and ordering are operator-driven; idempotency is per-script (e.g. 031 declares "This migration is idempotent"). - ~60 scripts numbered
001–059, with duplicated numbers (005,009,010,050) and gaps (014,052);010-backfill-post-notification-images.mongosh.jsis a mongosh script rather than ts-node. - Notable migrations:
- 025 (
025-family-to-representative-rename.ts): renamesfamily_profiles→representative_profiles, rewrites theFamilyrole toRepresentativeacross users, care-proposal status history, reviews, requests, notifications, and business feature flags, handling pre-created empty collections and merge-on-conflict. - 031 (
031-migrate-care-manager-to-custom-role.ts): for each business with "Care Manager" users, creates aBusinessRolenamed "Care Manager" withCARE_MANAGER_MIGRATION_PERMISSIONSfrom@anaya/shared, flips those users to roleCustom+customRole, and initializesbaseRolePermissionOverrideson all businesses — the data-side removal of the built-in CareManager role. - A long tail of permission-split migrations (041–051, 053, 055, 059) that reshape
BusinessPermissiongrants as the RBAC model evolved.
- 025 (
migrations/__tests__/contains 5 specs (001-create-verification-token-collection.spec.ts…) that test migrations which do not exist in the directory — the spec numbering refers to a different, older migration set and the tests re-declare expected constants locally rather than importing the scripts.- Separately,
package.jsonexposes one script-based migration:script:migrate-dnr-orders-to-advance-directives(apps/backend/package.json:26).
common/ — cross-cutting infrastructure (summary)
- Global providers (
app.module.ts:279-310):SentryGlobalFilter+HttpExceptionFilter; guardsBusinessGuard,PermissionsGuard,IdleTimeoutGuard; interceptorsRateLimitInterceptor,BusinessScopeInterceptor,PhiAccessAuditInterceptor.JwtAuthGuardis not global — it is applied per controller, with@Public()opting individual handlers out (auth/guards/jwt-auth.guard.ts). - Tenancy:
BusinessScopeInterceptorputs the caller's business id into CLS;createBusinessScopePlugin(common/plugins/business-scope.plugin.ts) is a global Mongoose plugin that injects{ business }into find/update/delete/count filters, prepends$matchto aggregations, and stampsdoc.businesson save for every schema with abusinesspath; SuperAdmins (null business) bypass it, and callers can opt out with the_bypassBusinessScopequery option. Full mechanics are documented in 11-identity-and-access.md — not duplicated here. - Audit decorators:
@AuditPhiAccess(see platform-logs above). - Misc:
RequestLoggerMiddlewareon all routes (skipping/admin/queues/), rate-limit decorator/interceptor, validation pipeline withwhitelist+forbidNonWhitelisted(main.ts:40-60), Helmet with CSP disabled and 1-year HSTS (main.ts:27-32), CORS origins required viaCORS_ORIGINSenv (regex-capable,main.ts:62-87), Swagger only outside production (main.ts:89-101).
Business Rules & Constraints
- File type and size limits are enforced server-side per type: image 15 MB, video 100 MB, document 100 MB, audio 50 MB, with extension allowlists (
files/interfaces/file-types.ts:1-22). - Alternative-dimension renditions are images-only; requesting them for other types is a 400 (
files/files.controller.ts:84-88). - All S3 objects are written with AES256 server-side encryption (
files/files.service.ts:117); private-object signed URLs expire after 1 hour (files/files.service.ts:369-371). - LockCare uploads are always private in S3 (
lock-care/lock-care.service.ts:423); reads requireLOCK_CARE_VIEW_ASSIGNED, writesLOCK_CARE_MANAGE(lock-care/lock-care.controller.ts). - Pagination in LockCare is clamped to 1–100 per page (
lock-care/lock-care.service.ts:150-151). - A client's
hasAdvanceDirectiveflag is derived solely from the count of LockCare documents in theADVANCE_DIRECTIVEfolder;hasDnrOrderis forcibly reset tofalseon every sync (clients/listeners/dnr-sync.listener.ts:38-43). - Platform logs are retained 6 years via Mongo TTL index, per the HIPAA citation in the schema (
platform-logs/entities/platform-log.entity.ts:61). - PHI access is logged only on successful responses and only when a
request.userexists; failures of the audit emit are swallowed (common/interceptors/phi-access-audit.interceptor.ts:50-83). - Platform settings are SuperAdmin-only and a single global document (
platform-settings/platform-settings.controller.ts:12; entitykey: 'global'unique). - A mobile build below
minVersion{Ios,Android}is force-updated; OTA force-reload additionally requires the globalisOtaUpdateRequiredflag (app-version/app-version.service.ts:43-59). - Statistics endpoints require
STATISTICS_VIEW_ALL; platform health requiresHEALTH_METRICS_VIEW_ASSIGNED; platform logs requirePLATFORM_LOGS_VIEW(respective controllers). - Internal endpoints are SuperAdmin-only and default to dry-run for destructive operations (
internal/internal.controller.ts:300-356). - Migrations refuse to run without
MONGO_URIand are executed manually per script (each migration's header).
Surfaces (Web & Mobile)
- Web — SuperAdmin platform panel
apps/web/app/(app)/(platform)/platform/: overview,settings/(app-version & store URLs form posting to/platform-settings),analytics/,verifications/(with its own statistics widgets),account-deletions/, plus businesses/users/trainings/etc. - Web — Admin dashboard
apps/web/app/(app)/(admin)/dashboard/:statistics/page.tsx(+super-admin-statistics.tsx),platform-logs/page.tsx(+ filters/table components),_components/platform-health/(health card on the super-admin dashboard,super-admin-dashboard.tsx). - Web — LockCare: per-client vault at
apps/web/app/(app)/(admin)/dashboard/clients/[id]/edit/lock-care/page.tsx; doctors-appointments attachment components also use LockCare files (.../doctors-appointments/_components/view-attachment-files.tsx). - Mobile: LockCare API client
apps/mobile/lib/lockare-api.ts(note the filename typo "lockare"); force-update hookapps/mobile/hooks/use-force-update.tswired into profile/me screens (apps/mobile/app/(app)/(me)/index.tsx). - Backend-served UI: Bull Board at
/admin/queues; Swagger at/in non-production only (main.ts:89-101).
Cross-Module Dependencies
This is the consumed-by-everything layer. Main consumers found in code:
- FilesService (~54 non-files files reference it): avatar generation, LockCare, users/auth (profile images, verification docs), clients (photos, care-plan PDFs upload), care-proposals (signed PDFs), business, notification, chat attachments, etc.
- PdfService:
care-proposals/care-proposals.service.ts(proposal + signed-proposal PDFs),clients/services/client-care-plans.service.ts,clients/services/clients.service.ts,clients/services/shift-summary.service.ts,clients/services/client-doctors-appointments.service.ts,health-metrics/health-metrics-export.service.ts,shifts/shift-report-export.service.ts,trainings/services/training-certificate.service.ts(Puppeteer path). - AvatarService:
users/users.service.ts,authsignup flows,business/business.service.ts,clients/services/clients.service.ts, notification module (system avatars). - GoogleMapsService:
users/users.service.tsandclients/services/clients.service.ts(timezone resolution from address), web/mobile address autocomplete via the controller. See 01-clients.md for client addresses. - LockCare: clients module (advance directives → DNR flags, doctors-appointment attachments, task submissions), care-proposals (signed documents,
CareProviderFolderType.SIGNED_CARE_PROPOSAL), mobile document features. See 02-assessments.md / 03-care-proposals.md. - Platform logs (event emitters):
auth,announcements,base-rates,business,care-proposals(+reviews),client-meals(×2 services), eightclients/services/*services,essential-needs,health-observations,home-inventories,incident-reports,initial-assessments,job-postings(applications),requests,shifts(+care-readiness),users(care-provider separation),wound-care— i.e., effectively every domain module. PHI auditing additionally hooks clients/health-metrics/incident-reports/wound-care/shift-summary controllers. - PlatformSettings → AppVersion → mobile: settings feed the version check consumed by the Expo app.
- Queues: registered by shifts, clients, client-meals, client-engagements, trainings, wound-care, notification, care-proposals modules; surfaced by bull-board and platform-health. Siblings: 05-scheduling-and-shifts.md, 12-communication.md (notification queue), 04-care-plans-and-tasks.md.
- Tenancy plugin/guards: every schema with a
businesspath; see 11-identity-and-access.md.
Open Questions & Gaps
- Security — unauthenticated dev endpoints live in production.
DevControllerhas no guards (dev/dev.controller.ts:13) andDevModuleis registered unconditionally (app.module.ts:265).GET /dev/users-by-roleenumerates real user emails, andPOST /dev/clients/:clientId/seed-templatecan overwrite a real client's medical record and assessments — all without a token, in any environment. - Security — Bull Board has no authentication. The queue dashboard at
/admin/queues(bull-board/bull-board.module.ts:9-11) is mounted with no guard or middleware; job payloads (which can include user/client identifiers) are visible to anyone who can reach the API. Cannot determine from code whether an infrastructure-level protection (e.g. proxy auth) exists in deployment. - Security —
GET /files/file/:keyis@Public()(files/files.controller.ts:176-179): anyone who knows (or guesses/leaks) an S3 key can stream the object — including private, HIPAA-relevant documents — without authentication, bypassing the 1-hour signed-URL discipline used everywhere else. Keys are UUID-based (hard to guess) but appear in logs and client payloads. Note Express:keywon't match/so folder-prefixed keys require URL-encoding; whether this endpoint works at all for nested keys cannot be determined from code alone. - Security — no per-object authorization on file endpoints. Authenticated users can call
DELETE /files/file/:key,POST /files/file/copy-private-to-public, andPUT /files/file/:key/update-to-privatefor any key in the bucket (files/files.controller.ts:198-230) — there is no ownership or business check, so any logged-in user (including Family/CareProvider) can delete or publicize another tenant's private files. - LockCare is not tenant-scoped. The
LockCareentity has nobusinessfield, so the business-scope plugin does not apply, andGET /lock-care/by-owner/.../by-metadataonly check theLOCK_CARE_VIEW_ASSIGNEDpermission — not that the owner belongs to the caller's business or that the caller is assigned to that client (lock-care/lock-care.controller.ts:103-135,lock-care/lock-care.entity.ts). A user in business A with the view permission can read business B's documents by id. - PHI audit coverage is partial and entity-id resolution is weak. Only 5 controllers use
@AuditPhiAccess; many other PHI surfaces (medications, assessments, daily-living, LockCare document reads) emit nophi_viewedevents. The interceptor falls back toentityId: 'unknown'when the route param isn'tid/clientId(common/interceptors/phi-access-audit.interceptor.ts:57-60). Whether this coverage is intentional cannot be determined from code. - Migrations have no execution tracking. No registry/runner — which migrations have run against which environment is unknowable from the database; numbering collisions (
005,009,010,050) and gaps (014,052) make ordering ambiguous;migrations/__tests__/tests reference migration scripts that don't exist in the directory. - Queue observability mismatch. 5 of 14 registered queues (
appointment-history-generation,shift-engagement-assignments,shift-meal-assignments,training-generation,wound-analysis) appear in neither Bull Board (bull-board/bull-board.module.ts) nor the platform-healthQueueIndicator(platform-health/indicators/queue.indicator.ts:14-24), so their failures are invisible to operators. lock-carequery params parsed unsafely in places:GET /lock-caredoesJSON.parse(query.metadata)without try/catch (lock-care/lock-care.controller.ts:42-44), so malformed JSON yields a 500 rather than a 400 (the sibling endpoints do handleexcludedMetadataparse errors).findByOwnerloads all matching documents into memory before deduplicating/paginating (lock-care/lock-care.service.ts:176-195) — fine at current scale, but pagination is not pushed to the database on that path (unlikefindByMetadata).- Avatar palette mentions "GCS" / watermark text "Geriatric Care Solution" (
avatar/avatar.service.ts:11,files/files.controller.ts:151) — apparent legacy branding; intended current behavior cannot be determined from code. platform-healthpermission choice: the endpoint is gated byHEALTH_METRICS_VIEW_ASSIGNED(platform-health/platform-health.controller.ts:32) — a client-health-metrics permission reused for infrastructure health; whether this is intentional cannot be determined from code.- Empty
PdfController(pdf/pdf.controller.ts) registers a/pdfroute prefix with no handlers — dead surface area.