create-ailk 0.5.0 → 0.6.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 (329) hide show
  1. package/README.md +55 -8
  2. package/component-catalog.json +336 -27
  3. package/component-catalog.md +25 -12
  4. package/dist/cli.js +54 -5
  5. package/dist/copy.d.ts +14 -0
  6. package/dist/copy.js +38 -1
  7. package/dist/lib/api-boot-assertion.d.ts +46 -0
  8. package/dist/lib/api-boot-assertion.js +70 -0
  9. package/dist/lib/extract-module-bundle.d.ts +20 -1
  10. package/dist/lib/extract-module-bundle.js +64 -5
  11. package/dist/lib/import-graph.d.ts +29 -0
  12. package/dist/lib/import-graph.js +140 -0
  13. package/dist/lib/module-license-gate.d.ts +6 -1
  14. package/dist/lib/module-license-gate.js +6 -1
  15. package/dist/lib/pro-callout.js +1 -1
  16. package/dist/module-architecture.d.ts +1 -0
  17. package/dist/module-architecture.js +253 -127
  18. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  19. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  20. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  21. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  22. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  23. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  24. package/dist/programmatic.js +14 -10
  25. package/dist/surfaces.js +105 -7
  26. package/dist/template-manifest.d.ts +33 -0
  27. package/dist/template-manifest.js +99 -16
  28. package/dist/template-names.d.ts +1 -1
  29. package/dist/template-names.js +1 -0
  30. package/package.json +3 -2
  31. package/site-templates/marketing-site/DESIGN.md +148 -0
  32. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
  34. package/site-templates/marketing-site/ailk-template.json +1 -1
  35. package/site-templates/marketing-site-free/DESIGN.md +148 -0
  36. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
  37. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
  38. package/site-templates/minimalism-site/DESIGN.md +148 -0
  39. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
  40. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
  41. package/site-templates/minimalism-site/ailk-template.json +2 -1
  42. package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
  43. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
  44. package/site-templates/saas-product/DESIGN.md +148 -0
  45. package/site-templates/simple-service/DESIGN.md +148 -0
  46. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
  47. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
  48. package/site-templates/simple-service/ailk-template.json +10 -0
  49. package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
  50. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
  51. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
  52. package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
  53. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
  54. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
  55. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
  56. package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
  57. package/site-templates/simple-service/provenance.json +4 -0
  58. package/templates/.env.example +72 -7
  59. package/templates/CLAUDE.md +2 -3
  60. package/templates/DESIGN.md +155 -0
  61. package/templates/README.md +42 -8
  62. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
  63. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
  64. package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
  65. package/templates/apps/api/.env.example +13 -0
  66. package/templates/apps/api/CLAUDE.md +26 -2
  67. package/templates/apps/api/jest.config.cjs +3 -0
  68. package/templates/apps/api/package.json +1 -0
  69. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
  70. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
  71. package/templates/apps/api/src/__tests__/server.test.ts +471 -0
  72. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
  73. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
  74. package/templates/apps/api/src/config/index.ts +62 -0
  75. package/templates/apps/api/src/config/modules.ts +7 -3
  76. package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
  77. package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
  78. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
  79. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
  81. package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
  82. package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
  83. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
  84. package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
  85. package/templates/apps/api/src/lib/content-adapter.ts +57 -31
  86. package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
  87. package/templates/apps/api/src/lib/error-serializer.ts +23 -3
  88. package/templates/apps/api/src/lib/outbound-http.ts +7 -2
  89. package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
  90. package/templates/apps/api/src/lib/site.ts +208 -9
  91. package/templates/apps/api/src/lib/stripe.ts +66 -0
  92. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
  93. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
  94. package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
  95. package/templates/apps/api/src/openapi/spec.ts +543 -2
  96. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
  97. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
  98. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
  99. package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
  100. package/templates/apps/api/src/routes/auth/user.ts +61 -0
  101. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
  102. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
  103. package/templates/apps/api/src/routes/checkout/index.ts +34 -0
  104. package/templates/apps/api/src/routes/checkout/session.ts +139 -0
  105. package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
  106. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
  107. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
  108. package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
  109. package/templates/apps/api/src/routes/content/get-page.ts +94 -12
  110. package/templates/apps/api/src/routes/content/index.ts +13 -73
  111. package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
  112. package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
  113. package/templates/apps/api/src/routes/content/search.ts +129 -0
  114. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
  115. package/templates/apps/api/src/routes/leads/index.ts +100 -13
  116. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
  117. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
  118. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
  119. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
  120. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
  121. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
  122. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
  123. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
  124. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
  125. package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
  126. package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
  127. package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
  128. package/templates/apps/api/src/routes/requests/index.ts +54 -0
  129. package/templates/apps/api/src/routes/webhooks/README.md +265 -0
  130. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
  131. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
  132. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
  133. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
  134. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
  135. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
  136. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
  137. package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
  138. package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
  139. package/templates/apps/api/src/server.ts +439 -49
  140. package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
  141. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
  142. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
  143. package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
  144. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
  145. package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
  146. package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
  147. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
  148. package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
  149. package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
  150. package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
  151. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
  152. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
  153. package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
  154. package/templates/apps/api/src/services/abandon-email.ts +8 -2
  155. package/templates/apps/api/src/services/aeo-score.ts +49 -12
  156. package/templates/apps/api/src/services/audit-log.ts +668 -0
  157. package/templates/apps/api/src/services/consent.ts +18 -0
  158. package/templates/apps/api/src/services/default-tenant.ts +70 -31
  159. package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
  160. package/templates/apps/api/src/services/dunning-policy.ts +294 -0
  161. package/templates/apps/api/src/services/entitlements.ts +80 -0
  162. package/templates/apps/api/src/services/lead-promotion.ts +13 -14
  163. package/templates/apps/api/src/services/lead-routing.ts +161 -2
  164. package/templates/apps/api/src/services/leads-inbox.ts +23 -2
  165. package/templates/apps/api/src/services/listing-config.ts +27 -9
  166. package/templates/apps/api/src/services/project-listings.ts +201 -6
  167. package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
  168. package/templates/apps/api/src/services/request-confirm.ts +646 -0
  169. package/templates/apps/api/src/services/revalidation.ts +173 -0
  170. package/templates/apps/api/src/services/verified-email.ts +71 -0
  171. package/templates/apps/api/src/vercel-handler.ts +75 -35
  172. package/templates/apps/mcp/.env.example +17 -0
  173. package/templates/apps/mcp/CLAUDE.md +57 -24
  174. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
  175. package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
  176. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
  177. package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
  178. package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
  179. package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
  180. package/templates/apps/mcp/__tests__/index.test.ts +39 -0
  181. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
  182. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
  183. package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
  184. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
  185. package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
  186. package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
  187. package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
  188. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
  189. package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
  190. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
  191. package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
  192. package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
  193. package/templates/apps/mcp/__tests__/server.test.ts +67 -14
  194. package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
  195. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
  196. package/templates/apps/mcp/jest.config.cjs +13 -0
  197. package/templates/apps/mcp/package.json +2 -0
  198. package/templates/apps/mcp/src/abuse/index.ts +159 -24
  199. package/templates/apps/mcp/src/config/modules.ts +10 -5
  200. package/templates/apps/mcp/src/http-transport.ts +102 -29
  201. package/templates/apps/mcp/src/index.ts +73 -11
  202. package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
  203. package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
  204. package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
  205. package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
  206. package/templates/apps/mcp/src/lib/prisma.ts +39 -0
  207. package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
  208. package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
  209. package/templates/apps/mcp/src/server.ts +59 -35
  210. package/templates/apps/mcp/src/tools/audit.ts +26 -2
  211. package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
  212. package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
  213. package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
  214. package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
  215. package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
  216. package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
  217. package/templates/apps/mcp/src/tools/index.ts +6 -4
  218. package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
  219. package/templates/apps/mcp/src/tools/purchase.ts +128 -45
  220. package/templates/apps/mcp/src/tools/read_content.ts +20 -6
  221. package/templates/apps/mcp/src/tools/search_content.ts +11 -4
  222. package/templates/apps/web/.env.example +71 -5
  223. package/templates/apps/web/CLAUDE.md +44 -0
  224. package/templates/apps/web/PRODUCT.md +86 -0
  225. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
  226. package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
  227. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
  228. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
  229. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
  230. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
  231. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
  232. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
  233. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
  234. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
  235. package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
  236. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
  237. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
  238. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
  239. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
  240. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
  241. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
  242. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
  243. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
  244. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
  245. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
  246. package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
  247. package/templates/apps/web/app/[locale]/layout.tsx +133 -87
  248. package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
  249. package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
  250. package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
  251. package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
  252. package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
  253. package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
  254. package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
  255. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
  256. package/templates/apps/web/app/api/revalidate/route.ts +194 -0
  257. package/templates/apps/web/auth.ts +407 -8
  258. package/templates/apps/web/jest.config.cjs +8 -4
  259. package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
  260. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
  261. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
  262. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  263. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
  264. package/templates/apps/web/lib/auth-email.ts +318 -0
  265. package/templates/apps/web/lib/auth-env.ts +85 -0
  266. package/templates/apps/web/lib/auth-routes.ts +55 -0
  267. package/templates/apps/web/lib/meeting-slugs.ts +53 -0
  268. package/templates/apps/web/lib/site-brand.tsx +4 -8
  269. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  270. package/templates/apps/web/messages/en.json +187 -0
  271. package/templates/apps/web/middleware.ts +1 -1
  272. package/templates/apps/web/package.json +2 -1
  273. package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
  274. package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
  275. package/templates/content/_site.mdx +18 -3
  276. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
  277. package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
  278. package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
  279. package/templates/database/CHANGELOG.md +149 -0
  280. package/templates/database/README.md +10 -3
  281. package/templates/database/__tests__/appointment.db.test.ts +106 -0
  282. package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
  283. package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
  284. package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
  285. package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
  286. package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
  287. package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
  288. package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
  289. package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
  290. package/templates/database/__tests__/tenancy.test.ts +141 -0
  291. package/templates/database/config/schema.prisma +79 -2
  292. package/templates/database/content/schema.prisma +87 -11
  293. package/templates/database/inbox/schema.prisma +452 -9
  294. package/templates/database/jest.config.cjs +5 -3
  295. package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
  296. package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
  297. package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
  298. package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
  299. package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
  300. package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
  301. package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
  302. package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
  303. package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
  304. package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
  305. package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
  306. package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
  307. package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
  308. package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
  309. package/templates/database/package.json +3 -2
  310. package/templates/database/tenancy.ts +86 -1
  311. package/templates/database/tsconfig.json +1 -0
  312. package/versions.json +34 -34
  313. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
  314. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
  315. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
  316. package/templates/apps/api/src/routes/content/create.ts +0 -115
  317. package/templates/apps/api/src/routes/content/delete.ts +0 -82
  318. package/templates/apps/api/src/routes/content/update.ts +0 -112
  319. package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
  320. package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
  321. package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
  322. package/templates/apps/mcp/src/tools/create_page.ts +0 -96
  323. package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
  324. package/templates/apps/mcp/src/tools/update_page.ts +0 -92
  325. package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
  326. package/templates/apps/web/public/logo-dark.svg +0 -21
  327. package/templates/apps/web/public/logo-light.svg +0 -21
  328. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
  329. /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
@@ -0,0 +1,72 @@
1
+ ---
2
+ type: "ServicesList"
3
+ slug: "services"
4
+ title: "Services"
5
+ description: "The three jobs {your company} does, each with its own price. Placeholder copy — replace before you launch."
6
+ sections:
7
+ [
8
+ {
9
+ "id": "hero",
10
+ "type": "hero",
11
+ "data":
12
+ {
13
+ "variant": "centered",
14
+ "eyebrow": "Services",
15
+ "headline": "Three jobs, three prices",
16
+ "subheadline": "{One line on how to choose between them. Name the question a reader is actually asking.}",
17
+ },
18
+ },
19
+ {
20
+ "id": "jobs",
21
+ "type": "items",
22
+ "data":
23
+ {
24
+ "headline": "What we do",
25
+ "columns": 3,
26
+ "items":
27
+ [
28
+ {
29
+ "title": "{Lawn maintenance}",
30
+ "description": "{One line on the recurring visit — what it includes and how often it runs.}",
31
+ "link":
32
+ {
33
+ "label": "Read more",
34
+ "href": "/services/lawn-maintenance",
35
+ },
36
+ },
37
+ {
38
+ "title": "{Seasonal cleanup}",
39
+ "description": "{One line on the spring and autumn clear-down.}",
40
+ "link":
41
+ {
42
+ "label": "Read more",
43
+ "href": "/services/seasonal-cleanup",
44
+ },
45
+ },
46
+ {
47
+ "title": "{Landscape design}",
48
+ "description": "{One line on the design-and-plant job, and what a customer ends up with.}",
49
+ "link":
50
+ {
51
+ "label": "Read more",
52
+ "href": "/services/landscape-design",
53
+ },
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ {
59
+ "id": "closing",
60
+ "type": "cta",
61
+ "data":
62
+ {
63
+ "variant": "minimal",
64
+ "headline": "Not sure which one?",
65
+ "description": "{One line inviting the reader to book the visit instead of guessing.}",
66
+ "primaryCta": { "label": "Book a visit →", "href": "/meet" },
67
+ },
68
+ },
69
+ ]
70
+ ---
71
+
72
+ Placeholder copy. The three detail pages under `content/en/Service/` carry the long form and the price; this page is the chooser.
@@ -0,0 +1,4 @@
1
+ {
2
+ "template": "simple-service",
3
+ "requires": ["marketing", "scheduling", "commerce"]
4
+ }
@@ -23,13 +23,20 @@ STEALTH=true
23
23
  # hide the Blog link. Set BLOG_ENABLED=true and redeploy to launch the blog.
24
24
  # BLOG_ENABLED=true
25
25
 
26
- # ── Content backend ──────────────────────────────────────────────────────────
27
- # Default: mdx — reads from /content/*.mdx at the repo root.
28
- # Set to "pro-cms" to use the paid Pro CMS adapter (separate package, not
29
- # bundled with the OSS template).
30
- CONTENT_BACKEND=mdx
26
+ # ── Content adapter ──────────────────────────────────────────────────────────
27
+ # Which content adapter every consumer resolves: mdx (default, reads
28
+ # /content/*.mdx at the repo root) | projection (compiled manifest) | database
29
+ # (the Artifact store — needs a workspaceId).
30
+ CONTENT_ADAPTER=mdx
31
+ # CONTENT_BACKEND is deprecated — read for one release, then removed.
31
32
  # Override the content directory (default: <repo-root>/content). Useful for tests.
32
33
  # AILK_CONTENT_ROOT=
34
+ # Publish revalidation (#5851) — where apps/api POSTs on publish/unpublish so
35
+ # the site purges the affected tag, and the HMAC-SHA256 key it signs the body
36
+ # with. Required only under CONTENT_ADAPTER=database; with the URL unset the
37
+ # webhook is a logged no-op.
38
+ # CONTENT_REVALIDATE_URL=https://example.com/api/revalidate
39
+ # CONTENT_REVALIDATE_SECRET=
33
40
 
34
41
  # ── Database (optional in standalone mode) ───────────────────────────────────
35
42
  # DATABASE_URL=postgres://user:pass@host:5432/db
@@ -51,8 +58,9 @@ CONTENT_BACKEND=mdx
51
58
  # BETTER_AUTH_SECRET=
52
59
 
53
60
  # ── Single-tenant operator pin (apps/api) ────────────────────────────────────
54
- # REQUIRED on a build WITHOUT the multi-tenant module if you want the
55
- # submissions review dashboard. OPTIONAL with the module.
61
+ # REQUIRED on a build WITHOUT the multi-tenant module — for the submissions
62
+ # review dashboard AND for the `workspace` module's settings surface.
63
+ # OPTIONAL with the multi-tenant module.
56
64
  #
57
65
  # On a single-tenant deployment (the default Workspace exists and is the ONLY
58
66
  # Workspace) the FIRST authenticated user to resolve a tenant context is
@@ -68,6 +76,15 @@ CONTENT_BACKEND=mdx
68
76
  # whoever authenticates first — on a site with public sign-ups that is not
69
77
  # necessarily you. The pinned account's email must also be VERIFIED.
70
78
  #
79
+ # The same MANDATORY-and-fail-closed rule covers a `--modules workspace`
80
+ # deployment (#5371, #5515 item 1). The Pro workspace module registers
81
+ # /v1/workspaces/:workspaceId/{theme,identity,seo,general,members} behind the
82
+ # same guard, so on a workspace-only build the module's Admin pages (/admin)
83
+ # answer 403 to everyone until this is set — and, because the guard refuses
84
+ # BEFORE resolving, the first account to load the settings page cannot bind
85
+ # ITSELF as the sole owner off a plain GET. Set this before you hand the site
86
+ # to its owner.
87
+ #
71
88
  # If your own dashboard returns 403, this variable is the first thing to
72
89
  # check. With the multi-tenant module present, resolution comes from the
73
90
  # session's active workspace and this stays optional, exactly as before.
@@ -123,6 +140,45 @@ CONTENT_BACKEND=mdx
123
140
  # default.
124
141
  # RESEND_AUDIENCE_ID=
125
142
  #
143
+ # Request-and-confirm (#5970) — the callback request a site owner confirms by
144
+ # an emailed link, which mints a Stripe Payment Link for the customer to pay
145
+ # on. Off by default: with the flag unset the callback route behaves exactly as
146
+ # it always has.
147
+ #
148
+ # Set to "true" to turn it on. Anything else (including absent) is off, and TO,
149
+ # FROM and AILK_PUBLIC_API_URL must ALSO resolve or the path stays off — a flag
150
+ # with no recipient would write a credential nobody is sent.
151
+ #
152
+ # SINGLE-TENANT ONLY. On a build carrying the multi-tenant module this flag is
153
+ # ignored and the callback route behaves exactly as it does with the feature
154
+ # off. Every value here is process-wide, as is STRIPE_SECRET_KEY, so on a
155
+ # multi-tenant deployment one tenant's customer would pay into the
156
+ # deployment's own Stripe account. Lighting it up there means resolving these
157
+ # per site, which is a separate piece of work.
158
+ # AILK_REQUEST_CONFIRM_ENABLED=
159
+ #
160
+ # Where the confirm link is sent — the site owner's own address. Optional:
161
+ # falls back to AILK_LEAD_NOTIFICATION_TO when unset. This message SUPERSEDES
162
+ # the generic lead notification for a callback, so the owner still receives
163
+ # exactly one email; it just carries more (the request's details, plus the
164
+ # link).
165
+ # AILK_REQUEST_CONFIRM_TO=
166
+ #
167
+ # The verified sender for both the owner's confirm email and the customer's
168
+ # payment-link email. Optional — falls back to AILK_LEAD_NOTIFICATION_FROM.
169
+ # AILK_REQUEST_CONFIRM_FROM=
170
+ #
171
+ # The confirm form's default currency, lowercase ISO-4217. Optional; defaults
172
+ # to usd.
173
+ # AILK_REQUEST_CONFIRM_CURRENCY=
174
+ #
175
+ # The most the confirm form will price a job at, in MINOR units (cents).
176
+ # Optional; defaults to 1000000 — ten thousand dollars at two decimal places.
177
+ # High enough not to obstruct a real job, low enough that a typo with three
178
+ # extra zeroes is refused rather than charged. A value that does not parse as a
179
+ # positive integer falls back to the default rather than becoming unbounded.
180
+ # AILK_REQUEST_CONFIRM_MAX_MINOR=
181
+ #
126
182
  # The public base URL this API is reachable at, e.g. https://api.example.com
127
183
  # (no trailing slash). This is the API's OWN origin — distinct from
128
184
  # FRONTEND_URL (the site the browser loads) and from `_site.mdx`'s `baseUrl`
@@ -137,6 +193,11 @@ CONTENT_BACKEND=mdx
137
193
  # 2. The OpenAPI `servers` block (#5306). Unset degrades to a single
138
194
  # document-relative server (`/`), which an agent resolves against the
139
195
  # origin it fetched the spec from — correct, just less explicit.
196
+ # 3. The request-and-confirm link (#5970):
197
+ # `${AILK_PUBLIC_API_URL}/v1/requests/:id/confirm?token=…`. The confirm
198
+ # route is on this API, so this is the right base and no second
199
+ # base-URL variable exists. Unset means the confirm email is skipped
200
+ # (logged, with the row still written and the 201 unchanged).
140
201
  # AILK_PUBLIC_API_URL=
141
202
  #
142
203
  # The apps/api origin @working-theory/api-client calls from
@@ -313,6 +374,10 @@ CONTENT_BACKEND=mdx
313
374
  # booking page. Full notes in apps/api/.env.example.
314
375
  # CAL_API_KEY=cal_live_xxx
315
376
  # CAL_API_BASE_URL=https://api.cal.com
377
+ # HMAC-SHA256 key POST /v1/webhooks/calcom verifies the x-cal-signature-256
378
+ # header against (#6085). Unset makes the receiver answer 503 and accept
379
+ # nothing — the same fail-closed posture an unset STRIPE_WEBHOOK_SECRET has.
380
+ # CAL_WEBHOOK_SECRET=
316
381
  # SCHEDULING_PUBLIC_BASE_URL=https://cal.com
317
382
  # SCHEDULING_PROVIDER=cal
318
383
  #
@@ -40,7 +40,7 @@ Where to find the rule, surface, or fact you need.
40
40
 
41
41
  | For… | Look at |
42
42
  | ---------------------------------------------- | -------------------------------------------------------------- |
43
- | Per-issue workflow (pick → branch → TDD → PR) | `.claude/rules/loop-contract.md` |
43
+ | Per-issue workflow (pick → branch → TDD → PR) | § Workflow, below |
44
44
  | Architecture boundaries (the why + escalation) | `.claude/rules/architecture.md` |
45
45
  | Diff hygiene when editing existing files | `.claude/rules/preserve-existing-style.md` |
46
46
  | Skills available to invoke | `.claude/skills/README.md` |
@@ -56,8 +56,7 @@ when you find it. Future-you will thank present-you.
56
56
 
57
57
  ## Workflow
58
58
 
59
- The single-developer flow is described fully in
60
- `.claude/rules/loop-contract.md`. The short version:
59
+ The single-developer flow, in full:
61
60
 
62
61
  1. Pick or open an issue with a `## Acceptance Criteria` section.
63
62
  2. Branch from your default branch (typically `main`).
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: AI Launch Kit base scaffold
3
+ description: The foundation every AI Launch Kit site starts from — pure white, black ink, black hairline rules, system type, generous pacing.
4
+ colors:
5
+ bg: "#ffffff"
6
+ fg: "#000000"
7
+ surface: "#ffffff"
8
+ surface-secondary: "#ffffff"
9
+ border: "#000000"
10
+ text-muted: "#333333"
11
+ brand: "#000000"
12
+ fg-on-brand: "#ffffff"
13
+ accent: "#000000"
14
+ bg-dark: "#000000"
15
+ fg-dark: "#ffffff"
16
+ border-dark: "#ffffff"
17
+ brand-dark: "#ffffff"
18
+ typography:
19
+ display:
20
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
21
+ fontSize: "6rem"
22
+ fontWeight: 400
23
+ lineHeight: 1
24
+ letterSpacing: "-0.05em"
25
+ heading-section:
26
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
27
+ fontSize: "2.25rem"
28
+ fontWeight: 400
29
+ lineHeight: 1.25
30
+ letterSpacing: "-0.05em"
31
+ heading-item:
32
+ fontSize: "1.25rem"
33
+ fontWeight: 600
34
+ lineHeight: 1.375
35
+ body:
36
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, sans-serif"
37
+ fontSize: "1.125rem"
38
+ fontWeight: 400
39
+ lineHeight: 1.5
40
+ eyebrow:
41
+ fontSize: "0.875rem"
42
+ fontWeight: 600
43
+ lineHeight: 1.4
44
+ letterSpacing: "0.05em"
45
+ rounded:
46
+ button: "0.25rem"
47
+ card: "0.75rem"
48
+ spacing:
49
+ section: "4rem"
50
+ card: "1.5rem"
51
+ grid: "1.5rem"
52
+ container: "1280px"
53
+ ---
54
+
55
+ <!--
56
+ Source template: _base (the AI Launch Kit base scaffold)
57
+ Version: create-ailk {{CREATE_AILK_VERSION}}
58
+
59
+ This file describes the visual system this scaffold ships with. The impeccable
60
+ design skill reads it at the start of every design session, so an agent
61
+ building a new page stays on-system.
62
+
63
+ A site template (installed with `--template <name>`) replaces this file with
64
+ its own DESIGN.md — same path, with a header naming the template it came
65
+ from. Edit whichever one your site has; the header tells you which.
66
+
67
+ The tokens above describe what the scaffold RENDERS today. Colours come from
68
+ content/_site.mdx (`theme.colors`); type, radius and spacing are platform
69
+ defaults a site cannot override yet — see "Where this is heading" below.
70
+ -->
71
+
72
+ # AI Launch Kit base scaffold
73
+
74
+ ## Overview
75
+
76
+ The foundation reads like a well-made wireframe: nothing on the page but the words, black on white, with rules where other sites use shadows and colour. It is the look of a site whose owner has not chosen a brand yet and does not pretend otherwise — every placeholder reads as a placeholder, and the structure of the argument is what the visitor sees. A brand is applied on top of it; it is not one.
77
+
78
+ Every page is composed from the AI Launch Kit component library (`@working-theory/ui`). Nothing is hand-built: a page is a list of registered sections, each with a bounded set of variants. Each section is one full-width band with a lot of air around it.
79
+
80
+ ## Colors
81
+
82
+ ### Primary
83
+
84
+ **Black** (`brand` `#000000`) fills the primary button; its label is white. The secondary button is white with a black hairline and black label.
85
+
86
+ ### Neutral
87
+
88
+ Pure white ground (`bg` `#ffffff`), pure black ink for headings (`fg` `#000000`), and near-black `#333333` for body copy — well above AA on white. **Borders are black** (`#000000`): card outlines, the rule under the navigation, the rules between sections and the rule above the footer are all 1px black lines. Cards are white, not tinted (`surface-secondary` is white too). There is no accent colour — `accent` is black.
89
+
90
+ Dark mode inverts: black ground, white ink, white rules, white primary button with black label.
91
+
92
+ ### Named Rules
93
+
94
+ - **Rules, not fills.** A container is drawn with a black hairline on the white ground. Grey fills are reserved for image placeholders.
95
+
96
+ ## Typography
97
+
98
+ System sans throughout; no web font.
99
+
100
+ ### Hierarchy
101
+
102
+ | Role | Size | Weight | Notes |
103
+ | --------------- | -------- | ------ | --------------------------------------------------------------------------- |
104
+ | Hero headline | 6rem | 400 | Tracking -0.05em, line-height 1 |
105
+ | Section heading | 2.25rem | 400 | Tracking -0.05em — light and tight, not bold |
106
+ | Item heading | 1.25rem | 600 | Card, feature and step titles |
107
+ | Body | 1.125rem | 400 | `#333333`, line-height 1.5 |
108
+ | Section label | 0.875rem | 600 | Uppercase, +0.05em tracking today — the small label above a section heading |
109
+
110
+ Measured from the rendered scaffold (1440px wide), not transcribed from tokens.
111
+
112
+ The small label above a section heading is **kept** as part of this foundation's voice (operator-disposed 2026-09-22) and is headed toward the treatment below.
113
+
114
+ ## Layout
115
+
116
+ Content sits in a 1280px container. Sections stack at the platform's 4rem rhythm today; a few sections choose a compact (2.5rem) or spacious (6rem) step internally. Card grids gap at 1.5rem. Breakpoints: 320 / 768 / 1024 / 1440.
117
+
118
+ ## Shapes
119
+
120
+ Two radii today: buttons at 0.25rem, cards and outlined boxes at 0.75rem. Headed toward square corners — see below.
121
+
122
+ ## Components
123
+
124
+ ### Buttons
125
+
126
+ Primary: black fill, white label, 0.25rem corners. Secondary: transparent with a black 1px outline and black label. Both are the library `Button`.
127
+
128
+ ### Cards / Containers
129
+
130
+ White panels with a black 1px outline, 0.75rem corners, `card` (1.5rem) padding. The one exception today: the "how it works" steps render as dark filled cards (see below).
131
+
132
+ ### Navigation
133
+
134
+ Wordmark or logo left, page links, then an outlined and a filled button at the far right. A black hairline separates it from the page. An optional announcement banner sits above it.
135
+
136
+ ## Where this is heading
137
+
138
+ The foundation's pinned direction is the pacing of a classic wireframe marketing kit — `standardissue.webflow.io` is the reference for rhythm, type scale, spacing density and section pacing only; no image, copy or code is taken from it. Six parts of that direction cannot be expressed through a site's theme today, so they wait on a platform change rather than being hand-coded. Until they land, this file describes the rendered site above, not these targets.
139
+
140
+ | Target | Reference value | Blocked by |
141
+ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
142
+ | Square corners on buttons, cards and inputs | radius 0 | A site's `theme:` block accepts colours only — no radius |
143
+ | Bold, tighter headings | hero ~3.6rem and section headings ~2.9rem, both at weight 700 with tracking about -0.02em (today 6rem and 2.25rem at weight 400) | No per-site type-role override |
144
+ | Generous section pacing | about 10rem between sections, not 4rem | No per-site section-spacing override; `spacious` (6rem) is not author-selectable |
145
+ | Section label in sentence case, bold, no tracking | "Core Features", not "CORE FEATURES" | The eyebrow role bakes in uppercase and tracking |
146
+ | Whitespace between sections, not rules | no rule between bands | Section dividers share the one `border` colour with card outlines, so black outlines make black dividers |
147
+ | Outlined steps in "how it works" | three plain outlined columns | The steps section renders dark filled cards; no outlined variant |
148
+
149
+ ## Do's and Don'ts
150
+
151
+ - **Do** keep every page built from registered sections. If a section you need does not exist, request it rather than writing one-off markup.
152
+ - **Do** change colours in `content/_site.mdx` `theme.colors`; they reach every section.
153
+ - **Don't** add a colour accent. The foundation's one idea is black on white; a brand pass adds colour deliberately, through the theme.
154
+ - **Don't** replace a black rule with a shadow or a grey fill.
155
+ - **Don't** hard-code the "heading toward" values above into a page. They arrive through the platform, or not at all.
@@ -29,7 +29,7 @@ pnpm preflight # architecture + types + lint + build
29
29
  pnpm build # production build
30
30
  ```
31
31
 
32
- > Requires Node.js 20+ and `pnpm`. No vendor API keys are required for first boot — Stripe, Anthropic, and email-provider integrations are opt-in.
32
+ > Requires Node.js 20+ and `pnpm` — that is the whole prerequisite list. No database is needed to run or build this site (see [When you need a database](#when-you-need-a-database)), and no vendor API keys are required for first boot — Stripe, Anthropic, and email-provider integrations are opt-in. `pnpm install` generates the Prisma client for you; there is no generator step to remember.
33
33
 
34
34
  ## Content
35
35
 
@@ -63,16 +63,50 @@ Apply `myTheme` via the provider you use to wrap children. The full extension su
63
63
 
64
64
  Copy `.env.example` to `.env` and fill in only what you need. Most variables are optional. A complete reference:
65
65
 
66
- | Variable | Purpose |
67
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
68
- | `AILK_LICENSE_KEY` | Optional. JWT issued by ailaunchkit.ai; verified locally to unlock Pro features. Get one at <https://ailaunchkit.ai/pro>. |
69
- | `NEXT_PUBLIC_STREAMING_AI_ENABLED` | Optional. Set to `true` to enable the opt-in streaming AI assist UI. Off by default. |
70
- | `ANTHROPIC_API_KEY` | Required only when `NEXT_PUBLIC_STREAMING_AI_ENABLED=true`. Used by the AI assist surface only. |
71
- | `AILK_LOCAL_LEAD_CAPTURE` | Optional. When `true`, leads from the contact form are written to a local file instead of an API. |
72
- | `AILK_LEADS_API_URL` | Optional. Override the upstream that receives lead-capture submissions. |
66
+ | Variable | Purpose |
67
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
68
+ | `AILK_LICENSE_KEY` | Optional. JWT issued by ailaunchkit.ai; verified locally to unlock Pro features. Get one at <https://ailaunchkit.ai/pricing>. |
69
+ | `NEXT_PUBLIC_STREAMING_AI_ENABLED` | Optional. Set to `true` to enable the opt-in streaming AI assist UI. Off by default. |
70
+ | `ANTHROPIC_API_KEY` | Required only when `NEXT_PUBLIC_STREAMING_AI_ENABLED=true`. Used by the AI assist surface only. |
71
+ | `AILK_LOCAL_LEAD_CAPTURE` | Optional. When `true`, leads from the contact form are written to a local file instead of an API. |
72
+ | `AILK_LEADS_API_URL` | Optional. Override the upstream that receives lead-capture submissions. |
73
73
 
74
74
  See [`.env.example`](./.env.example) for the full template with comments. Per-app overrides (e.g. `apps/api/.env`) are honored alongside the root `.env`.
75
75
 
76
+ ## When you need a database
77
+
78
+ **A free marketing site needs no database at all.** `pnpm install`, `pnpm dev` and `pnpm build` all run green on an empty machine with no Postgres, no `DATABASE_URL` and no `.env` — the pages render from `content/*.mdx`, and the Prisma client is generated for you by `database`'s `postinstall` so the workspace type-checks and builds either way.
79
+
80
+ You need Postgres once you turn on one of these four:
81
+
82
+ | Surface | What it stores |
83
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
84
+ | **Auth** (sign-in, sessions, accounts) | users, sessions, accounts |
85
+ | **Flows** — `/save` and `/checkout` | a visitor's in-progress flow state, so they can resume or pay later |
86
+ | **Lead capture in DB mode** | submitted leads. The default file mode (`AILK_LOCAL_LEAD_CAPTURE=true`) writes to disk instead and needs nothing. |
87
+ | **`CONTENT_ADAPTER=database`** | pages authored in the Artifact store rather than in `content/*.mdx` |
88
+
89
+ ### One command to get one
90
+
91
+ ```bash
92
+ docker run --name ailk-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:17
93
+ ```
94
+
95
+ Point the app at it in `.env` (both variables, same URL for local work — the second is the direct connection `prisma migrate` uses):
96
+
97
+ ```bash
98
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
99
+ DATABASE_URL_NON_POOLED=postgresql://postgres:postgres@localhost:5432/postgres
100
+ ```
101
+
102
+ Then create the tables:
103
+
104
+ ```bash
105
+ pnpm db:migrate:deploy
106
+ ```
107
+
108
+ That is the whole setup. `pnpm db:studio` opens a browser UI over the data, and `pnpm db:seed` loads the sample rows. Turning the database off again is just unsetting the two variables — the marketing pages never read it.
109
+
76
110
  ## AI assist (optional)
77
111
 
78
112
  The site ships with an opt-in streaming AI assist. It is **off by default**. To turn it on, set `NEXT_PUBLIC_STREAMING_AI_ENABLED=true` in `.env` and provide an `ANTHROPIC_API_KEY`. The assist runs server-side so the API key never reaches the browser. With both unset, the chat UI is hidden and no AI traffic flows.
@@ -0,0 +1,8 @@
1
+ {
2
+ "id": "fallback-1789916251590-037e9005",
3
+ "siteId": "site_1",
4
+ "source": "callback",
5
+ "email": "",
6
+ "payload": {},
7
+ "createdAt": "2026-09-20T14:57:31.552Z"
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "fallback-1789916253842-b52659e9",
3
+ "siteId": "wksp_00000000-0000-7000-8000-000000000001",
4
+ "source": "flow:get-a-quote",
5
+ "email": "abandoner@example.com",
6
+ "payload": {
7
+ "email": "abandoner@example.com",
8
+ "flowSlug": "get-a-quote",
9
+ "locale": "en"
10
+ },
11
+ "createdAt": "2026-09-20T14:57:33.840Z"
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "fallback-1789916253845-6b266f8c",
3
+ "siteId": "default",
4
+ "source": "flow:get-a-quote",
5
+ "email": "abandoner@example.com",
6
+ "payload": {
7
+ "email": "abandoner@example.com",
8
+ "flowSlug": "get-a-quote",
9
+ "locale": "en"
10
+ },
11
+ "createdAt": "2026-09-20T14:57:33.844Z"
12
+ }
@@ -57,6 +57,19 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
57
57
  # Provider selector. Only "cal" is implemented today; the self-hosted engine
58
58
  # (#4288) lands as a second value here with no change to the section.
59
59
  # SCHEDULING_PROVIDER=cal
60
+ #
61
+ # The booking webhook receiver (#6085) — a SEPARATE credential from the
62
+ # three above: it verifies POST /v1/webhooks/calcom's x-cal-signature-256
63
+ # header, it does not call the provider. Unset makes the receiver answer 503
64
+ # and accept nothing (the same fail-closed posture STRIPE_WEBHOOK_SECRET
65
+ # unset already has) — every booking is then silently lost, so this must be
66
+ # set for the receiver to do anything.
67
+ # CAL_WEBHOOK_SECRET=
68
+ # GET /v1/schedule/appointments only returns rows where this deployment's
69
+ # AILK_SITE_ID (declared below, under "Project listings — site addressing")
70
+ # equals the caller's own workspace id: the receiver writes under
71
+ # AILK_SITE_ID, the read scopes to the tenant's workspace, and a mismatch
72
+ # fails EMPTY, never wide.
60
73
 
61
74
  # =============================================================================
62
75
  # Paid modules (D-44, #5368)
@@ -40,14 +40,24 @@ Routes are versioned under `/v1/*`:
40
40
  - `/v1/leads` (GET, `?cursor=&limit=`) — the leads INBOX read (#5678): the caller's OWN sites' leads, newest first, cursor-paginated (`limit` defaults to 50, max 200; `(createdAt desc, id desc)`; `cursor` is the previous page's last row id, and one that does not resolve within the caller's scope is a 400, never a silent page 1 — the API keeps that contract, and the web inbox deliberately softens it on its own side by re-reading page one when a URL-supplied cursor 400s, which is a rendering choice and not a change to this response). Five fields per row and nothing more — `id`, `name` (lifted from `payload.name` when the site's form captured one), `email`, `source`, `createdAt`; `payload`'s other keys, `siteId`, `promotedUserId` and `promotedAt` are never selected (`src/services/leads-inbox.ts` is the only read path). Registered in `authScope` only (auth, no tenant guard) and — alone in that scope — **unconditionally**, with no `isModuleEnabled()` gate: lead capture is a never-gate surface at every level, so reading a lead back cannot be gated either. Identity is `request.userId`; there is no site, org or workspace parameter, so the scope cannot be widened from the wire. It is resolved from the caller's own memberships by `src/services/owner-workspaces.ts`'s `resolveOwnerWorkspaces` — the ONE site→workspace resolution in this app, shared with `lead-promotion.ts` rather than written a second time — and every read filters `siteId ∈ workspaceIds`. Role floor `owner|admin` (contact PII), the same floor and the same reason as `/v1/waitlist-experiments/*`; the scope is the UNION of every workspace the caller owns or administers, not one active tenant, because this route registers outside the tenant scope. **Two deployment preconditions decide whether it returns anything, and both fail EMPTY rather than wide:** a build with no `multi-tenant` module resolves no workspaces (a deliberate scope limit since #5677, not an absence of anything to resolve), and the `siteId` the leads were WRITTEN under must equal a workspace id. That `siteId` has TWO sources, and `currentSiteId()` is not the one on the primary path: the three POST capture routes read it from the REQUEST BODY (`routes/leads/index.ts`), which the capture sections declare in content and default to `"default"` (`ContactForm`/`Newsletter`/`StepFlow`, `ext.siteId ?? "default"`), while the recommender's capture path (`routes/flows/recommender.ts`) is the one that uses `currentSiteId()` (`lib/site.ts`, `AILK_SITE_ID ?? "default"`). A workspace id is `wksp_<uuidv7>`, so `siteId ∈ workspaceIds` matches only when the content-declared value — and, for recommender leads, `AILK_SITE_ID` — is set to it (the same convention `waitlist-dashboard.ts` already relies on). The rendered `name` is lifted from `payload.name` and truncated at 200 characters: the write side bounds `payload` at 64 keys and no per-value length, and `POST /v1/leads/submit` is anonymous with no ownership check on `siteId`. No MCP twin — identity-bound (#3591 exception, the same shape as `/v1/workspaces/*` and `/v1/billing/*`).
41
41
  - `/v1/flows/:slug` (GET), `/v1/flows/:slug/steps` (POST) — serve a flow config + submit a step's answers; server-side per-step partial-state persistence (#3956). Module: `signal-collectors` (D-48, #5366) — registers only when the module is enabled; MCP twins `get_flow`/`submit_flow_step` carry the same `module` tag (D7).
42
42
  - `/v1/flows/:slug/aggregate` (GET) — anonymous, site-scoped per-question response counts across all completed respondents for the results reveal (#4771): respondents are completed `FlowSession` rows (not `Lead` rows — a recommending-mode respondent has no Lead until the email moment), question metadata + the chart flag come from the `WaitlistExperiment` register, and only `resultsReveal`-flagged `single`/`multi` questions are returned (`src/services/flow-aggregate.ts`). No individual answer, email, name, session id, or score in the response; no auth. Module: `signal-collectors`. No MCP twin — recorded exception (an agent walking a flow already gets everything it needs from `submit_flow_step`'s own response; "how everyone else answered" changes none of it).
43
- - `/v1/deliverables/:grantId` (GET) — the capability-token download route for gated PDFs and reports, including client deliverables (#3961 D5); the token itself is the sole gate, no auth preHandler applies (`src/routes/deliverables/index.ts`). Module: `signal-collectors` (D-48, #5366). No MCP twin — identity-bound capability-link download (#3591 exception, the same shape as `/v1/waitlist-signups/me`).
43
+ - `/v1/deliverables/:grantId` (GET) — the capability-token download route for gated PDFs and reports, including client deliverables (#3961 D5); the token itself is the sole gate, no auth preHandler applies (`src/routes/deliverables/index.ts`). Module: `signal-collectors` (D-48, #5366). No MCP twin — identity-bound capability-link download (#3591 exception, the same shape as `/v1/waitlist-signups/me`). **Three grant classes since #6310**, keyed on `grant.assetId`'s ADR-0031 prefix: `playbook:` (unchanged, the FEAT-074 U4b virtual asset); `asset_` — a workspace `Asset`, resolved through the named boundary `src/lib/deliverable-asset-store.ts`, scoped by `grant.workspaceId` (a serve-time tenancy re-check restored for this class — D15) — served `attachment` with an extension pinned to the stored content type, `nosniff`, a non-raster `Content-Security-Policy` (for EVERY content type, including raster — never `inline`), and `Cache-Control: private, no-store` so a deleted asset stops downloading immediately; `dasset_` — the LEGACY `DeliverableAsset` store (unchanged, tried only when a magnet's `assetKey` does not resolve against `Asset`). Every miss on the `asset_` class — a NULL `workspaceId`, no row, a foreign-workspace row — folds into the SAME uniform 404 as every other class; there is deliberately no redirect to the public `GET /v1/assets/:assetId` (that route's year-long `immutable` cache would defeat deletion as a revocation lever, and would collapse every lead's capability onto one workspace-canonical id — spec #6310 §9 SEC-001/SEC-002).
44
44
  - `/v1/flows/:slug/save` (POST), `/v1/flows/:slug/checkout` (POST) — the recommender's email moment (#4841): `save` creates the FlowSession + Lead (+ the T+0 DeliverableGrant) for the permalink's answers; `checkout` does the same, records the `FlowCheckout` buyer row at a SERVER-recomputed `price()` (the body carries no amount — `flowCheckoutInputSchema` is strict), and opens a Stripe TEST-mode Checkout Session whose metadata carries the permalink + offer key (`src/routes/flows/recommender.ts`, `src/services/recommender-capture.ts`, `packages/stripe-client`'s `createQuotedCheckoutSession`). Anonymous, rate-limited; a non-`sk_test_` key is refused before any side effect. No MCP twin — recorded exception: an agent walking a flow has no email to give and no card to pay with; the anonymous MCP surface is identity-blind.
45
+ - `/v1/workspaces/:workspaceId/magnets` (GET, POST), `/v1/workspaces/:workspaceId/magnets/:id` (GET, PATCH), `/v1/workspaces/:workspaceId/magnets/:id/signups` (GET), `/v1/workspaces/:workspaceId/magnets/:id/subscribers` (GET), `/v1/workspaces/:workspaceId/magnets/:id/messages` (GET) — the tenant-scoped magnet Admin authoring surface over the flow engine's `FlowDefinition` rows (#6157 S5c, `src/routes/magnets/index.ts`): list (cursor-paginated, fixed 50-row page), create, get, update, and the three per-magnet captured-lead reads (#6246 S6h). Reads and writes only through `tenantDb(tenant).flows` — never `prisma.flowDefinition`, `currentSiteId()`, or `upsertFlowDefinition` (the PUBLIC/seed path below, which the admin path never calls). No DELETE — the reversible alternative is `status: "draft"` (#6156), which stops a NEW walk from starting while an in-flight walk finishes on its pinned snapshot. `slug` lives only inside `config.slug` and is immutable through this surface after create (`400 slug_immutable` on a mismatched PATCH). The two operator-writable config URLs (`abandonEmail.resumeUrl`, `followUps[].cta.href`) pass an exact-origin gate against `FRONTEND_URL` before any write, the same discipline the recommender's checkout redirect already applies (#4912). Role floor `owner|admin` on all seven routes — a magnet's `config` carries `scoring` thresholds, `offers`/`rateTable` and `deliverables` asset keys, so no route exposes it at member level. Both writes declare a route-level `bodyLimit` (256 KiB) and each records one `recordAgentAction` audit row. Module: `multi-tenant || signal-collectors`, the same gate the review-surface dashboard (`/v1/waitlist-experiments/*`, `/v1/project-listings/*`) already registers under. No MCP twin on any of the seven — identity-bound tenant routes (#3591 exception, same shape as the sibling `/v1/workspaces/*` routes); the public `/v1/flows/*` routes and their two MCP tools are unchanged, pinned by `src/routes/flows/__tests__/parity.test.ts`. **The three new list routes (#6246 S6h)** join every magnet through `Lead.source = 'flow:' + <the resolved magnet's slug>` — the literal string `flow-engine.ts`'s `completeSubmission` stamps on every flow-engine `Lead` row regardless of kind, including `newsletter`/`waitlist` (`Subscriber` is written only by the separate, pre-flow-engine `subscribe_newsletter` path, unrelated to a magnet) — via ONE new `tenantDb(tenant).leads.listBySource(source, { limit })` accessor, an exact-source-equality sibling of #6204's allowlist-scoped `listUnhandledMessages`, served by a new `(workspaceId, source)` composite index on `Lead`. `/signups` expects `kind: "waitlist"`, `/subscribers` expects `"newsletter"`, `/messages` expects `"contact"`; a magnet id absent from the caller's workspace OR resolved to the WRONG kind for the called route is `404 not_found`, indistinguishably (mirrors the existing GET's absent-or-foreign fold). `/signups` and `/subscribers` serve four keys (`id`, `name`, `email`, `createdAt`); `/messages` serves those four plus `handledAt`. Bounded `?limit=` (default 50, max 200), newest-first — mirrors `workspaces/messages.ts`'s own query schema. Applications (Listicle) is deliberately OUT of this surface — `ProjectListing` carries no reverse link to the listicle-kind `FlowDefinition` that captured it, so "scoped to that magnet" cannot be honored without a schema change (split to #6314).
45
46
  - `/v1/content/{types,:type,:type/:slug}` — public content reads
46
47
  - `/v1/auth/tokens`, `/v1/user/{profile,data/:key}` — local auth + user data
47
48
  - `/v1/checkout/sessions` — start a Stripe checkout
48
49
  - `/v1/workspaces/:workspaceId/{theme,identity,seo}` (GET/PUT) — per-workspace site config (multi-tenant module)
50
+ - `/v1/workspaces/:workspaceId/meetings` (GET/PUT) — the operator's enabled-slug gate for meeting types (#6183 S9b, `src/routes/workspaces/meetings.ts`). Beside the three siblings above in URL shape and tenant-isolation posture, but registered in its OWN scope, gated on `isModuleEnabled("scheduling")` ALONE — not the `workspace`/`multi-tenant` scope its siblings share, because its GET reaches `services/scheduling/` (see below), which a scaffold without that module does not carry (`tools/lint-slice-registration.ts`, #5701). Both verbs are role-floored `owner|admin` (the same floor `general.ts` puts on a rename): PUT because this blob decides which pages the public site serves, and GET because — since it does NOT just return the stored blob — it echoes the deployment's whole provider catalog, including the rows the operator chose not to publish (security review, 2026-09-19). GET is the one segment in this file's family that does NOT just return the stored blob: it ALSO lists the scheduling provider's own bookable event types (`listEventTypes`, #6182), joined with `enabled: boolean`; an unconfigured/unreachable provider answers the same `SchedulingErrorKind` outcome `/v1/schedule/*` uses, distinct from an empty list. No MCP twin — identity-bound tenant route (#3591 exception).
51
+ - `/v1/workspaces/:workspaceId/general` (GET/PUT) — the workspace's own display name, the Pro `workspace` module's general-settings surface (#5371). Registered under `isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")` alongside the site-config routes above, behind the same tenant guard; the scope is always `request.tenant.workspaceId`, never the path param (the guard 403s a mismatch before any handler runs). PUT takes a strict `{ name }` body (1–200 chars, trimmed) and both verbs answer the same `{ name }` shape. Role floor on PUT `owner|admin` (#5515 item 2) — renaming is an org-level settings write; GET stays open to every member. No MCP twin — identity-bound tenant route (#3591 exception, same shape as `/v1/billing/*`).
52
+ - `/v1/workspaces/:workspaceId/messages` (GET, `?limit=`), `/v1/workspaces/:workspaceId/messages/:leadId` (GET, PATCH) — the Inbox message API (#6216 S7e, `src/routes/workspaces/messages.ts`): the workspace's unhandled messages newest first, the item read (resolves a message handled or not, serving `handledAt`), and the one-way idempotent handled stamp over #6204's `markMessageHandled`. Registered under `isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")`, the same scope as the two routes above. A message is a `Lead` whose `source` is in #6204's closed `MESSAGE_LEAD_SOURCE_LIST` allowlist — checked only in `tenantDb(tenant).leads`, never in this file; a non-message lead is a 404 on the item read and the PATCH and absent from the list, the SAME 404 body as an absent id, another workspace's id, or an unbackfilled `workspaceId`. **One served row across all three routes** — `id`, `name`, `email`, `source`, `createdAt`, `handledAt` — and nothing else: no `payload`, no message body (operator ruling, 2026-09-19; `leads-inbox.ts`'s PII boundary is unchanged, not widened). PATCH takes `{ "handled": true }` only, `.strict()`; there is no un-handle path (#6204 D4), so `{ "handled": false }` is a 400. The stamp records the actor via `tenant.userId` in `Lead.handledBy` — written, not served. Role floor `owner|admin` (`LEAD_INBOX_ROLES`, the SAME constant `services/inbox-messages.ts`'s sibling functions already enforce), checked before any read or write — **and on a single-workspace deployment RUNNING the `multi-tenant` module with `AILK_OPERATOR_EMAIL` unset, that role is self-servable first-come** (the SAME `bindOperatorMembership` TOFU bind the `AILK_OPERATOR_EMAIL` paragraph below names, reached there via `multi-tenant`'s own null-binding fallback to `resolveDefaultTenant` — NOT via the base `workspace`-only guard, which REQUIRES the pin and fails closed when it is unset, so TOFU is unreachable on a bare `workspace`-only build: these are the first routes to serve anonymous visitors' contact PII through that grant). No MCP twin — identity-bound tenant routes (#3591 exception, the same shape as `/v1/waitlist-experiments/*` and `/v1/project-listings/index.ts`): MCP carries no caller identity today, so a twin could only invent an MCP auth story or serve whatever workspace the environment points at; the deferred ambition is #6222.
53
+ - `/v1/workspaces/:workspaceId/members` (GET) — the org's member roster (`userId`, `name`, `email`, `role`), the Pro `workspace` module's Team tab (#5371). Read-only by design: granting a Membership is the invite flow, which is its own spec with a mandatory security review (§2.3), so no POST/PUT/DELETE is routed at this path. Scoped to `request.tenant.organizationId` — Membership is org-level — server-resolved from the validated session. Role floor `owner|admin` (#5515 item 2), the same floor and the same reason as `/v1/waitlist-experiments/*` and `/v1/leads`: the roster carries every member's email, so membership alone is not authorization. No MCP twin — identity-bound tenant route (#3591 exception).
54
+ - `/v1/workspaces/:workspaceId/content` (GET, POST), `/v1/workspaces/:workspaceId/content/:type/:slug` (GET, PATCH), `/v1/workspaces/:workspaceId/content/:type/:slug/{publish,unpublish}` (POST) — the tenant-guarded content editor's write surface (#5853, `src/routes/workspaces/content.ts`): list, create, read/save the draft tip (under `If-Match`), and publish/unpublish, all over `Artifact`/`ArtifactVersion`. `:type` is validated against the page-type registry (`z.enum(SCHEMA_ORG_TYPES)`) rather than accepted as a free string on every addressed operation — an unregistered type is a 400 before any lookup, since it is echoed back and passed to `notifyRevalidate` as the site's cache tag. Role floor: publish and unpublish are `owner|admin` only (#5922, operator decision 2026-09-17) — publishing to, or pulling down from, a live public site is the highest-consequence action in the family, so it carries the same floor as this file's rename and roster routes above; list, create, read and save stay member-level. **The GET list is a UNION of page artifacts and DOCUMENT assets** (#6093, D1/D9 — "one store, two views"): every row carries `tags: string[]` (never null, read from the draft tip's stored content for a page, from `Asset.tags` for a file) and a DERIVED `kind: "page" | "post" | "file"` (`BlogPosting` alone maps to `"post"`; an asset row is always `"file"`) — an optional `?tag=<slug>` filters the union AFTER it is built, so a file is returned under either its own tag or a shared one. The asset half of the union (`DOCUMENT_CONTENT_TYPES`, i.e. `application/pdf`) is skipped entirely when `locale`/`type` is supplied or `status` names anything but `"published"`, since a file row can never satisfy those. A foreign `:workspaceId` costs zero artifact **and** zero asset queries. No MCP twin — identity-bound tenant routes (#3591 exception, same shape as the sibling `/v1/workspaces/*` routes).
55
+ - `/v1/workspaces/:workspaceId/assets` (GET, POST), `/v1/workspaces/:workspaceId/assets/:assetId` (PATCH, DELETE), `/v1/assets/:assetId` (GET, public/anonymous) — the workspace asset store (#5854 upload/read; #6093 list/PATCH/DELETE, `src/routes/assets/{index,upload,read,manage}.ts`). Upload accepts an allowlisted union of three named classes (`src/routes/assets/index.ts`): five RASTER images (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/avif`), one VECTOR image (`image/svg+xml`, admitted under the operator's 2026-09-18 `allow-svg-assets` ruling — issue #6093 comment `5731573595` — superseding #5854's original SVG exclusion) and one DOCUMENT type (`application/pdf`), all under the existing 20 MB cap; `?tags=<slug>[,<slug>…]` stores tags on the row. The public read serves a RASTER type `inline` exactly as before; every OTHER admitted type (SVG, PDF) is served `attachment`, with the filename's extension pinned to the stored content type (closing extension spoofing) and a five-directive `Content-Security-Policy` (`default-src 'none'; sandbox; frame-ancestors 'none'; form-action 'none'; base-uri 'none'`) that makes the bytes never a document on this origin — this is what makes admitting SVG safe with NO server-side sanitizer. `GET …/assets` (member-level — the same floor the union content list above already carries for the very same rows, D9) lists a workspace's assets, metadata only, never `bytes`, with an optional `?tag=<slug>` filter; this is what narrows the store's prior "nothing lists assets" premise to its precise form — no ANONYMOUS and no CROSS-TENANT enumeration, both still true. `PATCH …/assets/:assetId` updates `title`/`description`/`tags` (`owner|admin`; `null` clears a text field to SQL `NULL`, `""` stores empty; the asset's `id` and public URL are untouched). `DELETE …/assets/:assetId` (`owner|admin`) removes the row and the bytes at the ORIGIN only — already-cached copies elsewhere are not recallable under the year-long `immutable` promise, and no reference check protects a page that still embeds the URL; absent and foreign ids both take the same 404. Every tenant route here is identity-bound; no MCP twin (#3591 exception). The public read has no MCP twin either — it serves bytes to an `<img>`/download, the same boundary `upload.ts` records for its own sibling.
49
56
  - `/v1/billing` (GET), `/v1/billing/portal-session` (POST) — org-scoped billing read + Stripe customer-portal session creation
50
57
  - `/v1/schedule/availability` (GET), `/v1/schedule/event-meta` (GET), `/v1/schedule/book` (POST) — anonymous scheduling proxy behind the provider seam (`src/services/scheduling/`); Cal.com v2 today, a self-hosted engine later, same three calls. `event-meta` is the meta-only read (title · duration · description · price) so rendering an event's facts never fetches an availability window (#4633). `CAL_API_KEY` is read server-side only. MCP twins: `list_availability` / `get_event_meta` / `create_booking` (#4416, #4633)
58
+ - `/v1/schedule/meeting-types` (GET) — the enabled-meeting-slugs public read (#6183 S9b, `src/routes/schedule/meeting-types.ts`): the session-less read the public `/meet` surface needs to honour the operator's enabled-slug gate (`/v1/workspaces/:workspaceId/meetings`, above), since a public page holds no session. Resolves its workspace via `resolvePublicWorkspace()` (the same fail-closed predicate the public content read uses — no client-suppliable workspace, ever) and reads through a new named public-read boundary, `src/lib/public-meetings-config.ts` (modelled on `lib/asset-store.ts`): one `findUnique` by the resolved `workspaceId`, no write, no list. `{ slugs: string[] | null }` — `null` means "no stored list" (an unconfigured deployment, or the non-database adapter); the caller (`apps/web`'s `enabledMeetingSlugs()`) applies the `SCHEDULING_CAL_LINK`-derived singleton fallback for that state, since that env var is `apps/web`'s, not this app's. Reads no client input. No MCP twin — deferred to a named follow-up rather than silently skipped (the route's own header, §10 Q3 of the spec).
59
+ - `POST /v1/webhooks/calcom` — the scheduling booking-event receiver (#6085, brief H7): verifies the provider's HMAC-SHA256 signature (`x-cal-signature-256`) over the raw body, then normalizes one of six `BOOKING_*` trigger events into an `Appointment` row (`src/services/scheduling/appointments.ts`). `CAL_WEBHOOK_SECRET` is read server-side only (`config/index.ts`); an unset secret answers `503`, never a silent accept. Registered on the same `webhooksScope` as `/v1/webhooks/stripe` (its raw-body content-type parser is what a signed receiver needs), module-conditional (`scheduling`, `src/routes/webhooks/index.ts`). No commerce-claim read: a booking event is not a money event (D-67 puts the payment on the provider's own side). No MCP twin — an inbound receiver for one named provider, not an endpoint an agent could reasonably call, the same footing `POST /v1/webhooks/stripe` already takes.
60
+ - `/v1/schedule/appointments` (GET, `?limit=&cursor=&window=`) — the SITE's own appointments (#6085), tenant-guarded and role-floored `owner|admin` (the rows carry attendee contact PII, the same floor `/v1/leads` and `/v1/waitlist-experiments/*` carry). Registered in its OWN tenant scope, gated on `isModuleEnabled("scheduling")` alone, behind `tenantPreHandlerForBuild()` — NOT the anonymous schedule scope above (a prospect booking a call has no account). Scope is ALWAYS `request.tenant.workspaceId`, NEVER `currentSiteId()` (the receiver's own write scope) — and deliberately carries NO platform-operator escape hatch, unlike `/v1/waitlist-experiments/*`: an appointment book is a seller's customer list. `window` defaults to `upcoming`; `cursor` is the previous page's last row id, and one that does not resolve within the caller's scope is a `400`, never a silent page 1 (the `/v1/waitlist-experiments/*/signups` cursor discipline). Eight fields per row and nothing more — `id`, `startTime`, `endTime`, `title`, `attendeeName`, `attendeeEmail`, `status`, `paid`; `siteId`, `providerBookingUid`, `provider`, the three watermark columns, `paymentInitiatedAt`, `noShow`, `cancelledAt`, `createdAt` and `updatedAt` are never selected (`src/services/scheduling/appointments.ts`'s `listAppointments` is the only read path). No MCP twin — identity-bound tenant route (#3591 exception, the same shape as `/v1/waitlist-experiments/*`).
51
61
  - `/v1/aeo/score` (POST), `/v1/aeo/score/example` (GET) — anonymous instant AEO score for a respondent-supplied URL, and the bundled pre-launch example, both fed by the SSRF-guarded crawler at `@working-theory/aeo/fetch` and the pure scorer at `@working-theory/aeo` (`src/services/aeo-score.ts`, #4666). No MCP twin — recorded exception, brief D7 (exposing an arbitrary-URL crawler to anonymous agents is a security-posture decision; `apps/mcp/src/tools/audit.ts` deliberately stays same-origin-locked instead).
52
62
  - `/v1/waitlist-experiments` (GET), `/v1/waitlist-experiments/:experimentId` (GET), `/v1/waitlist-experiments/:experimentId/signups` (GET), `/v1/waitlist-experiments/:experimentId/signups/:leadId` (GET) — the operator-dashboard read API for Parallel Waitlist Validation (#4665 D7, B0): tenant-scoped list + rollup, per-waitlist comparison, paginated signups, and a signup drill-down over A1's `WaitlistExperiment` / `WaitlistScore` / `Lead` rows (`src/services/waitlist-dashboard.ts`, #4671). Role floor `owner|admin` (signup PII); the platform-operator scope (`isSentinel(organizationId) && role === "owner"`, ALSO requiring `AILK_OPERATOR_EMAIL` to be set — fail-closed, PR #4687) sees every site, an ordinary owner/admin sees only its own `siteId`. No MCP twin — identity-bound tenant routes (#3591 exception, same shape as `/v1/billing/*`).
53
63
  - `/v1/waitlist-signups/me` (GET, `?experimentKey=<slug>`) — the respondent read (#4731): a signed-in, **email-verified** user's OWN newest signup for one experiment (`experimentKey`, `waitlistKey`, `completedAt`, `tier`, `score`, `aeoScore`, `answers.q3` — seven fields, nothing more; `src/services/waitlist-signups.ts` is the only read path). Registered in `authScope` only (auth, no tenant guard — a respondent has no workspace). Identity is `request.userId`; the email is the `User`'s, never a parameter; `emailVerified !== true` → 403 `email_unverified` before any `Lead` read; no match → 404 `signup_not_found`; every read scoped to `currentSiteId()`. Module: `signal-collectors` (D-48, #5366). No MCP twin — identity-bound (#3591 exception).
@@ -56,6 +66,8 @@ Routes are versioned under `/v1/*`:
56
66
  - `/v1/project-listings/{start,submit}` (POST), `/v1/project-listings/:id` (PATCH, draft), `/v1/project-listings/public` (GET) — the anonymous founder's-application lifecycle (#4998, reshaped by #5036): draft start/patch/submit plus the public shelves read, identity-blind and rate-limited alongside the leads routes. Module: `signal-collectors`. No MCP twin — anonymous form-lifecycle routes, not agent-facing.
57
67
  - `/v1/slack/actions` (POST) — the project-listing operator decision (Slack Approve/Reject, #4999): verifies the Slack signature, applies the decision via `src/services/project-listing-decision.ts`. Module: `signal-collectors`. No MCP twin — a Slack-signed webhook, not agent-facing.
58
68
 
69
+ **`AILK_OPERATOR_EMAIL` is REQUIRED for a `workspace`-module deployment** (#5515 item 1), exactly as it already is for the review dashboard. On a build with the `workspace` module and WITHOUT `multi-tenant`, `server.ts`'s `tenantPreHandlerForBuild()` selects the base guard (`src/middleware/default-tenant.ts` → `resolveDefaultTenantForOperator`), which fails CLOSED when the pin is unset: every `/v1/workspaces/*` route answers 403 to everyone — so the module's Admin pages (`apps/web/app/[locale]/(authed)/admin*`, renamed from `workspace` by #5637) render their shared `ForbiddenState` — and — the reason this matters — the TOFU operator bind (`services/default-tenant.ts#bindOperatorMembership`, #3631) is never reached, so the first account to load the settings page cannot seat ITSELF as the sole owner off a plain GET. With `multi-tenant` present the pin stays optional and resolution comes from the session's active workspace, unchanged. `.env.example` § Single-tenant operator pin carries the operator-facing copy; `src/middleware/__tests__/default-tenant.test.ts` § "#5515 item 1" is the objective record.
70
+
59
71
  The workspace site-config routes are **identity-bound tenant routes** (guarded by the tenant preHandler; the write scope is the caller's active workspace, never a body-supplied id). Like the sibling `/v1/workspaces/*` routes they have **no anonymous MCP twin** — the OSS MCP surface is identity-blind (`apps/mcp/CLAUDE.md`), so the "1:1 MCP tool" rule above does not apply to them; the tenant-scoped authed MCP surface is tracked in #3591. The `/v1/billing/*` and `/v1/waitlist-experiments/*` routes are the same shape — identity-bound tenant routes, admin/settings-facing rather than agent-facing, no MCP twin.
60
72
 
61
73
  AEO Layer 1 scoring runs both ways: locally as a CLI command (`ailk audit`, filesystem content-root input) AND behind the anonymous `/v1/aeo/score` HTTP endpoint above (external-URL input, #4666) — the CLI's content-root audit and the route's crawler-fed score are two different input paths over the same underlying scorer, not a contradiction.
@@ -144,7 +156,19 @@ sweep's own DB constraint (never a caller-side lock).
144
156
  - Don't put session state in `apps/api`. It's stateless.
145
157
  - Don't bundle `apps/api` (esbuild, rollup, webpack). Slice specifiers cannot survive it — see `## Deploy path`.
146
158
 
147
- Last updated: 2026-09-16 — doc-sync: added `GET /v1/leads` (#5678), the owner-scoped leads inbox read, to Endpoint conventions, recording its unconditional registration, its five-field row, its `resolveOwnerWorkspaces` scoping and its no-MCP-twin parity decision — verified against `apps/api/src/routes/leads/list.ts`, `apps/api/src/services/leads-inbox.ts`, `apps/api/src/services/owner-workspaces.ts` and `apps/api/src/server.ts`.
159
+ Last updated: 2026-09-21 — doc-sync: re-pointed magnet deliverables at the workspace-scoped `Asset` store (#6310) — extended the `/v1/deliverables/:grantId` row with the new `asset_` grant class (the named boundary it reads through, its serve-time tenancy re-check, its hardened attachment+CSP+no-store serve for every content type, and its uniform-404 fold), recording the rejected redirect-to-`/v1/assets/:assetId` mechanism and why — verified against `apps/api/src/routes/deliverables/index.ts`, `apps/api/src/lib/deliverable-asset-store.ts` and `apps/api/src/services/deliverable-fulfillment.ts`.
160
+
161
+ Prior: 2026-09-19 — doc-sync: added the three per-magnet captured-lead list routes (#6246 S6h) — `GET /v1/workspaces/:workspaceId/magnets/:id/{signups,subscribers,messages}` — to the magnets bullet, recording the `Lead.source = 'flow:' + slug` join, the new `tenantDb(tenant).leads.listBySource` accessor, the new `(workspaceId, source)` index, the wrong-kind/absent-magnet shared 404, the four/five-key wire shapes, the bounded `?limit=`, and the no-MCP-twin decision — verified against `apps/api/src/routes/magnets/index.ts`, `apps/api/src/lib/tenant-db.ts` and `database/inbox/schema.prisma`.
162
+
163
+ Prior: 2026-09-19 — doc-sync: added the Inbox message API (#6216 S7e) — `GET /v1/workspaces/:workspaceId/messages` and `GET`/`PATCH /v1/workspaces/:workspaceId/messages/:leadId` — to Endpoint conventions, recording the six-key served row, the message-source allowlist confinement, the shared 404 for four causes, the one-way idempotent PATCH, the `owner|admin` role floor and its TOFU precondition, and the no-MCP-twin decision — verified against `apps/api/src/routes/workspaces/messages.ts`, `apps/api/src/services/inbox-messages.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/server.ts`.
164
+
165
+ Prior: 2026-09-19 — doc-sync: added the tenant-scoped magnet Admin authoring surface (#6157 S5c) — `GET`/`POST /v1/workspaces/:workspaceId/magnets` and `GET`/`PATCH /v1/workspaces/:workspaceId/magnets/:id` — to Endpoint conventions, recording its `tenantDb`-only data path, its `owner|admin` role floor, the `multi-tenant || signal-collectors` gate, the config-URL origin gate and the no-MCP-twin parity decision — verified against `apps/api/src/routes/magnets/index.ts`, `apps/api/src/lib/tenant-db.ts` and `apps/api/src/server.ts`.
166
+
167
+ Prior: 2026-09-19 — doc-sync: the enabled-meeting-slugs gate (#6183 S9b) — added the `/v1/workspaces/:workspaceId/meetings` bullet (its own scope, gated on `scheduling` alone, the D24 `listEventTypes` join) and the `/v1/schedule/meeting-types` bullet (the public read, its own boundary file) — verified against `apps/api/src/routes/workspaces/meetings.ts`, `apps/api/src/routes/schedule/meeting-types.ts`, `apps/api/src/lib/public-meetings-config.ts` and `apps/api/src/server.ts`.
168
+
169
+ Prior: 2026-09-18 — doc-sync: the Docs list gains tags and a closed kind, and the asset store admits documents and gains list, metadata update and delete (#6093) — completed the `/v1/workspaces/:workspaceId/content` bullet with the page/asset union (`tags`, derived `kind`, `?tag=` filter), and added the `/v1/workspaces/:workspaceId/assets` + `/v1/assets/:assetId` family bullet (list/PATCH/DELETE, the widened upload allowlist admitting `image/svg+xml` and `application/pdf`, and the public read's raster-inline/non-raster-attachment+CSP serve split) — verified against `apps/api/src/routes/workspaces/content.ts`, `apps/api/src/routes/assets/{index,upload,read,manage}.ts` and `apps/api/src/lib/tenant-db.ts`.
170
+
171
+ Prior: 2026-09-16 — doc-sync: added `GET /v1/leads` (#5678), the owner-scoped leads inbox read, to Endpoint conventions, recording its unconditional registration, its five-field row, its `resolveOwnerWorkspaces` scoping and its no-MCP-twin parity decision — verified against `apps/api/src/routes/leads/list.ts`, `apps/api/src/services/leads-inbox.ts`, `apps/api/src/services/owner-workspaces.ts` and `apps/api/src/server.ts`.
148
172
 
149
173
  Prior: 2026-09-14 — doc-sync: signal-collectors module extraction (D-48, #5366) — added the `module: signal-collectors` note to the flows/waitlist-signups-me/project-listings-me endpoints, added the previously-undocumented `/v1/deliverables/:grantId`, `/v1/project-listings/{start,submit,:id,public}` and `/v1/slack/actions` rows — verified against `apps/api/src/server.ts` and each route's own header.
150
174