Anaya Care Docs

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

FieldTypeNotes
keystring, requiredS3 object key
namestringDisplay filename (original upload name; renameable)
sizenumberBytes
mimeTypestringFrom the uploaded file
descriptionstring, optional
ownerTypeenum FileOwnerType, requiredUSER | CLIENT | CARE_PROPOSAL (packages/shared/src/enums/lock-care.ts)
ownerIdObjectId, requiredUntyped ref — resolved per ownerType
uploadedByObjectId ref User, requiredPopulated in list queries
tagsstring[]Default []
uploadedAtDateDefault Date.now
metadataMixedFree-form; metadata.folder carries the folder enum value
blurhashstring | nullGenerated for image uploads
urlvirtualSigned 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

FieldTypeNotes
actorObjectId ref User, requiredWho performed the action
entityTypeenum PlatformLogEntityType, requiredShared enum (packages/shared/src/enums/domain-status.ts)
entityIdstring, requiredId of the affected entity
actionenum 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+)
businessObjectId ref Business | nullTenant; null for platform-level events
descriptionstringAuto-generated if not supplied (platform-logs/platform-logs.service.ts:53-55)
metadataobjectFree-form
ipAddressstring, 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 from S3_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 to uploads/ (:252-260). Objects are stored with ServerSideEncryption: AES256 and ACL public-read or private per the isPublic flag (: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-level JwtAuthGuard): POST /files/:type and POST /files/:type/batch (multipart upload), POST /files/download (bundle images as pdf-print/pdf-standard/png-zip via 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 commit 46e2daa3).

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.createAvatarImage draws a 1000×1000 PNG on a @napi-rs/canvas canvas: linear gradient from the role's palette (getRoleGradientColors in @anaya/shared, plus local Client/GCS palettes) with up to two white initials in DM Sans (avatar/avatar.service.ts:42-100).
  • createAndUploadAvatar uploads the PNG via FilesService as a public file under folder avatars/ (or caller-supplied folder) and returns the URL (:102-130); generateAvatarUrlIfMissing is 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: uploadFileToS3AndSaveToLockCare uploads the file private (isPublic: false), generates a blurhash for images, then creates the LockCare record (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 at metadata.folder (controller merges body.folder into 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).
  • getStatisticsByMetadataKey aggregates 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) or LOCK_CARE_MANAGE (upload/delete/rename) (lock-care/lock-care.controller.ts). Permission semantics are documented in 11-identity-and-access.md.
  • hasAdvanceDirective sync: createLockCare and delete emit lockcare.document.changed (lock-care/lock-care.service.ts:50-54, :319-323). DnrSyncListener (clients/listeners/dnr-sync.listener.ts) reacts when ownerType === CLIENT and metadata.folder === ADVANCE_DIRECTIVE: it counts remaining advance-directive documents and updates the client to { hasDnrOrder: false, hasAdvanceDirective: count > 0 }. hasDnrOrder is always forced to false here (a legacy flag retired by scripts/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 in pdf/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: generateTrainingCertificatePdf launches 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).
  • convertMarkdownToPdfStream renders 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/textkit glyph crashes (:73-105).
  • PdfController is 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-level JwtAuthGuard): 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 (commit 46e2daa3): non-OK responses log Google's real error body and surface as 503 ServiceUnavailableException ("Address lookup is temporarily unavailable") instead of unhandled 500s (:126-156).
  • getPlaceData fetches place details, parses address components (with fallbacks to adrFormatAddress span scraping and the user's typed selectedText), and geocodes route-level places to a street address when needed (:228-318).
  • getTimezone wraps the legacy @googlemaps/google-maps-services-js timezone API and returns the timeZoneId (:54-72) — consumed by user/client timezone resolution (internal/ migration endpoints, signup flows).

statistics/ — super-admin dashboards

  • StatisticsController requires JwtAuthGuard + PermissionsGuard + BusinessPermission.STATISTICS_VIEW_ALL on 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 over care_proposals; status distribution and totals over clients (statistics/statistics.service.ts:48-360). /overview composes 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 a business path), 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=true bypasses 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: QueueIndicator checks 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; PlatformLogListener consumes it and persists via PlatformLogsService.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-registered PhiAccessAuditInterceptor (app.module.ts:307-310) reads the metadata and, on successful response, emits a platform-log event with action PHI_VIEWED, entity id from params.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-logs and GET /platform-logs/my-activity (PLATFORM_LOGS_VIEW permission) with filters for actor/entityType/action/date-range/search, scoped to the caller's businessId when 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=&currentVersion=&currentUpdateId= 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 Expo updateId differs from the configured latest, and "required" only when isOtaUpdateRequired is also set (app-version/app-version.service.ts:17-60).
  • Mobile: apps/mobile/hooks/use-force-update.ts polls every 5 minutes (and on app foreground), skips in __DEV__, detects TestFlight installs via the itms-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):

QueueRegistered inIn Bull Board?
notificationmany modules (e.g. clients/clients.module.ts:244)yes
shifts, shift-reminders, care-readiness-assessmentshifts/shifts.module.ts:78-81yes
care-provider-tasks-generation, care-plan-generationclients/clients.module.ts:245-246yes
client-mealsclient-meals/client-meals.module.ts:40yes
client-engagement-activitiesclients/clients.module.ts:247yes
slides-video-mergetrainings/trainings.module.ts:56-58yes
appointment-history-generationclients/clients.module.ts:248no
shift-engagement-assignmentsclient-engagements/client-engagements.module.ts:44no
shift-meal-assignmentsclient-meals/client-meals.module.ts:41no
training-generationtrainings/trainings.module.ts:68-70no
wound-analysiswound-care/wound-care.module.tsno

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 001059, with duplicated numbers (005, 009, 010, 050) and gaps (014, 052); 010-backfill-post-notification-images.mongosh.js is a mongosh script rather than ts-node.
  • Notable migrations:
    • 025 (025-family-to-representative-rename.ts): renames family_profilesrepresentative_profiles, rewrites the Family role to Representative across 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 a BusinessRole named "Care Manager" with CARE_MANAGER_MIGRATION_PERMISSIONS from @anaya/shared, flips those users to role Custom + customRole, and initializes baseRolePermissionOverrides on 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 BusinessPermission grants as the RBAC model evolved.
  • 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.json exposes 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; guards BusinessGuard, PermissionsGuard, IdleTimeoutGuard; interceptors RateLimitInterceptor, BusinessScopeInterceptor, PhiAccessAuditInterceptor. JwtAuthGuard is not global — it is applied per controller, with @Public() opting individual handlers out (auth/guards/jwt-auth.guard.ts).
  • Tenancy: BusinessScopeInterceptor puts 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 $match to aggregations, and stamps doc.business on save for every schema with a business path; SuperAdmins (null business) bypass it, and callers can opt out with the _bypassBusinessScope query option. Full mechanics are documented in 11-identity-and-access.md — not duplicated here.
  • Audit decorators: @AuditPhiAccess (see platform-logs above).
  • Misc: RequestLoggerMiddleware on all routes (skipping /admin/queues/), rate-limit decorator/interceptor, validation pipeline with whitelist + forbidNonWhitelisted (main.ts:40-60), Helmet with CSP disabled and 1-year HSTS (main.ts:27-32), CORS origins required via CORS_ORIGINS env (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 require LOCK_CARE_VIEW_ASSIGNED, writes LOCK_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 hasAdvanceDirective flag is derived solely from the count of LockCare documents in the ADVANCE_DIRECTIVE folder; hasDnrOrder is forcibly reset to false on 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.user exists; 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; entity key: 'global' unique).
  • A mobile build below minVersion{Ios,Android} is force-updated; OTA force-reload additionally requires the global isOtaUpdateRequired flag (app-version/app-version.service.ts:43-59).
  • Statistics endpoints require STATISTICS_VIEW_ALL; platform health requires HEALTH_METRICS_VIEW_ASSIGNED; platform logs require PLATFORM_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_URI and 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 hook apps/mobile/hooks/use-force-update.ts wired 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, auth signup flows, business/business.service.ts, clients/services/clients.service.ts, notification module (system avatars).
  • GoogleMapsService: users/users.service.ts and clients/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), eight clients/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 business path; see 11-identity-and-access.md.

Open Questions & Gaps

  1. Security — unauthenticated dev endpoints live in production. DevController has no guards (dev/dev.controller.ts:13) and DevModule is registered unconditionally (app.module.ts:265). GET /dev/users-by-role enumerates real user emails, and POST /dev/clients/:clientId/seed-template can overwrite a real client's medical record and assessments — all without a token, in any environment.
  2. 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.
  3. Security — GET /files/file/:key is @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 :key won't match / so folder-prefixed keys require URL-encoding; whether this endpoint works at all for nested keys cannot be determined from code alone.
  4. Security — no per-object authorization on file endpoints. Authenticated users can call DELETE /files/file/:key, POST /files/file/copy-private-to-public, and PUT /files/file/:key/update-to-private for 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.
  5. LockCare is not tenant-scoped. The LockCare entity has no business field, so the business-scope plugin does not apply, and GET /lock-care/by-owner/.../by-metadata only check the LOCK_CARE_VIEW_ASSIGNED permission — 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.
  6. 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 no phi_viewed events. The interceptor falls back to entityId: 'unknown' when the route param isn't id/clientId (common/interceptors/phi-access-audit.interceptor.ts:57-60). Whether this coverage is intentional cannot be determined from code.
  7. 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.
  8. 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-health QueueIndicator (platform-health/indicators/queue.indicator.ts:14-24), so their failures are invisible to operators.
  9. lock-care query params parsed unsafely in places: GET /lock-care does JSON.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 handle excludedMetadata parse errors).
  10. findByOwner loads 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 (unlike findByMetadata).
  11. 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.
  12. platform-health permission choice: the endpoint is gated by HEALTH_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.
  13. Empty PdfController (pdf/pdf.controller.ts) registers a /pdf route prefix with no handlers — dead surface area.

On this page