create-ailk 0.24.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 (599) hide show
  1. package/README.md +42 -20
  2. package/component-catalog.json +3 -3
  3. package/component-catalog.md +3 -3
  4. package/dist/add-module.js +4 -2
  5. package/dist/add-route.d.ts +12 -1
  6. package/dist/add-route.js +31 -3
  7. package/dist/cli.js +12 -7
  8. package/dist/derive-architecture.d.ts +3 -4
  9. package/dist/derive-architecture.js +65 -29
  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/module-architecture.d.ts +20 -3
  20. package/dist/module-architecture.js +117 -550
  21. package/dist/paid-sections.d.ts +25 -1
  22. package/dist/paid-sections.js +88 -3
  23. package/dist/surfaces.js +10 -2
  24. package/dist/template-record.d.ts +14 -4
  25. package/dist/template-record.js +23 -5
  26. package/dist/topology.d.ts +16 -3
  27. package/dist/topology.js +31 -19
  28. package/dist/update.d.ts +10 -1
  29. package/dist/update.js +540 -16
  30. package/package.json +3 -3
  31. package/release-scope.json +1 -1
  32. package/templates/.claude/cloudparity-repo-hooks.json +5 -0
  33. package/templates/.claude/hooks/template-guard.sh +137 -0
  34. package/templates/.claude/rules/extending.md +106 -0
  35. package/templates/.claude/rules/loop-contract.md +2 -2
  36. package/templates/.claude/settings.json +11 -0
  37. package/templates/.env.example +5 -5
  38. package/templates/.github/workflows/ci.yml +7 -4
  39. package/templates/CLAUDE.md +11 -1
  40. package/templates/CONVENTIONS.md +8 -10
  41. package/templates/README.md +14 -13
  42. package/templates/apps/api/.env.example +2 -2
  43. package/templates/apps/api/CLAUDE.md +102 -76
  44. package/templates/apps/api/jest.config.cjs +15 -5
  45. package/templates/apps/api/package.json +16 -20
  46. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
  47. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
  48. package/templates/apps/api/src/index.ts +1 -1
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
  50. package/templates/apps/api/src/modules/commerce.ts +10 -0
  51. package/templates/apps/api/src/modules/content-write.ts +10 -0
  52. package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
  53. package/templates/apps/api/src/modules/scheduling.ts +10 -0
  54. package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
  55. package/templates/apps/api/src/modules/workspace.ts +10 -0
  56. package/templates/apps/api/src/server.ts +117 -1882
  57. package/templates/apps/api/vercel.json +2 -2
  58. package/templates/apps/mcp/CLAUDE.md +67 -20
  59. package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
  60. package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
  61. package/templates/apps/mcp/jest.config.cjs +6 -1
  62. package/templates/apps/mcp/package.json +5 -13
  63. package/templates/apps/mcp/src/app.ts +8 -7
  64. package/templates/apps/mcp/src/index.ts +6 -34
  65. package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
  66. package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
  67. package/templates/apps/mcp/src/site.ts +12 -0
  68. package/templates/apps/web/.env.example +1 -1
  69. package/templates/apps/web/CLAUDE.md +10 -3
  70. package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
  71. package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
  72. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
  73. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
  74. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
  75. package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
  76. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
  77. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
  78. package/templates/apps/web/app/api/revalidate/route.ts +3 -3
  79. package/templates/apps/web/auth.ts +2 -2
  80. package/templates/apps/web/jest.config.cjs +1 -0
  81. package/templates/apps/web/lib/auth-email.ts +1 -1
  82. package/templates/apps/web/lib/relay.ts +2 -2
  83. package/templates/apps/web/lib/release-scope.ts +47 -0
  84. package/templates/apps/web/lib/server-api.ts +1 -1
  85. package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
  86. package/templates/customizations.yaml +37 -0
  87. package/templates/database/prisma.config.ts +10 -14
  88. package/templates/database/seed-test.ts +2 -2
  89. package/templates/database/seed.ts +5 -5
  90. package/templates/package.json +12 -6
  91. package/templates/pnpm-workspace.yaml +0 -1
  92. package/templates/project.yaml +16 -8
  93. package/templates/tools/jest/reporters.cjs +65 -0
  94. package/templates/tools/lint-architecture.ts +553 -7
  95. package/templates/tsconfig.json +1 -2
  96. package/versions.json +48 -35
  97. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
  98. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
  99. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
  100. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
  101. package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
  102. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
  103. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
  104. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
  105. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
  106. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
  107. package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
  108. package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
  109. package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
  110. package/templates/apps/api/src/bin/spokes.ts +0 -198
  111. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
  112. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
  113. package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
  114. package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
  115. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
  116. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
  117. package/templates/apps/api/src/config/index.ts +0 -596
  118. package/templates/apps/api/src/config/modules.ts +0 -83
  119. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
  120. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
  121. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
  122. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
  123. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
  124. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
  125. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
  126. package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
  127. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
  128. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
  129. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
  130. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
  131. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
  132. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
  133. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
  134. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
  135. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
  136. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
  137. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
  138. package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
  139. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
  140. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
  141. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
  142. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
  143. package/templates/apps/api/src/lib/agent-store.ts +0 -613
  144. package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
  145. package/templates/apps/api/src/lib/consent-store.ts +0 -363
  146. package/templates/apps/api/src/lib/content-adapter.ts +0 -112
  147. package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
  148. package/templates/apps/api/src/lib/error-serializer.ts +0 -164
  149. package/templates/apps/api/src/lib/errors.ts +0 -95
  150. package/templates/apps/api/src/lib/file-fallback.ts +0 -87
  151. package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
  152. package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
  153. package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
  154. package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
  155. package/templates/apps/api/src/lib/license-key-store.ts +0 -45
  156. package/templates/apps/api/src/lib/locales.ts +0 -201
  157. package/templates/apps/api/src/lib/outbound-http.ts +0 -127
  158. package/templates/apps/api/src/lib/prisma.ts +0 -21
  159. package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
  160. package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
  161. package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
  162. package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
  163. package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
  164. package/templates/apps/api/src/lib/site.ts +0 -257
  165. package/templates/apps/api/src/lib/slice-load.ts +0 -90
  166. package/templates/apps/api/src/lib/spoke-store.ts +0 -180
  167. package/templates/apps/api/src/lib/stripe.ts +0 -321
  168. package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
  169. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
  170. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
  171. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
  172. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
  173. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
  174. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
  175. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
  176. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
  177. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
  178. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
  179. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
  180. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
  181. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
  182. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
  183. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
  184. package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
  185. package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
  186. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
  187. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
  188. package/templates/apps/api/src/middleware/auth.ts +0 -864
  189. package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
  190. package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
  191. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
  192. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
  193. package/templates/apps/api/src/openapi/index.ts +0 -18
  194. package/templates/apps/api/src/openapi/spec.ts +0 -2148
  195. package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
  196. package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
  197. package/templates/apps/api/src/routes/aeo/index.ts +0 -107
  198. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
  199. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
  200. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
  201. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
  202. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
  203. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
  204. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
  205. package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
  206. package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
  207. package/templates/apps/api/src/routes/auth/user.ts +0 -305
  208. package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
  209. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
  210. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
  211. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
  212. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
  213. package/templates/apps/api/src/routes/checkout/index.ts +0 -34
  214. package/templates/apps/api/src/routes/checkout/session.ts +0 -171
  215. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
  216. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
  217. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
  218. package/templates/apps/api/src/routes/consent/account.ts +0 -268
  219. package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
  220. package/templates/apps/api/src/routes/consent/index.ts +0 -26
  221. package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
  222. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
  223. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
  224. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
  225. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
  226. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
  227. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
  228. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
  229. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
  230. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
  231. package/templates/apps/api/src/routes/content/get-page.ts +0 -183
  232. package/templates/apps/api/src/routes/content/index.ts +0 -49
  233. package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
  234. package/templates/apps/api/src/routes/content/list-types.ts +0 -19
  235. package/templates/apps/api/src/routes/content/offerings.ts +0 -184
  236. package/templates/apps/api/src/routes/content/pricing.ts +0 -176
  237. package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
  238. package/templates/apps/api/src/routes/content/search.ts +0 -100
  239. package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
  240. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
  241. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
  242. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
  243. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
  244. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
  245. package/templates/apps/api/src/routes/leads/index.ts +0 -639
  246. package/templates/apps/api/src/routes/leads/list.ts +0 -104
  247. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
  248. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
  249. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
  250. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
  251. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
  252. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
  253. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
  254. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
  255. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
  256. package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
  257. package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
  258. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
  259. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
  260. package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
  261. package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
  262. package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
  263. package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
  264. package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
  265. package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
  266. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
  267. package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
  268. package/templates/apps/api/src/routes/requests/index.ts +0 -54
  269. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
  270. package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
  271. package/templates/apps/api/src/routes/webhooks/README.md +0 -398
  272. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
  273. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
  274. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
  275. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
  276. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
  277. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
  278. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
  279. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
  280. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
  281. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
  282. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
  283. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
  284. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
  285. package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
  286. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
  287. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
  288. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
  289. package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
  290. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
  291. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
  292. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
  293. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
  294. package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
  295. package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
  296. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
  297. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
  298. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
  299. package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
  300. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
  301. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
  302. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
  303. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
  304. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
  305. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
  306. package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
  307. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
  308. package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
  309. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
  310. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
  311. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
  312. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
  313. package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
  314. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
  315. package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
  316. package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
  317. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
  318. package/templates/apps/api/src/services/abandon-email.ts +0 -336
  319. package/templates/apps/api/src/services/aeo-score.ts +0 -325
  320. package/templates/apps/api/src/services/answer-distributions.ts +0 -138
  321. package/templates/apps/api/src/services/audit-log.ts +0 -961
  322. package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
  323. package/templates/apps/api/src/services/consent.ts +0 -834
  324. package/templates/apps/api/src/services/default-tenant.ts +0 -416
  325. package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
  326. package/templates/apps/api/src/services/dunning-policy.ts +0 -294
  327. package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
  328. package/templates/apps/api/src/services/entitlements.ts +0 -338
  329. package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
  330. package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
  331. package/templates/apps/api/src/services/lead-promotion.ts +0 -160
  332. package/templates/apps/api/src/services/lead-routing.ts +0 -1372
  333. package/templates/apps/api/src/services/leads-inbox.ts +0 -312
  334. package/templates/apps/api/src/services/listing-config.ts +0 -178
  335. package/templates/apps/api/src/services/magnet-completion.ts +0 -242
  336. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
  337. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
  338. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
  339. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
  340. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
  341. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
  342. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
  343. package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
  344. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
  345. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
  346. package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
  347. package/templates/apps/api/src/services/offerings/index.ts +0 -614
  348. package/templates/apps/api/src/services/offerings/source.ts +0 -140
  349. package/templates/apps/api/src/services/offerings/store.ts +0 -150
  350. package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
  351. package/templates/apps/api/src/services/person.ts +0 -1211
  352. package/templates/apps/api/src/services/plan-offerings.ts +0 -125
  353. package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
  354. package/templates/apps/api/src/services/project-listings.ts +0 -2085
  355. package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
  356. package/templates/apps/api/src/services/request-confirm.ts +0 -698
  357. package/templates/apps/api/src/services/revalidation.ts +0 -235
  358. package/templates/apps/api/src/services/slack-notify.ts +0 -291
  359. package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
  360. package/templates/apps/api/src/services/usage-credits.ts +0 -340
  361. package/templates/apps/api/src/services/verified-email.ts +0 -71
  362. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
  363. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
  364. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
  365. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
  366. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
  367. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
  368. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
  369. package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
  370. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
  371. package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
  372. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
  373. package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
  374. package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
  375. package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
  376. package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
  377. package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
  378. package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
  379. package/templates/apps/mcp/__tests__/index.test.ts +0 -195
  380. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
  381. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
  382. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
  383. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
  384. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
  385. package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
  386. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
  387. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
  388. package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
  389. package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
  390. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
  391. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
  392. package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
  393. package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
  394. package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
  395. package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
  396. package/templates/apps/mcp/__tests__/server.test.ts +0 -388
  397. package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
  398. package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
  399. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
  400. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
  401. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
  402. package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
  403. package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
  404. package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
  405. package/templates/apps/mcp/src/abuse/index.ts +0 -349
  406. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
  407. package/templates/apps/mcp/src/boot.ts +0 -167
  408. package/templates/apps/mcp/src/config/modules.ts +0 -43
  409. package/templates/apps/mcp/src/http-transport.ts +0 -564
  410. package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
  411. package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
  412. package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
  413. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
  414. package/templates/apps/mcp/src/lib/errors.ts +0 -61
  415. package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
  416. package/templates/apps/mcp/src/lib/locales.ts +0 -171
  417. package/templates/apps/mcp/src/lib/prisma.ts +0 -39
  418. package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
  419. package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
  420. package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
  421. package/templates/apps/mcp/src/lib/site-config.ts +0 -53
  422. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
  423. package/templates/apps/mcp/src/request-context.ts +0 -52
  424. package/templates/apps/mcp/src/server.ts +0 -343
  425. package/templates/apps/mcp/src/serverless-app.ts +0 -88
  426. package/templates/apps/mcp/src/tools/audit.ts +0 -127
  427. package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
  428. package/templates/apps/mcp/src/tools/create_project.ts +0 -134
  429. package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
  430. package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
  431. package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
  432. package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
  433. package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
  434. package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
  435. package/templates/apps/mcp/src/tools/index.ts +0 -138
  436. package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
  437. package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
  438. package/templates/apps/mcp/src/tools/proposals.ts +0 -27
  439. package/templates/apps/mcp/src/tools/purchase.ts +0 -205
  440. package/templates/apps/mcp/src/tools/read_content.ts +0 -81
  441. package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
  442. package/templates/apps/mcp/src/tools/search_content.ts +0 -63
  443. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
  444. package/templates/apps/mcp/src/workspace-server.ts +0 -224
  445. package/templates/database/CHANGELOG.md +0 -981
  446. package/templates/database/LICENSE +0 -202
  447. package/templates/database/NOTICE +0 -2
  448. package/templates/database/README.md +0 -243
  449. package/templates/database/__tests__/agents.db.test.ts +0 -320
  450. package/templates/database/__tests__/appointment.db.test.ts +0 -106
  451. package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
  452. package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
  453. package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
  454. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
  455. package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
  456. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
  457. package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
  458. package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
  459. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
  460. package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
  461. package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
  462. package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
  463. package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
  464. package/templates/database/__tests__/flows-presets.test.ts +0 -173
  465. package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
  466. package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
  467. package/templates/database/__tests__/member-area.db.test.ts +0 -257
  468. package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
  469. package/templates/database/__tests__/member-contract.db.test.ts +0 -413
  470. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
  471. package/templates/database/__tests__/offerings-migration.test.ts +0 -197
  472. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
  473. package/templates/database/__tests__/offerings.db.test.ts +0 -303
  474. package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
  475. package/templates/database/__tests__/person-contact.db.test.ts +0 -563
  476. package/templates/database/__tests__/person-notes.db.test.ts +0 -249
  477. package/templates/database/__tests__/person.db.test.ts +0 -586
  478. package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
  479. package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
  480. package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
  481. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
  482. package/templates/database/__tests__/seed.test.ts +0 -272
  483. package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
  484. package/templates/database/__tests__/tenancy.test.ts +0 -141
  485. package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
  486. package/templates/database/__tests__/user-cascade.test.ts +0 -140
  487. package/templates/database/_gitignore +0 -8
  488. package/templates/database/auth/.gitkeep +0 -0
  489. package/templates/database/auth/schema.prisma +0 -641
  490. package/templates/database/config/.gitkeep +0 -0
  491. package/templates/database/config/schema.prisma +0 -441
  492. package/templates/database/content/.gitkeep +0 -0
  493. package/templates/database/content/schema.prisma +0 -339
  494. package/templates/database/eslint.config.mjs +0 -69
  495. package/templates/database/flows/listing-diagnostic.ts +0 -182
  496. package/templates/database/flows/presets.ts +0 -315
  497. package/templates/database/inbox/.gitkeep +0 -0
  498. package/templates/database/inbox/README.md +0 -3
  499. package/templates/database/inbox/__tests__/schema.test.ts +0 -44
  500. package/templates/database/inbox/schema.prisma +0 -1447
  501. package/templates/database/jest.config.cjs +0 -36
  502. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
  503. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
  504. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
  505. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
  506. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
  507. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
  508. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
  509. package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
  510. package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
  511. package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
  512. package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
  513. package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
  514. package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
  515. package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
  516. package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
  517. package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
  518. package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
  519. package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
  520. package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
  521. package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
  522. package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
  523. package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
  524. package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
  525. package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
  526. package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
  527. package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
  528. package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
  529. package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
  530. package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
  531. package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
  532. package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
  533. package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
  534. package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
  535. package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
  536. package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
  537. package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
  538. package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
  539. package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
  540. package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
  541. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
  542. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
  543. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
  544. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
  545. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
  546. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
  547. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
  548. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
  549. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
  550. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
  551. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
  552. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
  553. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
  554. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
  555. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
  556. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
  557. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
  558. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
  559. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
  560. package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
  561. package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
  562. package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
  563. package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
  564. package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
  565. package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
  566. package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
  567. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
  568. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
  569. package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
  570. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
  571. package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
  572. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
  573. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
  574. package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
  575. package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
  576. package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
  577. package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
  578. package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
  579. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
  580. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
  581. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
  582. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
  583. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
  584. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
  585. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
  586. package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
  587. package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
  588. package/templates/database/migrations/migration_lock.toml +0 -3
  589. package/templates/database/ops/.gitkeep +0 -0
  590. package/templates/database/ops/schema.prisma +0 -43
  591. package/templates/database/package.json +0 -89
  592. package/templates/database/schema.prisma +0 -71
  593. package/templates/database/scripts/db-generate-locked.sh +0 -80
  594. package/templates/database/scripts/person-backfill.sql +0 -132
  595. package/templates/database/tenancy.ts +0 -183
  596. package/templates/database/tsconfig.build.json +0 -16
  597. package/templates/database/tsconfig.flows.json +0 -15
  598. package/templates/database/tsconfig.json +0 -16
  599. 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
- }