create-ailk 0.23.0 → 0.25.1

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 (604) hide show
  1. package/README.md +68 -33
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +10 -3
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +38 -4
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +75 -22
  10. package/dist/lib/design-checks.d.ts +9 -4
  11. package/dist/lib/design-checks.js +13 -7
  12. package/dist/lib/extract-module-bundle.js +29 -2
  13. package/dist/lib/fetch-template-snapshot.d.ts +1 -1
  14. package/dist/lib/fetch-template-snapshot.js +7 -3
  15. package/dist/lib/import-graph.d.ts +1 -1
  16. package/dist/lib/import-graph.js +4 -4
  17. package/dist/lib/module-license-gate.d.ts +12 -1
  18. package/dist/lib/module-license-gate.js +34 -12
  19. package/dist/lib/scaffold-render-sweep.d.ts +33 -0
  20. package/dist/lib/scaffold-render-sweep.js +45 -0
  21. package/dist/module-architecture.d.ts +20 -3
  22. package/dist/module-architecture.js +122 -538
  23. package/dist/paid-sections.d.ts +25 -1
  24. package/dist/paid-sections.js +88 -3
  25. package/dist/surfaces.js +10 -2
  26. package/dist/sync-routes.d.ts +30 -4
  27. package/dist/sync-routes.js +271 -18
  28. package/dist/template-record.d.ts +14 -4
  29. package/dist/template-record.js +23 -5
  30. package/dist/topology.d.ts +16 -3
  31. package/dist/topology.js +31 -19
  32. package/dist/update.d.ts +10 -1
  33. package/dist/update.js +540 -16
  34. package/package.json +3 -3
  35. package/release-scope.json +1 -1
  36. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  37. package/templates/.claude/hooks/template-guard.sh +137 -0
  38. package/templates/.claude/rules/extending.md +106 -0
  39. package/templates/.claude/rules/loop-contract.md +2 -2
  40. package/templates/.claude/settings.json +11 -0
  41. package/templates/.env.example +9 -5
  42. package/templates/.github/workflows/ci.yml +7 -4
  43. package/templates/CLAUDE.md +15 -3
  44. package/templates/CONVENTIONS.md +8 -10
  45. package/templates/README.md +14 -13
  46. package/templates/apps/api/.env.example +9 -2
  47. package/templates/apps/api/CLAUDE.md +122 -82
  48. package/templates/apps/api/jest.config.cjs +15 -5
  49. package/templates/apps/api/package.json +16 -20
  50. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  51. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  52. package/templates/apps/api/src/index.ts +1 -1
  53. package/templates/apps/api/src/lib/__mocks__/prisma.ts +7 -4
  54. package/templates/apps/api/src/modules/commerce.ts +10 -0
  55. package/templates/apps/api/src/modules/content-write.ts +10 -0
  56. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  57. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  58. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  59. package/templates/apps/api/src/modules/workspace.ts +10 -0
  60. package/templates/apps/api/src/server.ts +117 -1857
  61. package/templates/apps/api/vercel.json +2 -2
  62. package/templates/apps/mcp/CLAUDE.md +77 -24
  63. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  64. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  65. package/templates/apps/mcp/jest.config.cjs +6 -1
  66. package/templates/apps/mcp/package.json +5 -12
  67. package/templates/apps/mcp/src/app.ts +8 -7
  68. package/templates/apps/mcp/src/index.ts +6 -34
  69. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  70. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  71. package/templates/apps/mcp/src/site.ts +12 -0
  72. package/templates/apps/web/.env.example +1 -1
  73. package/templates/apps/web/CLAUDE.md +21 -5
  74. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  75. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +1 -1
  76. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +14 -4
  77. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  78. package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
  79. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  80. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +28 -7
  81. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  82. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  83. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  84. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  85. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  86. package/templates/apps/web/auth.ts +2 -2
  87. package/templates/apps/web/jest.config.cjs +1 -0
  88. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +7 -0
  89. package/templates/apps/web/lib/auth-email.ts +1 -1
  90. package/templates/apps/web/lib/magnet-routes.ts +3 -1
  91. package/templates/apps/web/lib/newsletter.actions.ts +9 -6
  92. package/templates/apps/web/lib/relay.ts +2 -2
  93. package/templates/apps/web/lib/release-scope.ts +47 -0
  94. package/templates/apps/web/lib/server-api.ts +1 -1
  95. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  96. package/templates/customizations.yaml +37 -0
  97. package/templates/database/prisma.config.ts +10 -14
  98. package/templates/database/seed-test.ts +2 -2
  99. package/templates/database/seed.ts +32 -5
  100. package/templates/package.json +12 -6
  101. package/templates/pnpm-workspace.yaml +0 -1
  102. package/templates/project.yaml +16 -8
  103. package/templates/tools/jest/reporters.cjs +65 -0
  104. package/templates/tools/lint-architecture.ts +757 -7
  105. package/templates/tsconfig.json +1 -2
  106. package/versions.json +48 -35
  107. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  108. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  109. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  110. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  111. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  112. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  113. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  114. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  115. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  116. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  117. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  118. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  119. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  120. package/templates/apps/api/src/bin/spokes.ts +0 -198
  121. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  122. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  123. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  124. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  125. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  126. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  127. package/templates/apps/api/src/config/index.ts +0 -596
  128. package/templates/apps/api/src/config/modules.ts +0 -83
  129. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  130. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  131. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  132. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  133. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  134. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  135. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  136. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  137. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  138. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  139. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  140. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  141. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  142. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  143. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  144. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  145. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  146. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  147. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  148. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -292
  149. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  150. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  151. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  152. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  153. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  154. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  155. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  156. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  157. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  158. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  159. package/templates/apps/api/src/lib/errors.ts +0 -95
  160. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  161. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  162. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  163. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  164. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  165. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  166. package/templates/apps/api/src/lib/locales.ts +0 -201
  167. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  168. package/templates/apps/api/src/lib/prisma.ts +0 -21
  169. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  170. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  171. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  172. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  173. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  174. package/templates/apps/api/src/lib/site.ts +0 -254
  175. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  176. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  177. package/templates/apps/api/src/lib/stripe.ts +0 -318
  178. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  179. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  180. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  181. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  182. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  183. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  184. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  185. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  186. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  187. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  188. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  189. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  190. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  191. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  192. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  193. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  194. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  195. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  196. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  197. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  198. package/templates/apps/api/src/middleware/auth.ts +0 -864
  199. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  200. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  201. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -264
  202. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  203. package/templates/apps/api/src/openapi/index.ts +0 -18
  204. package/templates/apps/api/src/openapi/spec.ts +0 -1815
  205. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  206. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  207. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  208. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  209. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  210. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  211. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  212. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  213. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  214. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  215. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  216. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  217. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  218. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -325
  219. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  220. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1300
  221. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  222. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  223. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  224. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  225. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  226. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  227. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  228. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  229. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  230. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  231. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  232. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  233. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  234. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  235. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  236. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  237. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  238. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  239. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  240. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  241. package/templates/apps/api/src/routes/content/get-page.ts +0 -268
  242. package/templates/apps/api/src/routes/content/index.ts +0 -49
  243. package/templates/apps/api/src/routes/content/list-pages.ts +0 -95
  244. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  245. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  246. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  247. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  248. package/templates/apps/api/src/routes/content/search.ts +0 -129
  249. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  250. package/templates/apps/api/src/routes/content/slug.ts +0 -9
  251. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  252. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  253. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  254. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  255. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  256. package/templates/apps/api/src/routes/leads/index.ts +0 -573
  257. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  258. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  259. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  260. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  261. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -480
  262. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -465
  263. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  264. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  265. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  266. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  267. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  268. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  269. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  270. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  271. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  272. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  273. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  274. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  275. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  276. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  277. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  278. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  279. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  280. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  281. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  282. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  285. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -448
  286. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  287. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  288. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  289. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  290. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  291. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  292. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  293. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  294. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -808
  295. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  296. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  297. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  298. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2026
  299. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -528
  300. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  301. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  302. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  303. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  304. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  305. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  306. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  307. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -867
  308. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  309. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  310. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  311. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  312. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  313. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  314. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  315. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  316. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  317. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1176
  318. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -444
  319. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -154
  320. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  321. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  322. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  323. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  324. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  325. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  326. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  327. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  328. package/templates/apps/api/src/services/abandon-email.ts +0 -313
  329. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  330. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  331. package/templates/apps/api/src/services/audit-log.ts +0 -961
  332. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  333. package/templates/apps/api/src/services/consent.ts +0 -834
  334. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  335. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -755
  336. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  337. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  338. package/templates/apps/api/src/services/entitlements.ts +0 -338
  339. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  340. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  341. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  342. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  343. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  344. package/templates/apps/api/src/services/listing-config.ts +0 -178
  345. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  346. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -295
  347. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  348. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -868
  349. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  350. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -396
  351. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -681
  352. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  353. package/templates/apps/api/src/services/offerings/catalog.ts +0 -308
  354. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -385
  355. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  356. package/templates/apps/api/src/services/offerings/connection.ts +0 -1003
  357. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  358. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  359. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  360. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  361. package/templates/apps/api/src/services/person.ts +0 -1188
  362. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  363. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  364. package/templates/apps/api/src/services/project-listings.ts +0 -2073
  365. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  366. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  367. package/templates/apps/api/src/services/revalidation.ts +0 -235
  368. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  369. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  370. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  371. package/templates/apps/api/src/services/verified-email.ts +0 -71
  372. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  373. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  374. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  375. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  376. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  377. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  378. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  379. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  380. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  381. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  382. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  383. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  384. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  385. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  386. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  387. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  388. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1151
  389. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  390. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  391. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  392. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -65
  393. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -187
  394. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  395. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  396. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -165
  397. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  398. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -228
  399. package/templates/apps/mcp/__tests__/locale.test.ts +0 -135
  400. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  401. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  402. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -474
  403. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -225
  404. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -256
  405. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  406. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  407. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -307
  408. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -223
  409. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  410. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  411. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  412. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  413. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  414. package/templates/apps/mcp/src/abuse/index.ts +0 -342
  415. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  416. package/templates/apps/mcp/src/boot.ts +0 -167
  417. package/templates/apps/mcp/src/config/modules.ts +0 -43
  418. package/templates/apps/mcp/src/http-transport.ts +0 -526
  419. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  420. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  421. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -142
  422. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -139
  423. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  424. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  425. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  426. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  427. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  428. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  429. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  430. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  431. package/templates/apps/mcp/src/request-context.ts +0 -52
  432. package/templates/apps/mcp/src/server.ts +0 -343
  433. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  434. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  435. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -64
  436. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  437. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  438. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  439. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  440. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  441. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  442. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  443. package/templates/apps/mcp/src/tools/index.ts +0 -137
  444. package/templates/apps/mcp/src/tools/list_pages.ts +0 -74
  445. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  446. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  447. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  448. package/templates/apps/mcp/src/tools/read_content.ts +0 -75
  449. package/templates/apps/mcp/src/tools/request_callback.ts +0 -59
  450. package/templates/apps/mcp/src/tools/search_content.ts +0 -67
  451. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -57
  452. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  453. package/templates/database/CHANGELOG.md +0 -961
  454. package/templates/database/LICENSE +0 -202
  455. package/templates/database/NOTICE +0 -2
  456. package/templates/database/README.md +0 -243
  457. package/templates/database/__tests__/agents.db.test.ts +0 -320
  458. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  459. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  460. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  461. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  462. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  463. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  464. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  465. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  466. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  467. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  468. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  469. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  470. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  471. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  472. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  473. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  474. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  475. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  476. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  477. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  478. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  479. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  480. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  481. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  482. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  483. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  484. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  485. package/templates/database/__tests__/person.db.test.ts +0 -586
  486. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  487. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  488. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  489. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  490. package/templates/database/__tests__/seed.test.ts +0 -240
  491. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  492. package/templates/database/__tests__/tenancy.test.ts +0 -141
  493. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  494. package/templates/database/_gitignore +0 -8
  495. package/templates/database/auth/.gitkeep +0 -0
  496. package/templates/database/auth/schema.prisma +0 -641
  497. package/templates/database/config/.gitkeep +0 -0
  498. package/templates/database/config/schema.prisma +0 -441
  499. package/templates/database/content/.gitkeep +0 -0
  500. package/templates/database/content/schema.prisma +0 -339
  501. package/templates/database/eslint.config.mjs +0 -69
  502. package/templates/database/flows/listing-diagnostic.ts +0 -182
  503. package/templates/database/flows/presets.ts +0 -315
  504. package/templates/database/inbox/.gitkeep +0 -0
  505. package/templates/database/inbox/README.md +0 -3
  506. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  507. package/templates/database/inbox/schema.prisma +0 -1447
  508. package/templates/database/jest.config.cjs +0 -36
  509. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  510. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  511. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  512. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  513. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  514. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  515. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  516. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  517. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  518. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  519. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  520. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  521. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  522. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  523. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  524. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  525. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  526. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  527. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  528. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  529. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  530. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  531. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  532. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  533. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  534. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  535. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  536. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  537. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  538. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  539. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  540. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  541. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  542. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  543. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  544. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  545. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  546. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  547. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  548. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  549. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  550. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  551. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  552. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  553. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  554. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  555. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  556. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  557. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  558. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  559. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  560. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  561. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  562. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  563. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  564. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  565. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  566. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  567. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  568. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  569. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  570. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  571. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  572. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  573. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  574. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  575. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  576. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  577. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  578. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  579. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  580. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  581. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  582. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  583. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  584. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  585. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  586. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  587. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  588. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  589. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  590. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  591. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  592. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  593. package/templates/database/migrations/migration_lock.toml +0 -3
  594. package/templates/database/ops/.gitkeep +0 -0
  595. package/templates/database/ops/schema.prisma +0 -43
  596. package/templates/database/package.json +0 -89
  597. package/templates/database/schema.prisma +0 -71
  598. package/templates/database/scripts/db-generate-locked.sh +0 -80
  599. package/templates/database/scripts/person-backfill.sql +0 -132
  600. package/templates/database/tenancy.ts +0 -183
  601. package/templates/database/tsconfig.build.json +0 -16
  602. package/templates/database/tsconfig.flows.json +0 -15
  603. package/templates/database/tsconfig.json +0 -16
  604. package/templates/database/tsconfig.tenancy.json +0 -14
@@ -1,1447 +0,0 @@
1
- // inbox namespace — Lead, Subscriber, Callback
2
- //
3
- // Backs the Phase B anonymous-write MCP tools (submit_lead, subscribe_newsletter,
4
- // request_callback) and their corresponding POST routes (B6).
5
- //
6
- // Per ADR 0009 D2: no @relation directives cross namespace boundaries.
7
- // siteId is a plain String identifier — no FK to auth/users.
8
- //
9
- // Generator + datasource blocks live in database/schema.prisma (the root schema
10
- // file). This file holds only model definitions for the inbox namespace.
11
- //
12
- // ── The fence fan-out (#6374) — why `workspaceId` sits where it does ────────
13
- // Eleven models in this file carry the tenancy fence: a NOT NULL
14
- // `workspaceId` mapped to `workspace_id`, an `@@index([workspaceId])`, and a
15
- // `<table>_workspace_site_agree` CHECK added by
16
- // database/migrations/20260922120000_tenancy_fence. No Prisma relation is
17
- // declared for it (no Prisma relation directive) and no raw-SQL foreign key
18
- // is added (ADR-0009 invariant #1) — the CHECK is the fence, and it is
19
- // strictly tighter than an FK, because `site_id` IS the resolved workspace id.
20
-
21
- /// Lead — anonymous lead submission (Phase B submit_lead tool / B6 route)
22
- model Lead {
23
- id String @id @default(dbgenerated("('lead_'::text || (uuidv7())::text)")) @db.Text
24
- siteId String @map("site_id")
25
- // #6203 — the isolation key `tenantDb(tenant).leads` filters on. Plain
26
- // scalar, no @relation (ADR 0009 D1, the same reasoning `promotedUserId`
27
- // below already applies). Backfilled by
28
- // database/migrations/20260919140000_inbox_workspace_key.
29
- //
30
- // #6254 AC-3/AC-4/AC-7 — NOT NULL as of
31
- // database/migrations/20260922120000_tenancy_fence, which also normalizes
32
- // `siteId` to the resolved workspace id and adds
33
- // `leads_workspace_site_agree CHECK ("workspace_id" = "site_id")`: since
34
- // `siteId` IS the workspace id (never a WorkspaceSiteIdentity id — see
35
- // that model's docblock), Postgres itself now rejects a row whose two
36
- // columns disagree, in place of the issue's literal (and unbuildable,
37
- // ADR-0009-crossing) composite FK — docs/database/namespaces.md. Every
38
- // write site derives this value server-side from the resolved site
39
- // alias; never from a request body (AC-6).
40
- workspaceId String @map("workspace_id")
41
- source String
42
- email String
43
- payload Json
44
- createdAt DateTime @default(now()) @map("created_at")
45
-
46
- // #6204 — the triage stamp. NOT `promotedAt`'s sibling: `promotedAt` below
47
- // is the lead→user PROMOTION stamp, written by the email-verification
48
- // reconciler and true of a lead regardless of whether anyone has answered
49
- // it. `handledAt` is written by an operator (or an agent acting for one)
50
- // clearing a MESSAGE out of the Needs-you queue (spec D8). Nullable, no
51
- // default — every existing row reads NULL, i.e. unhandled (spec D5).
52
- // `handledBy` is the actor, mirroring `ProjectListing.decidedBy` below
53
- // (spec D4/S-7) — plain scalar, no @relation (ADR 0009 D2), the same
54
- // posture `promotedUserId` already takes.
55
- handledAt DateTime? @map("handled_at")
56
- handledBy String? @map("handled_by")
57
-
58
- // Lead → user promotion (#3586). Plain scalars — no cross-namespace
59
- // @relation into auth.User (ADR 0009 invariant #1). Set by
60
- // apps/api/src/services/lead-promotion.ts once the reconciling user's
61
- // email is verified and the lead's siteId resolves to one of that user's
62
- // own workspaces (tenant-safety, spec D3).
63
- promotedUserId String? @map("promoted_user_id")
64
- promotedAt DateTime? @map("promoted_at")
65
-
66
- // #7869 (roadmap D5) — the caller's `Idempotency-Key` header on
67
- // `POST /v1/leads/submit`, stored verbatim so a retried write finds the
68
- // first lead. Written only by `apps/api/src/services/lead-routing.ts`'s
69
- // submit branch and read only by its two scoped lookups. Untrusted,
70
- // client-supplied text — never echo it. Nullable, no default: every
71
- // pre-#7869 row and every keyless lead reads NULL, and Postgres treats
72
- // NULLs as distinct under the unique below. Same shape as
73
- // `FlowCheckout.idempotencyKey` (#4912).
74
- idempotencyKey String? @map("idempotency_key")
75
-
76
- // #852 D1 — the Person this lead belongs to: one client of this workspace,
77
- // stamped at creation by `apps/api/src/services/lead-routing.ts` through
78
- // `findOrCreatePerson`. Plain scalar, no @relation (the inbox house style,
79
- // `FlowCheckout.leadId`). NULL only when the email is not a storable
80
- // address or the Person step failed — a Person step never refuses a lead.
81
- // Backfilled by database/migrations/20261010121000_person_backfill.
82
- personId String? @map("person_id")
83
-
84
- // #8185 D1 — the hub's conclusion, at write time, that this lead is a
85
- // spoke's LIVE paid checkout: the submit carried a registered spoke's key,
86
- // was not relayed for a visitor, and passed the #7877 checkout plan rule's
87
- // own predicate (a `cs_live_` Idempotency-Key and the checkout shape).
88
- // Written only by `apps/api/src/services/lead-routing.ts`'s submit create
89
- // and never updated; read only by `apps/api/src/lib/member-store.ts`'s
90
- // `listMemberPurchases`, which serves a marked lead as a purchase. A lead
91
- // written before the column existed reads false: `viaSpoke` was never
92
- // stored, so its provenance cannot be proven (no backfill). No index: the
93
- // read filters by the already-indexed `personId`.
94
- spokeCheckout Boolean @default(false) @map("spoke_checkout")
95
-
96
- // #7869 — the idempotency guarantee, scoped per workspace so a key can
97
- // never reach another tenant's row.
98
- @@unique([workspaceId, idempotencyKey])
99
- @@index([siteId])
100
- @@index([createdAt])
101
- @@index([email])
102
- @@index([workspaceId])
103
- // #6204 — the message-inbox read: workspaceId = $1 AND handledAt IS NULL
104
- // AND source = ANY($2), ordered by createdAt desc. Two columns, not three
105
- // (spec D10): `source` is an IN over four values Postgres applies cheaply
106
- // after this narrowing, so it is not worth widening the index on every
107
- // write.
108
- @@index([workspaceId, handledAt])
109
- // #6246 — the per-magnet read: workspaceId = $1 AND source = $2, ordered
110
- // by createdAt desc (spec § 1.5/D1). `source` here is an EXACT-equality
111
- // `'flow:' + <magnet slug>` value, not the `handledAt` index's allowlist
112
- // `IN` — a distinct query shape that composite does not serve.
113
- @@index([workspaceId, source])
114
- @@index([personId])
115
- @@map("leads")
116
- }
117
-
118
- /// Person — one client of one workspace (#852). Created only by
119
- /// apps/api/src/services/person.ts's findOrCreatePerson. Plain scalars, no
120
- /// @relation (ADR 0009): workspaceId and userId name rows in the auth
121
- /// namespace.
122
- ///
123
- /// `email` is the lowercased, trimmed address (the `people_email_normalized`
124
- /// CHECK holds it to printable ASCII). `userId` is written only on a verified
125
- /// email match (`stampPersonUser`) and never overwritten. `billingCustomerId`
126
- /// is NOT unique — the spokes share one Stripe account — and the first
127
- /// writer wins. `planOffering` is written by a later phase (Epic 2's
128
- /// checkout-lead rule); this slice only carries the column.
129
- model Person {
130
- id String @id @default(dbgenerated("('person_'::text || (uuidv7())::text)")) @db.Text
131
- workspaceId String @map("workspace_id")
132
- email String @db.VarChar(254)
133
- name String? @db.VarChar(200)
134
- userId String? @map("user_id")
135
- billingProvider String? @map("billing_provider") @db.VarChar(32)
136
- billingCustomerId String? @map("billing_customer_id") @db.VarChar(255)
137
- planOffering String? @map("plan_offering") @db.VarChar(100)
138
- // #8435 D1 — contact details, to the standard (option A). Every column is
139
- // nullable: a capture fills only a blank one (D4), an edit overwrites (D9,
140
- // D11). The address is the magnet's six parts, stored whole or not at all
141
- // (people_address_complete).
142
- givenName String? @map("given_name") @db.VarChar(100)
143
- familyName String? @map("family_name") @db.VarChar(100)
144
- telephone String? @db.VarChar(16)
145
- addressLine1 String? @map("address_line1") @db.VarChar(100)
146
- addressLine2 String? @map("address_line2") @db.VarChar(60)
147
- addressCity String? @map("address_city") @db.VarChar(60)
148
- addressRegion String? @map("address_region") @db.VarChar(40)
149
- addressPostalCode String? @map("address_postal_code") @db.VarChar(20)
150
- addressCountry String? @map("address_country") @db.Char(2)
151
- companyName String? @map("company_name") @db.VarChar(200)
152
- jobTitle String? @map("job_title") @db.VarChar(100)
153
- // Who last saved the contact by hand — "operator" (the Person page) or
154
- // "client" (their account) — and when. NULL until a human saves it; while
155
- // NULL and `userId` is NULL, a capture may fill a blank field (D4).
156
- contactConfirmedAt DateTime? @map("contact_confirmed_at")
157
- contactConfirmedBy String? @map("contact_confirmed_by") @db.VarChar(16)
158
- // #8514 D1 — the team's private note on this client: one per Person, never
159
- // served to the client. Who last saved it (a user id of the auth namespace,
160
- // no foreign key — ADR 0009) and when; both NULL until the first save.
161
- // When an agent saved it, which agent (its person is notesUpdatedBy).
162
- notes String? @db.VarChar(5000)
163
- notesUpdatedAt DateTime? @map("notes_updated_at")
164
- notesUpdatedBy String? @map("notes_updated_by")
165
- notesUpdatedByAgent String? @map("notes_updated_by_agent")
166
- createdAt DateTime @default(now()) @map("created_at")
167
- updatedAt DateTime @updatedAt @map("updated_at")
168
-
169
- pieces Piece[]
170
- interactions Interaction[]
171
- callbacks Callback[]
172
- subscribers Subscriber[]
173
-
174
- @@unique([workspaceId, email])
175
- @@unique([workspaceId, userId])
176
- // #7877 D1 — redundant with the PK, load-bearing as the target of the
177
- // composite (person_id, workspace_id) FKs from pieces and interactions:
178
- // "a Person of the same workspace" is a database fact.
179
- @@unique([id, workspaceId])
180
- @@index([workspaceId, billingCustomerId])
181
- @@index([workspaceId, createdAt])
182
- @@map("people")
183
- }
184
-
185
- /// Piece — one deliverable or proposal for one client (#7877, #8516). Written only through
186
- /// tenantDb(tenant).memberArea; read by a client only through
187
- /// lib/member-store.ts. `stage` is one of the workspace's declared stages
188
- /// (WorkspacePieceStages), checked at write time, never an enum.
189
- ///
190
- /// #8168 D5 — `approvedAt` is set in the same transaction as an `approved`
191
- /// review of the latest PieceVersion and freezes review: once set, no further
192
- /// review lands. Approval is a piece-level fact; the version's own `review`
193
- /// records which version was approved.
194
- model Piece {
195
- id String @id @default(dbgenerated("('piece_'::text || (uuidv7())::text)")) @db.Text
196
- workspaceId String @map("workspace_id")
197
- personId String @map("person_id")
198
- title String @db.VarChar(200)
199
- stage String @db.VarChar(40)
200
- needsClient Boolean @default(false) @map("needs_client")
201
- updatedBy String @map("updated_by")
202
- approvedAt DateTime? @map("approved_at")
203
- // #8516 D1 — what the piece is: a `deliverable` (every row before #8516) or a
204
- // `proposal` the client approves or sends back. Never changes after create.
205
- kind String @default("deliverable") @db.VarChar(16)
206
- // A proposal's price, in minor units of a two-decimal currency; both or
207
- // neither, and only on a proposal.
208
- priceAmount Int? @map("price_amount")
209
- priceCurrency String? @map("price_currency") @db.Char(3)
210
- // The client's current Stripe Checkout Session (#8516 D11) — its id, URL and
211
- // expiry. Never served to the operator. Replaced once five minutes or less
212
- // remain; the replaced session is expired at Stripe.
213
- checkoutSessionId String? @unique @map("checkout_session_id")
214
- checkoutUrl String? @map("checkout_url") @db.VarChar(2048)
215
- checkoutExpiresAt DateTime? @map("checkout_expires_at")
216
- // When Stripe reported the proposal paid, and the session that paid it (D13).
217
- paidAt DateTime? @map("paid_at")
218
- paidSessionId String? @map("paid_session_id")
219
- createdAt DateTime @default(now()) @map("created_at")
220
- updatedAt DateTime @updatedAt @map("updated_at")
221
-
222
- person Person @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "pieces_person_fk")
223
- versions PieceVersion[]
224
-
225
- @@index([workspaceId, personId, createdAt])
226
- // #8168 D1 — redundant with the PK, load-bearing as the target of
227
- // piece_versions_piece_fk: a version names a piece of THIS Person in THIS
228
- // workspace, as one database fact.
229
- @@unique([id, personId, workspaceId])
230
- @@map("pieces")
231
- }
232
-
233
- /// PieceVersion — one delivery of a piece to its client (#8168 D1, D2): a
234
- /// file (`assetId`, a private library asset of the same workspace) or a link
235
- /// (`link`, https only), never both, numbered from 1 per piece. The client's
236
- /// review of it (`review`, `reviewComment`, `reviewedAt`) is recorded on the
237
- /// version itself. There is NO round count, limit or remaining-count column,
238
- /// by design (#8140 disposition 3).
239
- ///
240
- /// Its one relation is the three-column FK onto `pieces(id, person_id,
241
- /// workspace_id)`, so the piece, its Person and the workspace are all one
242
- /// database fact. `assetId` and `deliveredBy` name rows of other namespaces:
243
- /// plain columns with format CHECKs, no foreign key (ADR 0009).
244
- model PieceVersion {
245
- id String @id @default(dbgenerated("('pver_'::text || (uuidv7())::text)")) @db.Text
246
- workspaceId String @map("workspace_id")
247
- personId String @map("person_id")
248
- pieceId String @map("piece_id")
249
- version Int
250
- assetId String? @map("asset_id")
251
- link String? @db.VarChar(2048)
252
- note String? @db.Text
253
- deliveredBy String @map("delivered_by")
254
- deliveredAt DateTime @default(now()) @map("delivered_at")
255
- review String? @db.VarChar(24)
256
- reviewComment String? @map("review_comment") @db.Text
257
- reviewedAt DateTime? @map("reviewed_at")
258
-
259
- piece Piece @relation(fields: [pieceId, personId, workspaceId], references: [id, personId, workspaceId], onDelete: Restrict, map: "piece_versions_piece_fk")
260
-
261
- @@unique([pieceId, version])
262
- // A client's artifacts: workspace_id = $1 AND person_id = $2 ORDER BY delivered_at DESC.
263
- @@index([workspaceId, personId, deliveredAt])
264
- // The public asset read's "is this asset a version file" check (AC-MAA-21).
265
- @@index([assetId])
266
- @@map("piece_versions")
267
- }
268
-
269
- /// Interaction — one entry on a client's record (#7877): the original CRM
270
- /// spec's Interaction cut to one channel. `readAt` is when a member read
271
- /// FIRST served the row to the client's spoke — a delivery stamp, not proof a
272
- /// human read it.
273
- ///
274
- /// #8168 D3/D4 — `direction` is `outbound` (an operator's message, #7877) or
275
- /// `inbound` (a client's own write); `kind` is `message`, `quote` or
276
- /// `review`. An outbound row is always a `message` whose `payload` is null
277
- /// or, since #8515 D6, exactly `{ "action": "onboarding" }` — the one outbound
278
- /// action, rendered by the client's site as a link to `/account/onboarding`
279
- /// (`interactions_outbound_shape`); a `quote` or `review` row carries the
280
- /// stored payload `packages/validation`'s member.ts types. Every existing
281
- /// reader filters to `direction: "outbound"`, so an inbound row is invisible
282
- /// to them until a reader opts in.
283
- model Interaction {
284
- id String @id @default(dbgenerated("('intx_'::text || (uuidv7())::text)")) @db.Text
285
- workspaceId String @map("workspace_id")
286
- personId String @map("person_id")
287
- channel String @db.VarChar(16)
288
- direction String @db.VarChar(16)
289
- kind String @default("message") @db.VarChar(16)
290
- subject String @db.VarChar(200)
291
- body String @db.Text
292
- payload Json?
293
- assetId String? @map("asset_id")
294
- createdBy String @map("created_by")
295
- createdAt DateTime @default(now()) @map("created_at")
296
- readAt DateTime? @map("read_at")
297
-
298
- person Person @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "interactions_person_fk")
299
-
300
- @@index([workspaceId, personId, createdAt])
301
- @@index([assetId])
302
- @@map("interactions")
303
- }
304
-
305
- /// Subscriber — newsletter subscription (Phase B subscribe_newsletter tool / B6 route)
306
- model Subscriber {
307
- id String @id @default(dbgenerated("('subscr_'::text || (uuidv7())::text)")) @db.Text
308
- siteId String @map("site_id")
309
- // #6374 — the tenancy fence, extended to this model (spec
310
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
311
- // Prisma relation directive (ADR-0009 invariant #1), and
312
- // subscribers_workspace_site_agree CHECK (workspace_id = site_id) — since
313
- // siteId IS the resolved workspace id, Postgres itself rejects a row
314
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
315
- // value, never one from a request body (AC-6).
316
- workspaceId String @map("workspace_id")
317
- email String
318
- source String
319
- createdAt DateTime @default(now()) @map("created_at")
320
- // #8435 D1/D5 — the Person this subscription belongs to; composite FK onto
321
- // people(id, workspace_id), the #7877 shape. NULL is not checked.
322
- personId String? @map("person_id")
323
- person Person? @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "subscribers_person_fk")
324
-
325
- @@unique([siteId, email])
326
- @@index([siteId])
327
- @@index([personId])
328
- @@index([workspaceId])
329
- @@map("subscribers")
330
- }
331
-
332
- /// Callback — callback request (Phase B request_callback tool / B6 route),
333
- /// plus the request-and-confirm lifecycle (#5970, D-67 / SUB-100).
334
- ///
335
- /// The eleven trailing members are the confirm/pay lifecycle. They are
336
- /// deliberately ON THIS MODEL rather than on a second `ServiceRequest` row
337
- /// (spec #5970 D1), and the cost of that choice is stated rather than hidden:
338
- /// every ORDINARY callback row — the overwhelming majority — now carries
339
- /// eleven NULL columns it will never use. The alternative duplicates
340
- /// siteId/name/phone/preferredTime/payload or carries a cross-row join for a
341
- /// one-to-one relationship, and it puts a SECOND write inside the anonymous
342
- /// lead-capture path, which is a never-gate surface. Eleven nullable columns
343
- /// is the cheaper side.
344
- model Callback {
345
- id String @id @default(dbgenerated("('callbk_'::text || (uuidv7())::text)")) @db.Text
346
- siteId String @map("site_id")
347
- // #6374 — the tenancy fence, extended to this model (spec
348
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
349
- // Prisma relation directive (ADR-0009 invariant #1), and
350
- // callbacks_workspace_site_agree CHECK (workspace_id = site_id) — since
351
- // siteId IS the resolved workspace id, Postgres itself rejects a row
352
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
353
- // value, never one from a request body (AC-6).
354
- workspaceId String @map("workspace_id")
355
- name String
356
- phone String
357
- preferredTime String? @map("preferred_time")
358
- payload Json
359
- createdAt DateTime @default(now()) @map("created_at")
360
-
361
- // ── request-and-confirm lifecycle (#5970) ──────────────────────────────
362
- /// sha-256 hex digest of the tenant's confirm token. The token itself is
363
- /// emailed once and never stored; NULLED when the credential is spent,
364
- /// which is the single-use mechanism. `@unique` mirrors `resumeTokenHash` —
365
- /// a digest collision is a bug, not a state.
366
- confirmTokenHash String? @unique @map("confirm_token_hash")
367
- /// When the credential stops resolving (mint + 7 days).
368
- confirmTokenExpiresAt DateTime? @map("confirm_token_expires_at")
369
- /// Stamped by the tenant's confirm POST. Also the "already confirmed" predicate.
370
- confirmedAt DateTime? @map("confirmed_at")
371
- /// What the tenant agreed to, as they typed it — a string like
372
- /// `preferredTime` above, not a parsed instant.
373
- confirmedTime String? @map("confirmed_time")
374
- /// Where the hosted payment URL is sent. Typed on the confirm form,
375
- /// prefilled from `payload.email` when the capture form carried one.
376
- buyerEmail String? @map("buyer_email")
377
- /// MINOR units, never major — Stripe's own `unit_amount` convention.
378
- amountMinor Int? @map("amount_minor")
379
- /// Lowercase ISO-4217.
380
- currency String?
381
- /// The Payment Link the confirm step minted — THE artifact id the webhook
382
- /// stamp matches on (`session.payment_link`).
383
- stripePaymentLinkId String? @map("stripe_payment_link_id")
384
- /// The session written from the same event, for support lookups. NOT a
385
- /// match key, which is why it carries no index.
386
- stripeCheckoutSessionId String? @map("stripe_checkout_session_id")
387
- /// NULL-forever at #5970's boundary — present so the hosted-invoice rung
388
- /// lands additively, with no second migration.
389
- stripeInvoiceId String? @map("stripe_invoice_id")
390
- /// Stamped exactly once by the Stripe webhook; the `paidAt: null` guard on
391
- /// that update IS the durable idempotency key.
392
- paidAt DateTime? @map("paid_at")
393
-
394
- // #8435 D1/D5 — the Person this request belongs to (its payload.email, or
395
- // the buyer email at confirm). Composite FK onto people(id, workspace_id):
396
- // "a Person of the same workspace" is a database fact. NULL is not checked.
397
- personId String? @map("person_id")
398
- person Person? @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "callbacks_person_fk")
399
-
400
- @@index([siteId])
401
- @@index([personId])
402
- @@index([createdAt])
403
- @@index([stripePaymentLinkId])
404
- @@index([stripeInvoiceId])
405
- @@index([workspaceId])
406
- @@map("callbacks")
407
- }
408
-
409
- /// FlowDefinition — server-owned flow config, one live definition per
410
- /// (siteId, slug) (#3956 D2). No versioning at this layer — a session
411
- /// snapshots the config at start (see FlowSession.configSnapshot), so a
412
- /// mid-flight definition edit never corrupts a live session.
413
- model FlowDefinition {
414
- id String @id @default(dbgenerated("('flow_'::text || (uuidv7())::text)")) @db.Text
415
- siteId String @map("site_id")
416
- /// #6155 AD-2 — the workspace isolation key. Backfilled from `siteId`: the
417
- /// literal "default" resolves to the seeded sentinel workspace
418
- /// (wksp_00000000-0000-7000-8000-000000000001); any other value resolves
419
- /// only by exact `workspaces.id` match. Plain string, no `@relation`: ADR
420
- /// 0009 forbids a Prisma relation across the inbox -> auth (and, per
421
- /// #6254, inbox -> config) namespace boundary, the same reason
422
- /// `Asset.workspaceId` (database/content/schema.prisma) carries none.
423
- ///
424
- /// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
425
- /// database/migrations/20260922120000_tenancy_fence, which also
426
- /// normalizes `siteId` to the resolved workspace id and adds
427
- /// `flow_definitions_workspace_site_agree CHECK ("workspace_id" =
428
- /// "site_id")` — see `Lead.workspaceId` above for the full rationale
429
- /// (identical here). Every write site derives this value server-side;
430
- /// never from a request body (AC-6).
431
- workspaceId String @map("workspace_id")
432
- slug String
433
- config Json
434
- /// #6156 D1/D3 — the magnet's classification, derived structurally (never
435
- /// from slug) by `deriveFlowKind` on CREATE only: special-report |
436
- /// diagnostic | listicle | waitlist | contact | newsletter. Keeps its
437
- /// column default ('contact') — unlike `status`, a mis-classified magnet
438
- /// is a mislabel, not a publication or security control.
439
- kind String @default("contact")
440
- /// #6156 D1 — the operator-facing name; the slug is not a name. NOT NULL
441
- /// with NO column default (D1): a nameless magnet must never enter the
442
- /// table silently, so every future writer is required by `tsc` to state
443
- /// one, exactly as `status` below is required to state a status.
444
- title String
445
- /// #6156 D1/D4 — an operator-declared publish address, site-relative only
446
- /// (`flowLandingPageSchema`, packages/validation/src/schemas/flow.ts).
447
- /// NULL means "no operator-declared landing page; the canonical
448
- /// `/{locale}/flows/{slug}` route serves it" — never a derived guess, since
449
- /// a magnet has THREE public addresses (the indexed landing route, the
450
- /// walk route one segment deeper, and the results route), and only the
451
- /// operator can say which one they are publishing.
452
- landingPage String? @db.Text @map("landing_page")
453
- /// #6156 D1/D2/D10 — draft | live. NOT NULL with NO column default (D1,
454
- /// SEC-08): silence must never publish, so every future writer is required
455
- /// by `tsc` to name a status. Existing rows are backfilled 'live' by the
456
- /// migration (every row that exists today is already serving). The
457
- /// `flow_definitions_status_domain` CHECK (D2) closes the deny-list gate in
458
- /// `resolveSession` (D10): an unknown value is unstorable, so "not
459
- /// 'draft'" and "'live'" are the same set at the database level.
460
- status String
461
- // #5036 D6d — the listing this definition was projected from, when it was.
462
- // Null for every flow that is not a listing diagnostic. Before this column
463
- // the only link was that the generated flow's slug HAPPENED to equal the
464
- // listing's slug, which nothing enforced; a slug collision would have
465
- // attributed a submission to the wrong founder. ADR 0009 forbids a relation
466
- // across the namespace boundary, so this is a plain string — but a stated
467
- // one rather than a coincidence.
468
- listingId String? @map("listing_id")
469
- /// #8168 D7 — whether a signed-in client may submit this magnet from the
470
- /// member area. A column, not a config block, because a spoke seed refuses
471
- /// non-seedable config (#7876); `upsertFlowDefinition` never names it, so a
472
- /// seed never changes it. Independent of `status`: the seeded
473
- /// `request-quote` magnet is `draft` (never served anonymously) with
474
- /// `customersEnabled = true`.
475
- customersEnabled Boolean @default(false) @map("customers_enabled")
476
- /// #8757 D1/D2 — what happens after someone submits the magnet: `lead`
477
- /// (the thank-you only), `invite` (an account link with the email
478
- /// prefilled) or `checkout` (continue to `checkoutOffering`). A column, on
479
- /// the `customersEnabled` footing: no seed or config names it. Closed by
480
- /// the `flow_definitions_completion_domain` CHECK; the standard defaults are
481
- /// the migration's backfill by slug (`STANDARD_MAGNET_COMPLETIONS`).
482
- completion String @default("lead") @db.VarChar(16)
483
- /// #8757 D1/D2 — the offering key a `checkout` magnet continues to; NULL
484
- /// exactly when `completion` is not `checkout`
485
- /// (`flow_definitions_checkout_offering_pair`), in the spoke checkout slug
486
- /// format (`flow_definitions_checkout_offering_format`).
487
- checkoutOffering String? @map("checkout_offering") @db.VarChar(40)
488
- createdAt DateTime @default(now()) @map("created_at")
489
- updatedAt DateTime @updatedAt @map("updated_at")
490
-
491
- @@unique([siteId, slug])
492
- @@index([workspaceId])
493
- @@map("flow_definitions")
494
- }
495
-
496
- /// FlowSession — a flow walk in progress or completed; the server-side
497
- /// partial-lead record (#3956 D2/D9). `configSnapshot` pins the definition at
498
- /// session start; `state` accumulates each landed step's values immediately
499
- /// on submission (the per-step persistence this feature exists for).
500
- /// `email` is denormalized from the landed answers the moment a
501
- /// `mapsTo:'email'` field lands, so the #3962 abandon-email query
502
- /// (`email != null AND completedAt == null`) is one indexed read.
503
- model FlowSession {
504
- id String @id @default(dbgenerated("('flsess_'::text || (uuidv7())::text)")) @db.Text
505
- siteId String @map("site_id")
506
- /// #6155 AD-2 — the workspace isolation key, same backfill contract as
507
- /// `FlowDefinition.workspaceId` above; #6254 makes it NOT NULL plus the
508
- /// same `flow_sessions_workspace_site_agree` equality CHECK — see
509
- /// `Lead.workspaceId` (database/inbox/schema.prisma, top of file) for the
510
- /// full rationale. Declared here, immediately after `siteId` and before
511
- /// `state`'s empty-object-literal default below: the drift guard's model
512
- /// regex (`tests/architecture/tenant-scope-pattern.test.ts`) truncates a
513
- /// model's captured body at its first closing brace — including one
514
- /// inside a comment or a string literal, not only in code — so a field
515
- /// placed after that point (or a stray brace pair written ABOVE it, as
516
- /// this very note must avoid) would be invisible to it.
517
- workspaceId String @map("workspace_id")
518
- // Informational only — never joined on (ADR 0009 D1: logical, not FK,
519
- // isolation). Session resumption + completion key on flowSlug, not this
520
- // column; deliberate, per code-review finding on #3956's acceptance PR.
521
- flowId String @map("flow_id")
522
- flowSlug String @map("flow_slug")
523
- configSnapshot Json @map("config_snapshot")
524
- state Json @default("{}")
525
- email String?
526
- // The session's locale (#3962 D3) — set once at create from the first
527
- // landed step's `input.locale ?? configSnapshot.defaultLocale`; not
528
- // updated on later steps. Null on pre-migration rows (no backfill; the
529
- // abandon-email sweep falls back to the live config's defaultLocale).
530
- locale String?
531
- // Server-derived segment (#3959 D3) — set from the segmentation step's
532
- // band map at landing time, persisted in the same write as `state`. No
533
- // index: #3960/#3961/#3962 read it off rows they already fetch by
534
- // existing indexes (subtraction-first, spec Clarifications).
535
- segment String?
536
- // D2 (#4663) — every derived flag, re-derived and persisted at each
537
- // landing (`{ [flagName]: true }`). `{}` on a snapshot declaring no
538
- // `flags` (the regression floor — byte-identical to a pre-#4663 row).
539
- flags Json @default("{}")
540
- // D6 (#4663) — the mid-flow aeo_score step's persisted result (`ready` /
541
- // `unavailable`). Null until the step lands, or after a URL change resets
542
- // it (D3). Never `computing` — that status never reaches the wire from
543
- // this repo (packages/validation/src/schemas/aeo-score.ts).
544
- aeoScore Json? @map("aeo_score")
545
- leadId String? @map("lead_id")
546
- // #5036 D6d — copied from the FlowDefinition at session create, so a
547
- // submission is attributable to ONE listing without going through the slug.
548
- // Null for every session on a flow that is not a listing diagnostic.
549
- listingId String? @map("listing_id")
550
- completedAt DateTime? @map("completed_at")
551
- // Abandon-email audit stamp (#3962 D5) — set at most once by the sweep,
552
- // per outcome ('sent' | 'dark' | 'failed'). The `abandonEmailStatus ==
553
- // null` selection term is what makes the sweep idempotent across any
554
- // number of overlapping runs; 'failed' is terminal (no auto-retry).
555
- abandonEmailStatus String? @map("abandon_email_status")
556
- abandonEmailAt DateTime? @map("abandon_email_at")
557
- createdAt DateTime @default(now()) @map("created_at")
558
- updatedAt DateTime @updatedAt @map("updated_at")
559
-
560
- @@index([siteId])
561
- @@index([siteId, completedAt])
562
- @@index([workspaceId])
563
- @@index([email])
564
- // Backs the abandon-email sweep's per-definition selection query
565
- // (apps/api/src/services/abandon-email.ts — flowId equality + completedAt
566
- // null + abandonEmailStatus null + updatedAt window), run once per
567
- // FlowDefinition on every sweep — code-review finding on #3962's
568
- // acceptance PR (without this, that filter set is a full table scan).
569
- @@index([flowId, completedAt, abandonEmailStatus])
570
- // #5036 D6d — backs the listing-to-submissions read the explicit reference
571
- // exists to make possible.
572
- @@index([listingId])
573
- @@map("flow_sessions")
574
- }
575
-
576
- /// DeliverableAsset — LEGACY (#6310 D3/D11). An operator-uploaded asset the
577
- /// funnel's `deliverables` config block references by `key` (#3961 D1).
578
- /// Bytes live IN THE DATABASE (Postgres bytea) behind one internal
579
- /// resolver — zero new infrastructure; reversibility is structural (a later
580
- /// blob-storage swap changes only the resolver + the upload CLI, never the
581
- /// config contract, grant model, or email link shape). Uploaded via the
582
- /// `deliverable:upload` CLI (apps/api/src/bin/deliverable-upload.ts) only —
583
- /// no HTTP upload route.
584
- ///
585
- /// Since #6310: a magnet deliverable resolves against the workspace-scoped
586
- /// `Asset` model (database/content/schema.prisma) FIRST; this store is the
587
- /// FALLBACK, tried only on an `Asset` miss (D2). No existing row is
588
- /// migrated — D3 grounds why in the two server-internal write/read paths —
589
- /// so every row here keeps delivering, unmigrated, until an operator
590
- /// confirms none remain (§2.3's exit condition).
591
- model DeliverableAsset {
592
- id String @id @default(dbgenerated("('dasset_'::text || (uuidv7())::text)")) @db.Text
593
- siteId String @map("site_id")
594
- // #6374 — the tenancy fence, extended to this model (spec
595
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
596
- // Prisma relation directive (ADR-0009 invariant #1), and
597
- // deliverable_assets_workspace_site_agree CHECK (workspace_id = site_id) — since
598
- // siteId IS the resolved workspace id, Postgres itself rejects a row
599
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
600
- // value, never one from a request body (AC-6).
601
- workspaceId String @map("workspace_id")
602
- // The assetKey a flow config's deliverables.assets entry references.
603
- key String
604
- // Download filename (Content-Disposition). Stored RAW at upload time
605
- // (the `deliverable:upload` CLI writes the operator's `--filename` or the
606
- // uploaded file's own basename verbatim) — sanitization to a safe charset
607
- // happens at SERVE time only, in the download route's `sanitizeFilename`
608
- // (apps/api/src/routes/deliverables/index.ts, #4002). Never trust this
609
- // column as pre-sanitized; the serve-time guard is load-bearing, not
610
- // redundant.
611
- filename String
612
- contentType String @map("content_type")
613
- bytes Bytes
614
- size Int
615
- createdAt DateTime @default(now()) @map("created_at")
616
- updatedAt DateTime @updatedAt @map("updated_at")
617
-
618
- @@unique([siteId, key])
619
- @@index([workspaceId])
620
- @@map("deliverable_assets")
621
- }
622
-
623
- /// DeliverableGrant — the capability token minted per fulfilled lead (#3961
624
- /// D5). The grant's id IS the token: the download route
625
- /// (GET /v1/deliverables/:grantId) looks up bytes by grant id alone, so
626
- /// nothing is publicly enumerable by key, filename, or any other guessable
627
- /// value — only the uuidv7-random grant id, delivered once via the lead's
628
- /// own email. `assetId`/`leadId` are stable-id refs (ADR 0009 invariant 1
629
- /// posture) — no cross-model `@relation`.
630
- ///
631
- /// `assetId`'s three id classes (#6310): `asset_` (a workspace `Asset`,
632
- /// scoped serve via `workspaceId` below), `dasset_` (legacy
633
- /// `DeliverableAsset`, scoped by `siteId` above), `playbook:` (the FEAT-074
634
- /// U4b virtual asset — no store row at all).
635
- model DeliverableGrant {
636
- id String @id @default(dbgenerated("('dgrant_'::text || (uuidv7())::text)")) @db.Text
637
- siteId String @map("site_id")
638
- assetId String @map("asset_id")
639
- leadId String @map("lead_id")
640
- sessionId String @map("session_id")
641
- /// The workspace that authorized this grant's asset resolution (#6310
642
- /// D15). Plain string, no
643
- /// @relation (ADR 0009 invariant 1) — the same shape
644
- /// FlowDefinition.workspaceId carries. The serve path reads it ONLY for
645
- /// the `asset_` class, and a NULL there is a 404, never an unscoped read.
646
- ///
647
- /// #6374 CHANGED WHO WRITES IT, AND THE SAFETY ARGUMENT MOVED WITH IT.
648
- /// Before #6374 this was written ONLY when `assetId` was an `asset_` id,
649
- /// and stayed NULL for a legacy `dasset_` grant and a `playbook:` virtual
650
- /// one. As of #6374 BOTH mint sites stamp it unconditionally
651
- /// (services/deliverable-fulfillment.ts, services/listing-csv-sweep.ts),
652
- /// so NO newly-minted grant is NULL and the fail-closed sentinel below
653
- /// now protects PRE-EXISTING rows only — the migration still touches none
654
- /// of them.
655
- ///
656
- /// That is safe because of a DIFFERENT invariant than the one that used to
657
- /// carry it, and the new one is more fragile, so it is written down here
658
- /// rather than left implicit. What makes stamping safe is NOT "the column
659
- /// is NULL on legacy grants" — it no longer is. It is that
660
- /// routes/deliverables/index.ts branches on the `assetId` PREFIX, and only
661
- /// the `asset_` arm ever reads this column: the `playbook:` arm scopes by
662
- /// siteId + leadId, and the legacy `dasset_` arm scopes by
663
- /// `asset.siteId === grant.siteId`. Neither reads `workspaceId`, so a
664
- /// stamped legacy grant gains no reach it did not already have.
665
- /// DO NOT make the legacy or playbook arm consult this column without
666
- /// re-deriving that argument — doing so WOULD be an authorization
667
- /// widening, which is exactly what the pre-#6374 NULL used to prevent.
668
- ///
669
- /// #6254 DELIBERATELY DOES NOT TOUCH THIS FIELD. It stays nullable, with
670
- /// no equality CHECK — unlike Lead/ProjectListing/FlowDefinition/
671
- /// FlowSession above, this column has no source column to derive from
672
- /// (its own migration says so: "no source column to derive from ... a
673
- /// wholly new fact, captured only at a FUTURE mint"), and `NULL` is the
674
- /// serve path's fail-closed sentinel (`deliverable-asset-store.ts`
675
- /// `readDeliverableAsset` opens `if (workspaceId === null) return null`).
676
- /// Stamping `workspace_id := site_id` would hand a legacy grant an
677
- /// authorization scope no mint ever granted, on a bearer-token download
678
- /// path — a security regression, not a tightening. That reasoning still
679
- /// holds for EXISTING rows, which is why the column stays nullable and why
680
- /// #6374's migration adds no column and rewrites no row. Carved out of the
681
- /// #6254 retrofit by the operator (spec §10.1 Q2 -> carve out); #6374
682
- /// fences it at mint grain, where the workspace is actually known, and
683
- /// adds the NULL-TOLERANT
684
- /// deliverable_grants_workspace_site_agree CHECK — so a STAMPED grant can
685
- /// never disagree with its site while a legacy grant stays storable.
686
- workspaceId String? @map("workspace_id")
687
- createdAt DateTime @default(now()) @map("created_at")
688
- // Post-mint send-outcome stamp (#3998 D1) — set at most once, by the
689
- // send-and-stamp tail shared between fulfillDeliverable and the
690
- // deliverable:resend CLI. 'sent' | 'send_failed' | 'sender_unconfigured' —
691
- // the exact post-mint subset of deliverable-fulfillment.ts's
692
- // FulfillmentOutcome union; carried here (not a Prisma enum — this repo
693
- // uses none), mirroring FlowSession.abandonEmailStatus/abandonEmailAt.
694
- // NULL means the process never reached a post-mint outcome — the crash
695
- // window between mint and send (#3961 D5's unhandled consequence) that
696
- // this column makes queryable. This is the SELECTION term for the resend
697
- // CLI's list mode only; a resend itself is an explicit operator act on one
698
- // named grant and is NOT guarded by this column (#3998 D3).
699
- sendStatus String? @map("send_status")
700
- sendAt DateTime? @map("send_at")
701
-
702
- // Backs the deliverable:resend CLI's list-mode query (siteId + sendStatus
703
- // IS NULL) — without this, that filter set is a full table scan (the same
704
- // lesson #3962's code-review finding established for FlowSession).
705
- @@index([siteId, sendStatus])
706
- @@map("deliverable_grants")
707
- }
708
-
709
- /// WaitlistExperiment — one ratified experiment config per (siteId, key), the
710
- /// runtime twin of a site-plan waitlistExperiments[] entry (#4661 D3/D7).
711
- /// `config` is the validated experimentConfig (D1) with the three registers
712
- /// EMBEDDED. Written only via upsertWaitlistExperiment (D5).
713
- model WaitlistExperiment {
714
- id String @id @default(dbgenerated("('wlexp_'::text || (uuidv7())::text)")) @db.Text
715
- siteId String @map("site_id")
716
- // #6374 — the tenancy fence, extended to this model (spec
717
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
718
- // Prisma relation directive (ADR-0009 invariant #1), and
719
- // waitlist_experiments_workspace_site_agree CHECK (workspace_id = site_id) — since
720
- // siteId IS the resolved workspace id, Postgres itself rejects a row
721
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
722
- // value, never one from a request body (AC-6).
723
- workspaceId String @map("workspace_id")
724
- key String
725
- mode String // lead-capture | qualifying | paid
726
- status String // draft | live | closed
727
- config Json
728
- createdAt DateTime @default(now()) @map("created_at")
729
- updatedAt DateTime @updatedAt @map("updated_at")
730
-
731
- @@unique([siteId, key])
732
- @@index([workspaceId])
733
- @@map("waitlist_experiments")
734
- }
735
-
736
- /// WaitlistScore — the rules layer's output for one signup (#4661 D3; written
737
- /// by A2 #4662). leadId is a plain scalar into inbox.Lead (ADR 0009). One row
738
- /// per lead — re-scoring overwrites, never appends.
739
- model WaitlistScore {
740
- id String @id @default(dbgenerated("('wlscore_'::text || (uuidv7())::text)")) @db.Text
741
- siteId String @map("site_id")
742
- // #6374 — the tenancy fence, extended to this model (spec
743
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
744
- // Prisma relation directive (ADR-0009 invariant #1), and
745
- // waitlist_scores_workspace_site_agree CHECK (workspace_id = site_id) — since
746
- // siteId IS the resolved workspace id, Postgres itself rejects a row
747
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
748
- // value, never one from a request body (AC-6).
749
- workspaceId String @map("workspace_id")
750
- leadId String @map("lead_id")
751
- experimentKey String @map("experiment_key")
752
- waitlistKey String @map("waitlist_key")
753
- tier String
754
- score Int
755
- categoryScores Json @map("category_scores")
756
- ruleTrace Json @map("rule_trace")
757
- createdAt DateTime @default(now()) @map("created_at")
758
- updatedAt DateTime @updatedAt @map("updated_at")
759
-
760
- // #5036 D6d — one score row per (lead, EXPERIMENT), not per lead.
761
- //
762
- // `@@unique([leadId])` was right for a single instrument and wrong for a
763
- // marketplace of them: a visitor who joins two founders' waitlists is one
764
- // lead with two scores, and the narrower constraint permitted one. The
765
- // second write silently overwrote the first, so one founder's evidence
766
- // disappeared and their weekly CSV under-reported with no error raised.
767
- // Scoping the key to the experiment keeps the overwrite-on-retake behaviour
768
- // that `scoreCompletedSignup` relies on, per project rather than globally.
769
- @@unique([leadId, experimentKey])
770
- @@index([siteId, experimentKey, waitlistKey])
771
- @@index([workspaceId])
772
- @@map("waitlist_scores")
773
- }
774
-
775
- /// PlaybookInstance — a compiled, person-scoped Playbook (FEAT-074 U4a,
776
- /// #4715). Append-only: `scoreCompletedSignup` (A2) fires the compile on
777
- /// every `targeting-maturity` completion, so a retake writes a NEW row
778
- /// rather than overwriting — unlike WaitlistScore, which is
779
- /// @@unique([leadId, experimentKey]) and overwritten on retake of the SAME
780
- /// experiment (D2; scoped per experiment by #5036 D6d). `tier`/`ruleTrace` are SNAPSHOTS of the
781
- /// WaitlistScore at compile time; `scoreId` is kept only as the provenance
782
- /// pointer to the (possibly since-overwritten) live row. `source` is the
783
- /// U1 `playbook-source` machine-block object; `markdown` is the rendered
784
- /// `PLAYBOOK-<slug>.md` body U4b (#4718) reads to render + deliver.
785
- model PlaybookInstance {
786
- id String @id @default(dbgenerated("('pbk_'::text || (uuidv7())::text)")) @db.Text
787
- siteId String @map("site_id")
788
- // #6374 — the tenancy fence, extended to this model (spec
789
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
790
- // Prisma relation directive (ADR-0009 invariant #1), and
791
- // playbook_instances_workspace_site_agree CHECK (workspace_id = site_id) — since
792
- // siteId IS the resolved workspace id, Postgres itself rejects a row
793
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
794
- // value, never one from a request body (AC-6).
795
- workspaceId String @map("workspace_id")
796
- leadId String @map("lead_id")
797
- experimentKey String @map("experiment_key")
798
- scoreId String @map("score_id")
799
- tier String
800
- level Int
801
- ruleTrace Json @map("rule_trace")
802
- source Json
803
- markdown String @db.Text
804
- createdAt DateTime @default(now()) @map("created_at")
805
-
806
- @@index([siteId, leadId, createdAt])
807
- @@index([workspaceId])
808
- @@map("playbook_instances")
809
- }
810
-
811
- /// WaitlistFollowUpSend — the audit stamp for one attempted follow-up email
812
- /// (#4732 D1/D5). `@@unique([leadId, followUpKey])` IS the sweep's
813
- /// idempotency guarantee (mirrors WaitlistScore's own `@@unique([leadId])`
814
- /// shape) — the stateless `runWaitlistFollowUpSweep` (#4732)
815
- /// selects candidates by the ABSENCE of a matching row, so a re-run never
816
- /// double-sends. `status` is 'sent' | 'failed' (no Prisma enum — this repo
817
- /// uses none, mirroring DeliverableGrant.sendStatus's free-string
818
- /// convention); 'failed' is terminal, no auto-retry (the abandon-email
819
- /// sweep's own posture) — a suppressed/skipped candidate (wrong tier, not
820
- /// yet due, a booked call, or unsubscribed) is NEVER stamped here at all, so
821
- /// it is re-evaluated on every later sweep run until it either sends or its
822
- /// completedAt/offsetDays window is permanently past.
823
- model WaitlistFollowUpSend {
824
- id String @id @default(dbgenerated("('wlfu_'::text || (uuidv7())::text)")) @db.Text
825
- siteId String @map("site_id")
826
- // #6374 — the tenancy fence, extended to this model (spec
827
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
828
- // Prisma relation directive (ADR-0009 invariant #1), and
829
- // waitlist_followup_sends_workspace_site_agree CHECK (workspace_id = site_id) — since
830
- // siteId IS the resolved workspace id, Postgres itself rejects a row
831
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
832
- // value, never one from a request body (AC-6).
833
- workspaceId String @map("workspace_id")
834
- leadId String @map("lead_id")
835
- followUpKey String @map("follow_up_key")
836
- sentAt DateTime @map("sent_at")
837
- status String
838
-
839
- @@unique([leadId, followUpKey])
840
- @@index([siteId])
841
- @@index([workspaceId])
842
- @@map("waitlist_followup_sends")
843
- }
844
-
845
- /// ListingCsvSend — the weekly founder-CSV send stamp (#5001). One row per
846
- /// DELIVERED CSV for one ProjectListing: `sentAt` is the watermark the next
847
- /// sweep selects after (signups whose WaitlistScore landed at or before it are
848
- /// already in a delivered CSV), `rowCount` is how many rows that CSV carried,
849
- /// and `grantId` is the DeliverableGrant whose id is the capability token in
850
- /// the founder's download link. Plain-scalar refs to ProjectListing /
851
- /// DeliverableGrant (ADR 0009 invariant 1) — no cross-model @relation. A
852
- /// listing with no new signups gets no row, so "no new rows ⇒ no send" is
853
- /// visible in the data, not only in the sweep's control flow.
854
- model ListingCsvSend {
855
- id String @id @default(dbgenerated("('lcsv_'::text || (uuidv7())::text)")) @db.Text
856
- // #6374 D2 — the tenancy fence, in the shape this model's ABSENT site_id
857
- // forces (operator disposition, 2026-09-20). It gets NOT NULL plus the
858
- // listing_csv_sends_workspace_id_format CHECK and NOTHING else: with no
859
- // site_id column there is nothing for an equality CHECK to compare, and a
860
- // composite FK to ProjectListing — legal, both models are inbox — was
861
- // offered and declined in favour of ONE fence shape. Backfilled and
862
- // stamped from the parent listing row, never from a request body (AC-6).
863
- workspaceId String @map("workspace_id")
864
- listingId String @map("listing_id")
865
- sentAt DateTime @map("sent_at")
866
- rowCount Int @map("row_count")
867
- grantId String @map("grant_id")
868
-
869
- @@index([listingId, sentAt])
870
- @@index([workspaceId])
871
- @@map("listing_csv_sends")
872
- }
873
-
874
- /// FlowCheckout — the recommender buyer record (#4837 D2/D4): one row per
875
- /// checkout ATTEMPT. `amountMinor` is the server's own `price().total` × 100
876
- /// (D6); `quote` is the full WaitlistPriceQuote beside it, so an onboarding
877
- /// reader sees the seat line and credit line, not only a total. `status` is
878
- /// 'created' | 'completed' | 'expired' (no Prisma enum — this repo uses none,
879
- /// mirroring DeliverableGrant.sendStatus's free-string convention); the
880
- /// webhook (#4838) advances it with a guarded update on 'created'. Plain-scalar
881
- /// refs to Lead / FlowSession (ADR 0009 invariant 1) — no cross-model
882
- /// @relation. `permalink` is the answers-only payload string the visitor's
883
- /// recommendation was rendered from (#4770). Written only by
884
- /// apps/api/src/services/recommender-capture.ts.
885
- model FlowCheckout {
886
- id String @id @default(dbgenerated("('flchk_'::text || (uuidv7())::text)")) @db.Text
887
- siteId String @map("site_id")
888
- // #6374 — the tenancy fence, extended to this model (spec
889
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
890
- // Prisma relation directive (ADR-0009 invariant #1), and
891
- // flow_checkouts_workspace_site_agree CHECK (workspace_id = site_id) — since
892
- // siteId IS the resolved workspace id, Postgres itself rejects a row
893
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
894
- // value, never one from a request body (AC-6).
895
- workspaceId String @map("workspace_id")
896
- flowSlug String @map("flow_slug")
897
- experimentKey String @map("experiment_key")
898
- sessionId String @map("session_id")
899
- leadId String @map("lead_id")
900
- offerKey String @map("offer_key")
901
- seats Int
902
- period String
903
- creditStop Json @map("credit_stop")
904
- amountMinor Int @map("amount_minor")
905
- currency String
906
- quote Json
907
- permalink String @db.Text
908
- stripeSessionId String? @map("stripe_session_id")
909
- // The hosted Checkout page URL an identical resubmission is replayed with
910
- // (#4912 D6) — stamped in the same write as `stripeSessionId`, because a
911
- // Checkout Session URL cannot be reconstructed from its id.
912
- checkoutUrl String? @db.Text @map("checkout_url")
913
- // The server-derived fingerprint of the checkout request (#4912 D5): a
914
- // SHA-256 hex digest, never the request fields themselves. Null on
915
- // pre-#4912 rows and on a row whose key a reconcile released.
916
- idempotencyKey String? @map("idempotency_key")
917
- stripeCustomerId String? @map("stripe_customer_id")
918
- status String
919
- completedAt DateTime? @map("completed_at")
920
- createdAt DateTime @default(now()) @map("created_at")
921
- updatedAt DateTime @updatedAt @map("updated_at")
922
-
923
- // The webhook's join (#4838) — nullable, so `created` rows with no session
924
- // id yet never collide.
925
- @@unique([stripeSessionId])
926
- // The /checkout idempotency guarantee (#4912 D5), tenant-scoped so a
927
- // fingerprint can never reach another site's row. Nullable, so pre-#4912
928
- // rows and reconciled rows never collide.
929
- @@unique([siteId, idempotencyKey])
930
- // The dashboard's checkedOut read (#4839 D3): siteId + experimentKey + status.
931
- @@index([siteId, experimentKey, status])
932
- // Onboarding's read: everything a lead bought.
933
- @@index([leadId])
934
- @@index([workspaceId])
935
- @@map("flow_checkouts")
936
- }
937
-
938
- /// ProjectListing — the founder's application row for the project-listings
939
- /// workflow (#4998): apply → Slack approve/reject → build issue → data-driven
940
- /// page + diagnostic → weekly CSV. `leadId` is a plain scalar ref into this
941
- /// namespace's own `Lead` (ADR 0009 invariant 1) — the POST route writes both
942
- /// rows via `routeLead` (kind `submit`, source `projects-apply`) then this
943
- /// row, never the reverse. `questions` is the diagnostic template instance
944
- /// (Q2–Q7 + up to 3 extras, bounded by `projectListingQuestionsSchema` in
945
- /// `@working-theory/validation`) per
946
- /// docs/decisions/2026-09-07-project-listings-workflow.md (consumer repo,
947
- /// salessmyth/samhenry — this row and its routes vendor down, operator
948
- /// 2026-09-07 option 2). `status` is a free string (`applied` | `approved` |
949
- /// `rejected` | `active`, no Prisma enum — this repo uses none, mirroring
950
- /// `FlowCheckout.status`'s convention). Those are the four STORED values
951
- /// (#5033): the fifth lifecycle state, `closed`, is COMPUTED in the read
952
- /// from `windowEndsAt` and never written, so an expired listing cannot stay
953
- /// publicly open because a scheduled job failed. `draft` is defined so the
954
- /// vocabulary never migrates again and is reachable through no route today.
955
- /// `decidedAt`/`decidedBy` stamp the approve/reject moment, and an approve
956
- /// also stamps `windowEndsAt = decidedAt + windowDays`. `windowDays` is the
957
- /// applicant's requested window ALREADY CLAMPED at create against the site's
958
- /// `SiteListingConfig` (database/config/schema.prisma) — the config itself
959
- /// is never accepted from a request body. `issueUrl` (the build issue,
960
- /// #5000) and `experimentId` (the generated flow's waitlist experiment,
961
- /// #5001) are null until their respective downstream unit lands.
962
- /// `featured` is the operator-set shelf flag, surfaced on the public read
963
- /// only where the site's config enables it. `headline`, `logoMarkUrl` and
964
- /// `logoWordmarkUrl` are the listing card's presentation fields; the two
965
- /// logo columns ship nullable with NO upload path (#5033 D5 — no storage
966
- /// dependency exists, and accepting arbitrary files from anonymous
967
- /// strangers deserves its own review), and a card with both absent renders
968
- /// a designed name-only treatment. `slackChannel`/`slackTs`
969
- /// (#4999) are the posted application message's coordinates, stamped by
970
- /// `postListingApplication` right after `chat.postMessage` succeeds, so the
971
- /// decision handler can `chat.update` the same message in place; null when
972
- /// Slack is unconfigured or the post failed. Still out of scope here: the
973
- /// GitHub issue (#5000), the diagnostic flow + weekly CSV (#5001).
974
- model ProjectListing {
975
- id String @id @default(dbgenerated("('prjl_'::text || (uuidv7())::text)")) @db.Text
976
- siteId String @map("site_id")
977
- // #6203 — the isolation key `tenantDb(tenant).projectListings` filters on.
978
- // Plain scalar, no @relation (ADR 0009 D1, same reasoning as ownerUserId
979
- // below).
980
- //
981
- // #6254 AC-3/AC-4/AC-7 — NOT NULL as of
982
- // database/migrations/20260922120000_tenancy_fence, which also normalizes
983
- // `siteId` to the resolved workspace id and adds
984
- // `project_listings_workspace_site_agree CHECK ("workspace_id" =
985
- // "site_id")` — see `Lead.workspaceId` (top of database/inbox/schema.prisma)
986
- // for the full rationale. Every write site derives this value
987
- // server-side; never from a request body (AC-6).
988
- workspaceId String @map("workspace_id")
989
- // #5036 — written at SUBMIT, not at start. The founder's contact details are
990
- // already captured on THIS row at step one, so a `Lead` at step one adds no
991
- // capture; what it does add is an operator notification email per abandoned
992
- // first step, on an anonymous route (PR #5042 security review, finding 4).
993
- // `routeLead` remains the one Lead write path; it just runs later.
994
- leadId String? @map("lead_id")
995
- // #5036 D1 — every field the SUBMIT payload requires is nullable at rest.
996
- // Validation moved from write-time to submit-time when the application
997
- // became a ten-step draft that saves as it goes; a half-filled draft is a
998
- // legitimate row, and a NOT NULL here would make one unstorable. Only
999
- // `submit` asserts completeness.
1000
- slug String?
1001
- name String?
1002
- oneLiner String? @map("one_liner")
1003
- problem String? @db.Text
1004
- // #5096 — `problem`'s narrative other half ("how are you solving it?").
1005
- // OPT-IN, not required: it is absent from the platform's default
1006
- // `collectedFields` mask, so a site collects it only by naming it.
1007
- solution String? @db.Text
1008
- url String?
1009
- // #5036 D2 — the six-rung product ladder that replaced the four-value
1010
- // `stage`. The migration remaps every old value by an explicit map; there
1011
- // is no shim, because the two ladders do not align rung-for-rung.
1012
- product String?
1013
- // #5036 D3/D5 — the two segmentation axes shared with the diagnostic.
1014
- commitment String?
1015
- teamComposition String? @map("team_composition")
1016
- // #5036 D5 — exactly three {question, answer} entries, the detail page's
1017
- // fixed FAQ slot.
1018
- faqs Json?
1019
- anythingElse String? @db.Text @map("anything_else")
1020
- // #5036 D6 — a legal control, not a contact detail: visitors answering this
1021
- // founder's diagnostic are owed the identity and address of the party
1022
- // collecting their data. Carried on the PUBLIC read for that reason.
1023
- //
1024
- // #5279 — still the single-line DISPLAY string. Where the six part columns
1025
- // below are written, this is COMPOSED from them server-side; where they are
1026
- // not, it is the line the founder typed, verbatim.
1027
- address String?
1028
- // #5279 — the structured parts of the address above, beside it rather than
1029
- // instead of it. Nullable for the same reason as the #5036 D1 block above: a
1030
- // row written before #5279 has no parts, and none may ever be GUESSED from
1031
- // its `address` string. Parts compose into a line deterministically; a line
1032
- // does not decompose into parts, and a wrong split silently corrupts the
1033
- // party identification the listing's privacy notice points at — so the
1034
- // migration deliberately backfills nothing and a null part stays null for
1035
- // the life of the row. Composition runs one way only, in `draftUpdateData`
1036
- // (apps/api/src/services/project-listings.ts).
1037
- addressLine1 String? @map("address_line1")
1038
- addressLine2 String? @map("address_line2")
1039
- addressCity String? @map("address_city")
1040
- addressRegion String? @map("address_region")
1041
- addressPostalCode String? @map("address_postal_code")
1042
- addressCountry String? @map("address_country")
1043
- questions Json?
1044
- // #5285 — the answers to the site's OWN questions: one flat JSON object of
1045
- // `{ questionId: answer }` for everything a site asks that the PLATFORM does
1046
- // not read. The field contract (packages/validation/.../listing-
1047
- // application-contract.ts) names what the platform reads and therefore what
1048
- // earns a column; everything else was previously unaskable without a
1049
- // migration and a release, which is what this column removes.
1050
- //
1051
- // BOUNDED, not free-form, and that is the point: the write path is
1052
- // anonymous (a resume token and nothing else), so an unbounded JSON bag on
1053
- // it is a place for a stranger to park arbitrary text in the table. The
1054
- // server accepts only ids the SITE declared in its own `SiteListingConfig`,
1055
- // at that site's own count and length caps, and refuses the whole request
1056
- // otherwise — so what lands here is never larger than the questions the
1057
- // operator chose to ask. Null on every row written before #5285 and on
1058
- // every site that asks nothing of its own.
1059
- //
1060
- // Only the `public`-visibility subset rides the public read; an
1061
- // `operator-only` answer reaches the dashboard and the review payload and
1062
- // stops there.
1063
- siteAnswers Json? @map("site_answers")
1064
- // #5407 — still the single-line DISPLAY name, and still NOT NULL: `start`
1065
- // has always required one and every reader below expects one. Where the two
1066
- // part columns beside it are written, this is COMPOSED from them
1067
- // server-side (`startProjectListingDraft`); where they are not, it is the
1068
- // line the founder typed, verbatim.
1069
- //
1070
- // Read by: the submit-time slug fallback, the approve/reject email's
1071
- // greeting, the Slack review card's Founder row, the build issue, and the
1072
- // `routeLead` identity. None of them changes.
1073
- founderName String @map("founder_name")
1074
- // #5407 — the structured parts of the display name above, beside it rather
1075
- // than instead of it, exactly as #5279 put the six address parts beside
1076
- // `address`. Nullable for the #5036 D1 reason and one more: a row written
1077
- // before #5407 has no parts, and none may ever be GUESSED from its
1078
- // `founder_name`. "First Last" composes one way only; a name does not
1079
- // decompose (a two-word split mangles "van der Berg", "Maria del Carmen"
1080
- // and every mononym), so the migration backfills nothing and a null part
1081
- // stays null for the life of the row.
1082
- founderFirstName String? @map("founder_first_name")
1083
- founderLastName String? @map("founder_last_name")
1084
- founderEmail String @map("founder_email")
1085
- // #5407 — the founder's phone, collected on the default contact screen
1086
- // alongside the email (samhenry spec 316 D1). Nullable at rest for the
1087
- // #5036 D1 reason AND permanently: a row written before #5407 has none, and
1088
- // a site may declare the question optional through its own config.
1089
- //
1090
- // NEVER on the public read: `projectListingPublicSchema` omits it beside
1091
- // `founderEmail`. The address on this row is a legal control a visitor is
1092
- // owed; a phone number is a contact detail nobody outside the operator is.
1093
- founderPhone String? @map("founder_phone")
1094
- // #5407 — sha-256 of the EMAIL VERIFICATION NONCE, never the nonce itself.
1095
- //
1096
- // A second credential beside `resumeTokenHash`, and the reason
1097
- // `emailVerifiedAt` below means what its name says. The resume token is
1098
- // returned to WHOEVER POSTED `start`'s FORM, so presenting it proves only
1099
- // that you opened the draft. This nonce is minted at `start`, digested
1100
- // here, and handed to nothing except the resume-email builder
1101
- // (`services/project-listing-resume-email.ts`): it is never in a response
1102
- // body, never in a log, and never in the browser that filled the form. The
1103
- // only way to hold it is to have read mail sent to `founder_email`.
1104
- //
1105
- // Null on every row written before #5407, and on every row whose site has
1106
- // not switched the resume email on — there the nonce exists and is simply
1107
- // never delivered, so nothing can verify, which is the correct outcome.
1108
- // Cleared at `submit` beside `resumeTokenHash`, for the same reason: a
1109
- // credential that leaks after the draft stops being a draft is inert twice
1110
- // over.
1111
- emailVerifyNonceHash String? @unique @map("email_verify_nonce_hash")
1112
- // #5407 — when the founder proved control of `founder_email`, by opening
1113
- // the link mailed to it.
1114
- //
1115
- // WRITE-ONCE. `verifyProjectListingDraftEmail` sets it only where it is
1116
- // still null, so a founder who opens the link five times is stamped with
1117
- // the FIRST opening rather than the latest.
1118
- //
1119
- // WHAT IT PROVES, and its exact limit. The stamp requires the nonce above,
1120
- // which left this server only inside the email. So it says the holder can
1121
- // read mail sent to `founder_email` — which is the whole of what "email
1122
- // verified" claims. It says nothing about WHO they are: `owner_user_id`
1123
- // (#5295) is the column that carries an identity claim, and it still needs
1124
- // a signed-in user whose own address the auth system verified. A row can
1125
- // carry either, both, or neither.
1126
- //
1127
- // Permanently nullable, and NOT a backfill target. Every row written before
1128
- // #5407 predates the stamp; asserting verification on one would assert
1129
- // exactly the thing nobody proved. Null means "not verified", never
1130
- // "verification failed", and it is also what a site running no resume email
1131
- // reads forever.
1132
- emailVerifiedAt DateTime? @map("email_verified_at")
1133
- founderCompany String? @map("founder_company")
1134
- status String
1135
- // #5036 D1/D6 — when the founder ticked step one's consent box. Never null:
1136
- // a row cannot exist without it, because `start` requires it.
1137
- consentAt DateTime @default(now()) @map("consent_at") @db.Timestamptz(3)
1138
- // #5036 D1 — sha-256 of the opaque resume token, never the token itself.
1139
- // The token is the ONLY credential on an anonymous surface, so the column
1140
- // holds a digest an attacker who reads the table still cannot present.
1141
- // Cleared at submit, which makes a leaked token inert the moment the draft
1142
- // stops being a draft.
1143
- resumeTokenHash String? @unique @map("resume_token_hash")
1144
- headline String?
1145
- logoMarkUrl String? @map("logo_mark_url")
1146
- logoWordmarkUrl String? @map("logo_wordmark_url")
1147
- featured Boolean @default(false)
1148
- decidedAt DateTime? @map("decided_at")
1149
- decidedBy String? @map("decided_by")
1150
- // The operator's copy-edit audit trio. A listing that is `requested` or
1151
- // `active` can have its PUBLISHED COPY trimmed by the operator (headline,
1152
- // oneLiner, problem, faqs, questions) — the third review outcome beside
1153
- // approve and reject. `copyEditedAt`/`copyEditedBy` mirror the decision pair
1154
- // directly above and are re-stamped on every edit.
1155
- //
1156
- // `submittedCopy` is WRITE-ONCE, on the FIRST edit only: it holds those five
1157
- // fields exactly as the applicant submitted them. Null for the untouched
1158
- // majority, which is why "what did the applicant actually send" reads as the
1159
- // snapshot if present and the row itself otherwise. Never cleared, so the
1160
- // submitted copy stays recoverable for the life of the row.
1161
- submittedCopy Json? @map("submitted_copy")
1162
- copyEditedAt DateTime? @map("copy_edited_at")
1163
- copyEditedBy String? @map("copy_edited_by")
1164
- issueUrl String? @map("issue_url")
1165
- // #5036 D6d — the experiment's KEY, not its id. `WaitlistScore` holds
1166
- // `experimentKey`, so holding the id here meant every listing-to-submissions
1167
- // read fetched the experiment purely to learn its key. One hop, the same
1168
- // string on both sides. ADR 0009 forbids a relation across the namespace
1169
- // boundary, so this is a plain string matched by convention — which is
1170
- // exactly why it has to be the string the other side actually stores.
1171
- experimentKey String? @map("experiment_key")
1172
- windowDays Int? @map("window_days")
1173
- windowEndsAt DateTime? @map("window_ends_at")
1174
- // The posted application message's coordinates (#4999) — set by
1175
- // `postListingApplication` right after `chat.postMessage` succeeds, so the
1176
- // Approve/Reject decision handler can `chat.update` the SAME message in
1177
- // place. Null when Slack is unconfigured or the post failed (create never
1178
- // rolls back on a Slack failure).
1179
- slackChannel String? @map("slack_channel")
1180
- slackTs String? @map("slack_ts")
1181
- // Listing -> owner-user link (#5295). Plain scalars — no cross-namespace
1182
- // @relation into auth.User (ADR 0009 invariant #1), exactly as
1183
- // `Lead.promotedUserId` / `promotedAt` above. Set by
1184
- // apps/api/src/services/listing-promotion.ts once the reconciling user's own
1185
- // email is VERIFIED, that verified address matches `founderEmail`, and this
1186
- // row's `siteId` resolves to one of that user's own workspaces.
1187
- //
1188
- // `founderEmail` is an UNVERIFIED string typed into an anonymous form, so it
1189
- // can never by itself say who owns the row: two people who type the same
1190
- // address are two people. This column is the only ownership claim any read
1191
- // may join on, and nothing derives it from `founderEmail` at read time.
1192
- //
1193
- // Nullable PERMANENTLY, not pending a backfill. A listing submitted by
1194
- // someone who never signs in has no owner for the life of the row, and that
1195
- // is a legitimate resting state: a backfill from `founderEmail` would assert
1196
- // a link nobody proved, which is the bug this column exists to remove.
1197
- ownerUserId String? @map("owner_user_id")
1198
- ownerLinkedAt DateTime? @map("owner_linked_at")
1199
- // #6314 — the listicle-kind `FlowDefinition` that CAPTURED this
1200
- // application: the magnet the visitor arrived through, so a magnet's own
1201
- // Applications list can be scoped to its own submissions rather than to the
1202
- // whole workspace (nothing constrains a workspace to one listicle magnet —
1203
- // `FlowDefinition` carries `@@unique([siteId, slug])` and no
1204
- // `@@unique([siteId, kind])`, so workspace-only scoping would conflate two).
1205
- //
1206
- // NOT `FlowDefinition.listingId` reversed: that column is written at
1207
- // APPROVAL time by `services/project-listing-flow.ts`, when an approved
1208
- // listing is projected into its own per-listing diagnostic sub-flow. A
1209
- // different link, at a different time, in the other direction.
1210
- //
1211
- // AN ID, NOT A SLUG (operator ruling 2026-09-19): a slug is a natural key
1212
- // `@@unique([siteId, slug])` implies is mutable, and `Lead.source`'s
1213
- // `'flow:' + slug` pattern is the weaker one, used there only because that
1214
- // write path has no resolve step. This one does — the wire carries a slug
1215
- // (no public surface hands a visitor a `flow_<uuidv7>`), and
1216
- // `startProjectListingDraft` resolves it server-side against `siteId` +
1217
- // `workspaceId` + `kind: "listicle"` before storing the id.
1218
- //
1219
- // PLAIN SCALAR REF — no `@relation`, no foreign key, no `onDelete`
1220
- // (operator decision 2026-09-20, withdrawing the ruling's "on-delete
1221
- // set-null" clause). Both models live in `inbox/`, so ADR 0009 invariant #1
1222
- // (which bans CROSS-namespace relations) does not reach this column and
1223
- // permission was never the question — the convention is: every
1224
- // intra-`inbox` reference on this model and its siblings (`leadId`,
1225
- // `listingId`, `grantId`, `flowSessionId`) is a plain scalar, and of the 24
1226
- // FOREIGN KEY constraints in `database/migrations/**` every one targets
1227
- // `auth`. The composite shape specifically was offered and declined on
1228
- // `ListingCsvSend` below.
1229
- //
1230
- // WHAT THAT GIVES UP, stated rather than argued away: the database does not
1231
- // constrain this to name a real magnet, nor one in the same workspace. Three
1232
- // things bound it — the resolve carries `siteId` and `kind`, so storing a
1233
- // foreign id needs the service layer to actively misbehave; nothing deletes
1234
- // a `FlowDefinition` (`routes/magnets/index.ts` D5: "No DELETE"), so
1235
- // `SET NULL` would have described no live code path; and the read carries
1236
- // the injected `workspaceId` alongside this id, so a dangling or foreign
1237
- // value yields NO rows, never another workspace's.
1238
- //
1239
- // WRITE-ONCE, at START. The magnet a visitor arrived through is knowable
1240
- // only on arrival — by submit, ten steps and possibly a resume-email
1241
- // round-trip later, the referring context is gone. `draftUpdateData` never
1242
- // learns this key, so no patch body can re-attribute a draft after the fact,
1243
- // the same shape `emailVerifiedAt`'s write-once stamp has.
1244
- //
1245
- // PERMANENTLY NULLABLE, and NOT a backfill target. Every row written before
1246
- // this column has no evidence naming the magnet that captured it — no
1247
- // referrer, no session link, no slug — so a backfill would have to guess
1248
- // "the workspace's only listicle magnet", which is exactly the conflation
1249
- // this column exists to remove, made permanent and unauditable. It is the
1250
- // reason `founder_name` parts (#5407), the address parts (#5279) and
1251
- // `owner_user_id` (#5295) each backfill nothing: a value that can only be
1252
- // guessed must not be asserted. NULL means "not attributed" — never
1253
- // "attribution failed" and never "captured directly": an application
1254
- // started through a direct link AFTER this ships is NULL for the same
1255
- // reason and is indistinguishable from a legacy row.
1256
- capturedByFlowId String? @map("captured_by_flow_id")
1257
- createdAt DateTime @default(now()) @map("created_at")
1258
- updatedAt DateTime @updatedAt @map("updated_at")
1259
-
1260
- @@unique([siteId, slug])
1261
- @@index([siteId, status])
1262
- // #5036 D7 — the concurrent-listing limit counts a founder's non-terminal
1263
- // rows on every submit, from an anonymous route. Indexed so a stranger
1264
- // cannot make that count a table scan.
1265
- @@index([siteId, founderEmail])
1266
- // The self-scoped read's whole WHERE clause (#5295) is `{ ownerUserId }` —
1267
- // the owner link IS the scope, so no site term joins it. Single-column by
1268
- // design: the promotion match (`founderEmail` + `siteId` + null owner) is
1269
- // already served by the (site_id, founder_email) index above.
1270
- @@index([ownerUserId])
1271
- // #6203 — backs a per-workspace list (tenantDb(tenant).projectListings.list).
1272
- @@index([workspaceId])
1273
- // #6314 — backs the per-magnet Applications read
1274
- // (tenantDb(tenant).projectListings.listByCapturingFlow), whose WHERE is
1275
- // exactly these two columns. Leading on `workspaceId`, matching the
1276
- // `@@index([workspaceId])` convention the fence set on this table. ONE
1277
- // index: no read reaches `capturedByFlowId` without a tenant, and with no
1278
- // foreign key there is no cascade wanting a referencing-side index either.
1279
- @@index([workspaceId, capturedByFlowId])
1280
- @@map("project_listings")
1281
- }
1282
-
1283
- /// ConsentGrant — one person's answer to one consent purpose, with the exact
1284
- /// wording they read (#5275). APPEND-ONLY: current state is the newest row
1285
- /// per (subject, purpose), so a later withdrawal never destroys the evidence
1286
- /// of the earlier grant (D9) — no `updatedAt`, and deliberately NO unique
1287
- /// constraint on subject + purpose. DECLINES ARE STORED as `granted: false`
1288
- /// (D8): an absent row cannot distinguish "they said no" from "we never
1289
- /// asked", and on an optional purpose that distinction is the only thing the
1290
- /// record is for. The subject is a type + id pair of plain scalars
1291
- /// (`project_listing` / `subscriber` / …) with NO cross-namespace @relation
1292
- /// (D10, ADR 0009 invariant 1) — which is what lets a new surface adopt
1293
- /// consent with a service call and no migration. `statement` is the resolved
1294
- /// text DENORMALIZED at write time (D7): a pointer into the site's config row
1295
- /// would prove nothing once that row is edited, and `statementHash` groups
1296
- /// identical wordings. `locale` is the locale the statement was actually
1297
- /// served in, not the one requested. `purposeId` is the site-config registry
1298
- /// id — the join key across surfaces. Two writers: apps/api/src/services/consent.ts
1299
- /// (every source), and the web sign-up hook apps/web/lib/sign-up-agreements.ts
1300
- /// (#8765 D4), which writes only `sign_up` rows (subject `user`) of a user it
1301
- /// creates in the same Better Auth transaction, from evidence the API built
1302
- /// with consent.ts's own functions.
1303
- model ConsentGrant {
1304
- id String @id @default(dbgenerated("('cnsnt_'::text || (uuidv7())::text)")) @db.Text
1305
- siteId String @map("site_id")
1306
- // #6374 — the tenancy fence, extended to this model (spec
1307
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
1308
- // Prisma relation directive (ADR-0009 invariant #1), and
1309
- // consent_grants_workspace_site_agree CHECK (workspace_id = site_id) — since
1310
- // siteId IS the resolved workspace id, Postgres itself rejects a row
1311
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
1312
- // value, never one from a request body (AC-6).
1313
- workspaceId String @map("workspace_id")
1314
- subjectType String @map("subject_type")
1315
- subjectId String @map("subject_id")
1316
- purposeId String @map("purpose_id")
1317
- granted Boolean
1318
- locale String
1319
- statement String @db.Text
1320
- statementHash String @map("statement_hash")
1321
- // #8438 D2 — the Person this row counts toward. A plain scalar with no
1322
- // foreign key (this table's own D10 posture): a row about a visitor or a
1323
- // listing has none, and a ledger row must outlive anything it names.
1324
- personId String? @map("person_id")
1325
- // #8438 D2 — where the answer came from (CONSENT_SOURCES). NOT NULL with no
1326
- // default after the migration: every new row must say. Rows stored before
1327
- // this column read `legacy`.
1328
- source String @db.Text
1329
- // The magnet slug when `source` is `magnet`.
1330
- sourceRef String? @map("source_ref") @db.VarChar(200)
1331
- // The registry's policy version the person read. NULL only for `legacy` rows.
1332
- policyVersion Int? @map("policy_version")
1333
- createdAt DateTime @default(now()) @map("created_at")
1334
-
1335
- // "What did this subject agree to" — every row for one subject, newest last.
1336
- @@index([siteId, subjectType, subjectId])
1337
- // "Who agreed to this purpose" — the cross-surface count the id exists for (D1/D2).
1338
- @@index([siteId, purposeId])
1339
- @@index([workspaceId])
1340
- // #8438 D2 — "current state per purpose" for one Person (DISTINCT ON).
1341
- @@index([workspaceId, personId, purposeId, createdAt])
1342
- @@map("consent_grants")
1343
- }
1344
-
1345
- // #8438 D2 — links a banner visitor (a random browser-held id) to the Person a
1346
- // later form or sign-in identifies, so the Person's cookie history can include
1347
- // the visitor's own rows without ever editing them. Append-only like the
1348
- // ledger: a database trigger refuses any change to a stored row. No relation
1349
- // directive (ADR-0009 invariant #1); no IP, user-agent or device column.
1350
- model ConsentVisitorLink {
1351
- id String @id @default(dbgenerated("('cvlink_'::text || (uuidv7())::text)")) @db.Text
1352
- workspaceId String @map("workspace_id")
1353
- visitorId String @map("visitor_id")
1354
- personId String @map("person_id")
1355
- source String @db.Text
1356
- createdAt DateTime @default(now()) @map("created_at")
1357
-
1358
- @@unique([workspaceId, visitorId, personId])
1359
- @@index([workspaceId, personId])
1360
- @@map("consent_visitor_links")
1361
- }
1362
-
1363
- /// Appointment — a Cal.com booking, normalized from the webhook receiver (#6085, brief H7).
1364
- ///
1365
- /// siteId-scoped with no FK, like every other inbox model (ADR 0009 D2). The
1366
- /// (siteId, provider, providerBookingUid) unique constraint IS the idempotency
1367
- /// key: every write is a guarded updateMany, and the create that follows a zero
1368
- /// match races on THIS index — which is what makes "exactly one row per booking
1369
- /// uid" a DATABASE property rather than a property of a process that may have
1370
- /// restarted. `provider` is in the key because a deployment may one day run two
1371
- /// providers against one receiver and their uid spaces do not coordinate (#4288).
1372
- model Appointment {
1373
- id String @id @default(dbgenerated("('appt_'::text || (uuidv7())::text)")) @db.Text
1374
- siteId String @map("site_id") @db.VarChar(200)
1375
- // #6374 — the tenancy fence, extended to this model (spec
1376
- // docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
1377
- // Prisma relation directive (ADR-0009 invariant #1), and
1378
- // appointments_workspace_site_agree CHECK (workspace_id = site_id) — since
1379
- // siteId IS the resolved workspace id, Postgres itself rejects a row
1380
- // whose two columns disagree. Every write site stamps a SERVER-RESOLVED
1381
- // value, never one from a request body (AC-6).
1382
- workspaceId String @map("workspace_id")
1383
- /// Cal.com's own booking `uid` — stable across reschedules of the same booking.
1384
- /// Bounded: a value above Postgres's btree entry limit cannot be indexed at all.
1385
- providerBookingUid String @map("provider_booking_uid") @db.VarChar(200)
1386
- /// Provider identifier, part of the unique key (#4288).
1387
- provider String @default("cal") @db.VarChar(32)
1388
- startTime DateTime @map("start_time")
1389
- endTime DateTime @map("end_time")
1390
- /// The event's own title as the provider stated it. PII-adjacent: never logged (NFR-1).
1391
- title String? @db.VarChar(300)
1392
- /// Free text typed by an anonymous prospect — bounded at the same 120/254 the
1393
- /// outbound seam already bounds them at (packages/validation SchedulingAttendeeSchema).
1394
- attendeeName String @map("attendee_name") @db.VarChar(120)
1395
- attendeeEmail String @map("attendee_email") @db.VarChar(254)
1396
- attendeeTimeZone String? @map("attendee_time_zone") @db.VarChar(64)
1397
- /// Normalized lifecycle state — see § D5. One of: pending | confirmed | cancelled.
1398
- status String @db.VarChar(32)
1399
- /// The payment flag. BOOKING_PAID sets it true; nothing else in this issue reads it.
1400
- paid Boolean @default(false)
1401
- /// Set by BOOKING_PAYMENT_INITIATED; a checkout was opened on Cal's side.
1402
- paymentInitiatedAt DateTime? @map("payment_initiated_at")
1403
- /// Set by BOOKING_NO_SHOW_UPDATED.
1404
- noShow Boolean @default(false) @map("no_show")
1405
- cancelledAt DateTime? @map("cancelled_at")
1406
- /// Three INDEPENDENT watermarks, one per event family (§ D7). One shared column
1407
- /// forced a choice between dropping a late payment fact and letting a replayed
1408
- /// one revert state; three columns make each family monotonic on its own terms.
1409
- lifecycleEventAt DateTime @map("lifecycle_event_at")
1410
- paymentEventAt DateTime? @map("payment_event_at")
1411
- noShowEventAt DateTime? @map("no_show_event_at")
1412
- createdAt DateTime @default(now()) @map("created_at")
1413
- updatedAt DateTime @updatedAt @map("updated_at")
1414
- /// #852 D1 — the Person of the attendee, in this row's workspace. Stamped
1415
- /// at create (and on a spoke bind's move) by appointments.ts through
1416
- /// findOrCreatePerson. Plain scalar, no @relation. NULL when the attendee
1417
- /// email is not a storable address.
1418
- personId String? @map("person_id")
1419
-
1420
- @@unique([siteId, provider, providerBookingUid])
1421
- @@index([siteId, startTime])
1422
- @@index([createdAt])
1423
- @@index([workspaceId])
1424
- @@index([personId])
1425
- @@map("appointments")
1426
- }
1427
-
1428
- /// AppointmentOrigin — which workspace a booking made THROUGH A SPOKE belongs
1429
- /// to (#7532, spec D5). Written by `POST /v1/schedule/book` the moment Cal
1430
- /// answers, and consulted by the deployment-wide Cal.com webhook BEFORE the
1431
- /// site alias, so a spoke's booking is stored in the spoke's workspace.
1432
- ///
1433
- /// One origin per booking uid: the unique key leaves `site_id` out. No
1434
- /// relation directive (ADR 0009) — `workspace_id` and `spoke_id` are plain
1435
- /// columns with format CHECKs, joined in apps/api/src/services/.
1436
- model AppointmentOrigin {
1437
- id String @id @default(dbgenerated("('apptorg_'::text || (uuidv7())::text)")) @db.Text
1438
- provider String @default("cal") @db.VarChar(32)
1439
- providerBookingUid String @map("provider_booking_uid") @db.VarChar(200)
1440
- workspaceId String @map("workspace_id")
1441
- spokeId String @map("spoke_id")
1442
- createdAt DateTime @default(now()) @map("created_at")
1443
-
1444
- @@unique([provider, providerBookingUid])
1445
- @@index([workspaceId])
1446
- @@map("appointment_origins")
1447
- }