evaldoc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.env.staging.example +19 -0
  2. package/.firebaserc +6 -0
  3. package/.github/workflows/auto-merge-to-main.yml +28 -0
  4. package/.github/workflows/ci.yml +27 -0
  5. package/.github/workflows/npxhub-publish.yml +170 -0
  6. package/.prettierignore +4 -0
  7. package/.prettierrc.json +7 -0
  8. package/README.md +142 -0
  9. package/SRS.md +448 -0
  10. package/apphosting.staging.yaml +48 -0
  11. package/apphosting.yaml +50 -0
  12. package/bin/evaldoc.cjs +105 -0
  13. package/cors.json +8 -0
  14. package/docs/ARCHITECTURE.md +769 -0
  15. package/docs/ARCHITECTURE_SUMMARY.md +323 -0
  16. package/docs/DEPLOYMENT.md +86 -0
  17. package/docs/DEPLOYMENT_STEPS.md +373 -0
  18. package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
  19. package/docs/DEPLOY_STORAGE_RULES.md +82 -0
  20. package/docs/DOCTOR_TUTORIAL.md +467 -0
  21. package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
  22. package/docs/FIXES_APPLIED.md +128 -0
  23. package/docs/IMPLEMENTATION.md +498 -0
  24. package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
  25. package/docs/LLM-SETUP.md +216 -0
  26. package/docs/MANUAL_TEST_GUIDE.md +327 -0
  27. package/docs/PATIENT_GUIDE.md +451 -0
  28. package/docs/PLATFORM_OVERVIEW.md +194 -0
  29. package/docs/QUICKSTART.md +252 -0
  30. package/docs/QUICK_REFERENCE.md +222 -0
  31. package/docs/READY_TO_TEST.md +383 -0
  32. package/docs/RUN.md +237 -0
  33. package/docs/START_TESTING.md +130 -0
  34. package/docs/STRIPE_CONNECT_SETUP.md +454 -0
  35. package/docs/SYSTEM_READY.md +345 -0
  36. package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
  37. package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
  38. package/docs/firebasefiles.md +21 -0
  39. package/docs/homepage.md +1208 -0
  40. package/docs/payments.md +159 -0
  41. package/eslint.config.js +76 -0
  42. package/firebase.json +27 -0
  43. package/firestore.indexes.json +88 -0
  44. package/firestore.rules +125 -0
  45. package/functions/package-lock.json +2884 -0
  46. package/functions/package.json +20 -0
  47. package/functions/src/analytics.js +101 -0
  48. package/functions/src/audit.js +28 -0
  49. package/functions/src/auth.js +19 -0
  50. package/functions/src/drafts.js +128 -0
  51. package/functions/src/feedback.js +164 -0
  52. package/functions/src/firebase-init.js +29 -0
  53. package/functions/src/i18n.js +134 -0
  54. package/functions/src/limits.js +27 -0
  55. package/functions/src/llm.js +271 -0
  56. package/functions/src/ner.js +162 -0
  57. package/functions/src/notifications.js +273 -0
  58. package/functions/src/orgs.js +127 -0
  59. package/functions/src/patient-api.js +545 -0
  60. package/functions/src/payment.js +830 -0
  61. package/functions/src/safety.js +188 -0
  62. package/functions/src/stripe-config.js +33 -0
  63. package/functions/src/topics.js +94 -0
  64. package/functions/src/triage.js +191 -0
  65. package/functions/src/utils.js +28 -0
  66. package/functions/src/validate.js +177 -0
  67. package/functions/test-firebase.js +36 -0
  68. package/jest.config.js +18 -0
  69. package/package.json +52 -0
  70. package/public/404.html +93 -0
  71. package/public/app/analytics.html +336 -0
  72. package/public/app/analytics.js +177 -0
  73. package/public/app/app.css +1400 -0
  74. package/public/app/app.js +1754 -0
  75. package/public/app/config.js +14 -0
  76. package/public/app/inbox-threads.js +843 -0
  77. package/public/app/inbox.html +598 -0
  78. package/public/app/inbox.js +397 -0
  79. package/public/app/index.html +612 -0
  80. package/public/assets/logo.jpg +0 -0
  81. package/public/chat.html +137 -0
  82. package/public/css/chat.css +870 -0
  83. package/public/css/landing-enhance.css +298 -0
  84. package/public/css/styles.css +1198 -0
  85. package/public/favicon.svg +5 -0
  86. package/public/find-doctor.html +220 -0
  87. package/public/index.html +399 -0
  88. package/public/js/chat-threads.js +513 -0
  89. package/public/js/chat.js +832 -0
  90. package/public/js/search.js +41 -0
  91. package/public/privacy.html +101 -0
  92. package/public/robots.txt +6 -0
  93. package/public/site.webmanifest +12 -0
  94. package/public/terms.html +110 -0
  95. package/scripts/create-test-provider.js +55 -0
  96. package/scripts/deploy-staging.sh +51 -0
  97. package/scripts/fix-provider.js +39 -0
  98. package/scripts/quick-test.sh +37 -0
  99. package/scripts/test-chat.sh +54 -0
  100. package/scripts/test-multi-turn.js +137 -0
  101. package/scripts/test-ner-simple.js +28 -0
  102. package/scripts/test-ner.js +60 -0
  103. package/scripts/test-stripe-connect.js +304 -0
  104. package/scripts/test-webhook-secret.sh +24 -0
  105. package/server.js +3864 -0
  106. package/storage.rules +22 -0
  107. package/test-api.json +1 -0
  108. package/tests/emulator/README.md +38 -0
  109. package/tests/emulator/counters.emulator.test.js +81 -0
  110. package/tests/integration/api-account-selfserve.test.js +312 -0
  111. package/tests/integration/api-admin-digest.test.js +202 -0
  112. package/tests/integration/api-chat-satisfaction.test.js +259 -0
  113. package/tests/integration/api-doctor-drafts.test.js +236 -0
  114. package/tests/integration/api-doctor-inbox.test.js +401 -0
  115. package/tests/integration/api-org.test.js +328 -0
  116. package/tests/integration/api-payment.test.js +664 -0
  117. package/tests/integration/api-provider-settings.test.js +475 -0
  118. package/tests/integration/api-qr.test.js +121 -0
  119. package/tests/integration/api-security.test.js +241 -0
  120. package/tests/integration/api-stripe-search.test.js +303 -0
  121. package/tests/integration/api-webhook-deletion.test.js +231 -0
  122. package/tests/mocks/firebase-admin.js +220 -0
  123. package/tests/unit/analytics.test.js +151 -0
  124. package/tests/unit/audit-bugs.test.js +203 -0
  125. package/tests/unit/chat-ui-logic.test.js +283 -0
  126. package/tests/unit/drafts.test.js +104 -0
  127. package/tests/unit/feedback.test.js +148 -0
  128. package/tests/unit/i18n.test.js +229 -0
  129. package/tests/unit/llm-retry.test.js +122 -0
  130. package/tests/unit/notifications.test.js +155 -0
  131. package/tests/unit/orgs.test.js +328 -0
  132. package/tests/unit/payment.test.js +889 -0
  133. package/tests/unit/safety-edge-cases.test.js +509 -0
  134. package/tests/unit/safety-srs-compliance.test.js +252 -0
  135. package/tests/unit/safety.test.js +291 -0
  136. package/tests/unit/server-helpers.test.js +774 -0
  137. package/tests/unit/topics-limits.test.js +158 -0
  138. package/tests/unit/triage.test.js +152 -0
  139. package/tests/unit/validate.test.js +408 -0
@@ -0,0 +1,159 @@
1
+ You are a senior full-stack engineer. Implement Stripe payments for EvalDoc (a healthcare AI link platform) using Node.js + Firebase (Firestore + Auth) + Stripe.
2
+
3
+ Goal:
4
+ - Doctors set their own subscription prices (daily/weekly/monthly).
5
+ - Patients must pay before accessing the doctor’s AI session.
6
+ - Platform (EvalDoc) takes 25% of the doctor’s fee.
7
+ - Doctor receives 75% via Stripe Connect Express payouts.
8
+ - Access is controlled strictly by Stripe webhook events (not by client redirects).
9
+
10
+ Tech stack:
11
+ - Backend: Node.js (Express) deployed on Firebase Cloud Functions (or Cloud Run if you prefer).
12
+ - DB: Firestore.
13
+ - Auth: Firebase Auth.
14
+ - Payments: Stripe Checkout + Stripe Connect (Express).
15
+ - Use Stripe test mode for development, environment variables for keys.
16
+
17
+ Deliverables:
18
+ 1) A working backend with endpoints to:
19
+ A. Onboard a doctor to Stripe Connect Express.
20
+ B. Create/update doctor-specific Products + Prices (daily/weekly/monthly).
21
+ C. Create a patient Stripe Customer if needed.
22
+ D. Create a Stripe Checkout Session (subscription) for a patient buying a doctor plan, charging on behalf of the doctor with a 25% application fee.
23
+ E. Webhook endpoint to process Stripe events and update Firestore access.
24
+ 2) Firestore data model for doctors, patients, prices, subscriptions, and access.
25
+ 3) Minimal client-side flow description (no full UI needed, just required calls and checks).
26
+
27
+ IMPORTANT BUSINESS LOGIC:
28
+ - A patient can only access a doctor AI session if Firestore shows access.status == "ACTIVE" for that doctor + patient, based on Stripe webhooks.
29
+ - A doctor must complete Stripe onboarding (charges_enabled / payouts_enabled) before patients can purchase.
30
+ - Platform fee: 25% of the subscription amount.
31
+ - Use destination charges / connected account routing so money flows to doctor and platform gets its fee.
32
+
33
+ ========================
34
+ FIRESTORE MODEL
35
+ ========================
36
+ Create collections:
37
+
38
+ doctors/{doctorId}
39
+ - displayName
40
+ - stripeAccountId (string)
41
+ - stripeOnboardingStatus: "NOT_STARTED" | "PENDING" | "COMPLETE"
42
+ - chargesEnabled (bool)
43
+ - payoutsEnabled (bool)
44
+ - stripeProductId (string)
45
+ - stripePriceIds: { day?: string, week?: string, month?: string }
46
+ - currency (e.g., "usd")
47
+ - createdAt, updatedAt
48
+
49
+ patients/{patientId}
50
+ - stripeCustomerId (string)
51
+ - createdAt, updatedAt
52
+
53
+ subscriptions/{subId}
54
+ - doctorId
55
+ - patientId
56
+ - stripeSubscriptionId
57
+ - stripeCustomerId
58
+ - stripePriceId
59
+ - status (active, past_due, canceled, unpaid, etc.)
60
+ - currentPeriodEnd (timestamp)
61
+ - createdAt, updatedAt
62
+
63
+ access/{doctorId_patientId}
64
+ - doctorId
65
+ - patientId
66
+ - status: "ACTIVE" | "INACTIVE"
67
+ - stripeSubscriptionId
68
+ - currentPeriodEnd (timestamp)
69
+ - updatedAt
70
+
71
+ ========================
72
+ ENDPOINTS TO IMPLEMENT
73
+ ========================
74
+
75
+ 1) POST /stripe/doctor/create-connect-account
76
+ Auth: doctor must be logged in (Firebase Auth).
77
+ - Create Stripe Connect Express account for the doctor if not exists.
78
+ - Save stripeAccountId to doctors/{doctorId}.
79
+ - Return accountId.
80
+
81
+ 2) POST /stripe/doctor/create-account-link
82
+ Auth: doctor logged in.
83
+ - Create an Account Link (Stripe-hosted onboarding) using doctor.stripeAccountId
84
+ - Requires refresh_url + return_url from env
85
+ - Return the url for the doctor to complete onboarding.
86
+
87
+ 3) POST /stripe/doctor/set-pricing
88
+ Auth: doctor logged in.
89
+ Input: currency, amounts for intervals (day/week/month), enabledIntervals list.
90
+ - Validate min amounts, currency format, etc.
91
+ - Ensure doctor has stripeAccountId and (optionally) charges_enabled/payouts_enabled or allow pricing setup before completion.
92
+ - Create a Stripe Product for the doctor if not exists (name: "EvalDoc AI Access - Dr. {displayName}")
93
+ - For each enabled interval, create (or replace) Stripe Price objects:
94
+ - recurring.interval = "day" | "week" | "month"
95
+ - unit_amount in cents
96
+ - Save stripeProductId and stripePriceIds.* in Firestore.
97
+
98
+ 4) POST /stripe/patient/create-checkout-session
99
+ Auth: patient logged in.
100
+ Input: doctorId, interval ("day"/"week"/"month")
101
+ - Load doctor document; require:
102
+ - stripeAccountId exists
103
+ - chargesEnabled == true (or at least doctor onboarding complete)
104
+ - selected priceId exists
105
+ - Ensure patient has stripeCustomerId (create stripe customer if missing).
106
+ - Create a Stripe Checkout Session in mode="subscription", line_items=[{price: doctorPriceId, quantity:1}], customer=patientCustomerId.
107
+ - IMPORTANT: route funds to doctor + take 25% platform fee.
108
+ - Use Stripe Connect approach for subscriptions:
109
+ - Use application_fee_percent = 25 (or application_fee_amount if you compute fixed cents)
110
+ - Ensure the payment is made for the connected account (destination / transfer settings).
111
+ - If there are multiple Stripe ways, choose the correct one for subscriptions with Connect and document it in comments.
112
+ - Provide success_url and cancel_url including session_id.
113
+ - Return checkout session url.
114
+
115
+ 5) POST /stripe/webhook
116
+ No auth; verify Stripe signature using STRIPE_WEBHOOK_SECRET.
117
+ - Handle these events at minimum:
118
+ - checkout.session.completed
119
+ - customer.subscription.created
120
+ - customer.subscription.updated
121
+ - customer.subscription.deleted
122
+ - invoice.payment_succeeded
123
+ - invoice.payment_failed
124
+ - For each relevant event, resolve doctorId + patientId:
125
+ - Store doctorId and patientId as metadata when creating the Checkout Session (recommended).
126
+ - Update Firestore:
127
+ - subscriptions/{subId} upsert by stripeSubscriptionId
128
+ - access/{doctorId_patientId} set ACTIVE if subscription is active and paid; set INACTIVE on canceled/unpaid/past_due/payment_failed depending on policy.
129
+ - Save currentPeriodEnd from Stripe subscription.
130
+
131
+ ========================
132
+ ACCESS POLICY
133
+ ========================
134
+ - ACTIVE only when subscription.status == "active" and latest invoice is paid (or invoice.payment_succeeded).
135
+ - INACTIVE on:
136
+ - invoice.payment_failed (immediately) OR after a grace period (choose and implement one; default immediate for now).
137
+ - subscription canceled/deleted.
138
+
139
+ ========================
140
+ SECURITY REQUIREMENTS
141
+ ========================
142
+ - All doctor endpoints require Firebase Auth and ensure role=doctor (use a custom claim or doctors collection check).
143
+ - Patient endpoint requires Firebase Auth and ensure role=patient.
144
+ - Prevent patients from purchasing if doctor onboarding incomplete.
145
+ - Never trust client to mark access active; only webhooks do that.
146
+ - Add idempotency in webhook handling (avoid duplicate writes; use stripe event id as processed marker, or upsert by subscription id).
147
+
148
+ ========================
149
+ OUTPUT FORMAT
150
+ ========================
151
+ - Provide:
152
+ 1) Folder/file structure.
153
+ 2) Environment variables list.
154
+ 3) Implementation code (backend) with clear comments.
155
+ 4) Example Firestore documents.
156
+ 5) Example requests/responses.
157
+ 6) A short “How to test in Stripe test mode” checklist.
158
+
159
+ Proceed now. Make reasonable assumptions. If multiple Stripe Connect approaches exist for subscriptions, pick one that works and explain why in code comments.
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ // ESLint 9 flat config for a Node.js CommonJS (Express) project.
4
+ //
5
+ // Node + Jest globals are declared inline so this config has no external
6
+ // dependencies at parse time (works even before `npm install`). If you prefer,
7
+ // you can replace the inline maps below with the `globals` package:
8
+ // const globals = require('globals');
9
+ // ...globals.node, ...globals.jest
10
+
11
+ const nodeGlobals = {
12
+ process: 'readonly',
13
+ __dirname: 'readonly',
14
+ __filename: 'readonly',
15
+ module: 'writable',
16
+ require: 'readonly',
17
+ exports: 'writable',
18
+ console: 'readonly',
19
+ Buffer: 'readonly',
20
+ global: 'readonly',
21
+ globalThis: 'readonly',
22
+ setTimeout: 'readonly',
23
+ clearTimeout: 'readonly',
24
+ setInterval: 'readonly',
25
+ clearInterval: 'readonly',
26
+ setImmediate: 'readonly',
27
+ clearImmediate: 'readonly',
28
+ queueMicrotask: 'readonly',
29
+ URL: 'readonly',
30
+ URLSearchParams: 'readonly',
31
+ TextEncoder: 'readonly',
32
+ TextDecoder: 'readonly',
33
+ AbortController: 'readonly',
34
+ fetch: 'readonly',
35
+ structuredClone: 'readonly'
36
+ };
37
+
38
+ const jestGlobals = {
39
+ describe: 'readonly',
40
+ it: 'readonly',
41
+ test: 'readonly',
42
+ expect: 'readonly',
43
+ beforeAll: 'readonly',
44
+ afterAll: 'readonly',
45
+ beforeEach: 'readonly',
46
+ afterEach: 'readonly',
47
+ jest: 'readonly',
48
+ xdescribe: 'readonly',
49
+ xit: 'readonly',
50
+ xtest: 'readonly',
51
+ fdescribe: 'readonly',
52
+ fit: 'readonly'
53
+ };
54
+
55
+ module.exports = [
56
+ {
57
+ ignores: ['node_modules/**', 'coverage/**', 'public/vendor/**', 'functions/node_modules/**']
58
+ },
59
+ {
60
+ files: ['**/*.js'],
61
+ languageOptions: {
62
+ ecmaVersion: 2022,
63
+ sourceType: 'commonjs',
64
+ globals: {
65
+ ...nodeGlobals,
66
+ ...jestGlobals
67
+ }
68
+ },
69
+ rules: {
70
+ 'no-unused-vars': 'warn',
71
+ 'no-undef': 'error',
72
+ eqeqeq: 'warn',
73
+ 'no-console': 'off'
74
+ }
75
+ }
76
+ ];
package/firebase.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "hosting": {
3
+ "public": "public",
4
+ "ignore": [
5
+ "firebase.json",
6
+ "**/.*",
7
+ "**/node_modules/**"
8
+ ],
9
+ "rewrites": [
10
+ {
11
+ "source": "/app/**",
12
+ "destination": "/app/index.html"
13
+ },
14
+ {
15
+ "source": "**",
16
+ "destination": "/index.html"
17
+ }
18
+ ]
19
+ },
20
+ "firestore": {
21
+ "rules": "firestore.rules",
22
+ "indexes": "firestore.indexes.json"
23
+ },
24
+ "storage": {
25
+ "rules": "storage.rules"
26
+ }
27
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "indexes": [
3
+ {
4
+ "collectionGroup": "conversations",
5
+ "queryScope": "COLLECTION",
6
+ "fields": [
7
+ { "fieldPath": "providerId", "order": "ASCENDING" },
8
+ { "fieldPath": "sessionId", "order": "ASCENDING" },
9
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
10
+ ]
11
+ },
12
+ {
13
+ "collectionGroup": "conversations",
14
+ "queryScope": "COLLECTION",
15
+ "fields": [
16
+ { "fieldPath": "providerId", "order": "ASCENDING" },
17
+ { "fieldPath": "updatedAt", "order": "DESCENDING" }
18
+ ]
19
+ },
20
+ {
21
+ "collectionGroup": "threads",
22
+ "queryScope": "COLLECTION",
23
+ "fields": [
24
+ { "fieldPath": "doctorId", "order": "ASCENDING" },
25
+ { "fieldPath": "lastMessageAt", "order": "DESCENDING" }
26
+ ]
27
+ },
28
+ {
29
+ "collectionGroup": "threads",
30
+ "queryScope": "COLLECTION",
31
+ "fields": [
32
+ { "fieldPath": "doctorId", "order": "ASCENDING" },
33
+ { "fieldPath": "status", "order": "ASCENDING" },
34
+ { "fieldPath": "lastMessageAt", "order": "DESCENDING" }
35
+ ]
36
+ },
37
+ {
38
+ "collectionGroup": "threads",
39
+ "queryScope": "COLLECTION",
40
+ "fields": [
41
+ { "fieldPath": "doctorId", "order": "ASCENDING" },
42
+ { "fieldPath": "triageCategory", "order": "ASCENDING" },
43
+ { "fieldPath": "lastMessageAt", "order": "DESCENDING" }
44
+ ]
45
+ },
46
+ {
47
+ "collectionGroup": "threads",
48
+ "queryScope": "COLLECTION",
49
+ "fields": [
50
+ { "fieldPath": "patientId", "order": "ASCENDING" },
51
+ { "fieldPath": "lastMessageAt", "order": "DESCENDING" }
52
+ ]
53
+ },
54
+ {
55
+ "collectionGroup": "notifications",
56
+ "queryScope": "COLLECTION",
57
+ "fields": [
58
+ { "fieldPath": "doctorId", "order": "ASCENDING" },
59
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
60
+ ]
61
+ },
62
+ {
63
+ "collectionGroup": "notifications",
64
+ "queryScope": "COLLECTION",
65
+ "fields": [
66
+ { "fieldPath": "doctorId", "order": "ASCENDING" },
67
+ { "fieldPath": "read", "order": "ASCENDING" }
68
+ ]
69
+ },
70
+ {
71
+ "collectionGroup": "doctorExamples",
72
+ "queryScope": "COLLECTION",
73
+ "fields": [
74
+ { "fieldPath": "topic", "order": "ASCENDING" },
75
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
76
+ ]
77
+ },
78
+ {
79
+ "collectionGroup": "waitlist",
80
+ "queryScope": "COLLECTION",
81
+ "fields": [
82
+ { "fieldPath": "email", "order": "ASCENDING" },
83
+ { "fieldPath": "approved", "order": "ASCENDING" }
84
+ ]
85
+ }
86
+ ],
87
+ "fieldOverrides": []
88
+ }
@@ -0,0 +1,125 @@
1
+ rules_version = '2';
2
+ service cloud.firestore {
3
+ match /databases/{database}/documents {
4
+ function isSignedIn() {
5
+ return request.auth != null;
6
+ }
7
+
8
+ function isOwner(ownerUid) {
9
+ return isSignedIn() && request.auth.uid == ownerUid;
10
+ }
11
+
12
+ // Provider documents
13
+ match /providers/{providerId} {
14
+ // Public: Anyone can read published providers
15
+ allow read: if resource.data.status == "published";
16
+
17
+ // Providers can only be created via backend API (Admin SDK)
18
+ allow create: if false;
19
+
20
+ // Providers can update their own profile
21
+ allow update: if isOwner(resource.data.ownerUid);
22
+
23
+ // Only backend can delete
24
+ allow delete: if false;
25
+
26
+ // Analytics subcollection
27
+ match /analyticsDaily/{dayId} {
28
+ allow read: if isOwner(providerId);
29
+ allow write: if false;
30
+ }
31
+
32
+ // Doctor examples subcollection
33
+ match /doctorExamples/{exampleId} {
34
+ allow read: if isSignedIn() && request.auth.uid == get(/databases/$(database)/documents/providers/$(providerId)).data.ownerUid;
35
+ allow write: if false;
36
+ }
37
+ }
38
+
39
+ // Handle lookup
40
+ match /handles/{handle} {
41
+ allow read: if true;
42
+ allow write: if false;
43
+ }
44
+
45
+ // Conversations (legacy)
46
+ match /conversations/{conversationId} {
47
+ allow read, write: if false;
48
+
49
+ match /messages/{messageId} {
50
+ allow read, write: if false;
51
+ }
52
+ }
53
+
54
+ // Safety events
55
+ match /safetyEvents/{eventId} {
56
+ allow read, write: if false;
57
+ }
58
+
59
+ // Audit log — backend-only, no client access
60
+ match /auditLog/{logId} {
61
+ allow read, write: if false;
62
+ }
63
+
64
+ // Platform configuration
65
+ match /platformConfig/{docId} {
66
+ allow read: if true;
67
+ allow write: if false;
68
+ }
69
+
70
+ // Payment access records: patients can read their own access status
71
+ match /access/{accessId} {
72
+ allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
73
+ allow write: if false;
74
+ }
75
+
76
+ // Subscriptions: patients can read their own
77
+ match /subscriptions/{subscriptionId} {
78
+ allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
79
+ allow write: if false;
80
+ }
81
+
82
+ // Transactions: patients can read their own
83
+ match /transactions/{transactionId} {
84
+ allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
85
+ allow write: if false;
86
+ }
87
+
88
+ // Patients collection
89
+ match /patients/{patientId} {
90
+ allow read, update: if isSignedIn() && request.auth.uid == patientId;
91
+ allow create, delete: if false;
92
+ }
93
+
94
+ // Notifications: doctors read their own
95
+ match /notifications/{notificationId} {
96
+ allow read: if isSignedIn() && resource.data.doctorId == request.auth.uid;
97
+ allow write: if false;
98
+ }
99
+
100
+ // Threads
101
+ match /threads/{threadId} {
102
+ allow read: if isSignedIn() && resource.data.patientId == request.auth.uid;
103
+ allow read: if isSignedIn() && request.auth.token.doctorId == resource.data.doctorId;
104
+ allow write: if false;
105
+
106
+ match /messages/{messageId} {
107
+ allow read: if isSignedIn() && (
108
+ get(/databases/$(database)/documents/threads/$(threadId)).data.patientId == request.auth.uid ||
109
+ request.auth.token.doctorId == get(/databases/$(database)/documents/threads/$(threadId)).data.doctorId
110
+ );
111
+ allow write: if false;
112
+ }
113
+ }
114
+
115
+ // Waitlist — all access via server-side Admin SDK only
116
+ match /waitlist/{docId} {
117
+ allow read, write: if false;
118
+ }
119
+
120
+ // Deny all other documents by default
121
+ match /{document=**} {
122
+ allow read, write: if false;
123
+ }
124
+ }
125
+ }