@invisibleloop/pulse 0.1.21

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 (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,193 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/stripe')
6
+
7
+ export default {
8
+ route: '/stripe',
9
+ meta: {
10
+ title: 'Payments (Stripe) — Pulse Docs',
11
+ description: 'Integrating Stripe Checkout and webhooks with Pulse using actions, raw response specs, and server data fetchers.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/stripe',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Payments (Stripe)')}
21
+ ${lead('Pulse uses Stripe\'s hosted Checkout — no client-side Stripe JS required. Checkout sessions are created server-side in an action\'s <code>run</code> function. Stripe handles the payment UI entirely. Webhooks are verified and handled through a raw response spec.')}
22
+
23
+ ${callout('info', 'Pulse has no external client-side JS. Use Stripe\'s hosted Checkout page (redirect flow) rather than Stripe Elements, which requires loading Stripe\'s client library.')}
24
+
25
+ ${section('setup', 'Setup')}
26
+ ${codeBlock(highlight(`npm install stripe`, 'bash'))}
27
+ ${codeBlock(highlight(`# .env
28
+ STRIPE_SECRET_KEY=sk_test_...
29
+ STRIPE_WEBHOOK_SECRET=whsec_...
30
+ APP_URL=http://localhost:3000`, 'bash'))}
31
+
32
+ ${section('checkout', 'Checkout action')}
33
+ <p>Create a Stripe Checkout session in an action's <code>run</code> function and redirect the browser to it.</p>
34
+ ${codeBlock(highlight(`// src/pages/pricing.js
35
+ import Stripe from 'stripe'
36
+
37
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
38
+ const APP_URL = process.env.APP_URL
39
+
40
+ export default {
41
+ route: '/pricing',
42
+
43
+ state: { status: 'idle' },
44
+
45
+ view: (state) => \`
46
+ <main id="main-content">
47
+ <h1>Pricing</h1>
48
+ <form data-action="checkout">
49
+ <input type="hidden" name="priceId" value="price_xxxx">
50
+ <button type="submit">
51
+ \${state.status === 'loading' ? 'Redirecting…' : 'Buy now'}
52
+ </button>
53
+ </form>
54
+ \${state.status === 'error'
55
+ ? '<p role="alert">Something went wrong. Please try again.</p>'
56
+ : ''}
57
+ </main>
58
+ \`,
59
+
60
+ actions: {
61
+ checkout: {
62
+ onStart: () => ({ status: 'loading' }),
63
+
64
+ run: async (state, serverState, formData) => {
65
+ const priceId = formData.get('priceId')
66
+
67
+ const session = await stripe.checkout.sessions.create({
68
+ mode: 'payment',
69
+ line_items: [{ price: priceId, quantity: 1 }],
70
+ success_url: \`\${APP_URL}/checkout/success?session={CHECKOUT_SESSION_ID}\`,
71
+ cancel_url: \`\${APP_URL}/checkout/cancel\`,
72
+ })
73
+
74
+ return { url: session.url }
75
+ },
76
+
77
+ onSuccess: (state, result) => {
78
+ // Redirect to Stripe's hosted checkout page
79
+ window.location.href = result.url
80
+ return { status: 'redirecting' }
81
+ },
82
+
83
+ onError: () => ({ status: 'error' }),
84
+ },
85
+ },
86
+ }`, 'js'))}
87
+
88
+ ${section('success', 'Success and cancel pages')}
89
+ ${codeBlock(highlight(`// src/pages/checkout/success.js
90
+ import Stripe from 'stripe'
91
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
92
+
93
+ export default {
94
+ route: '/checkout/success',
95
+ meta: { title: 'Payment successful', styles: ['/app.css'] },
96
+
97
+ server: {
98
+ session: async (ctx) => {
99
+ const { session } = ctx.query
100
+ if (!session) return null
101
+ return stripe.checkout.sessions.retrieve(session)
102
+ },
103
+ },
104
+
105
+ state: {},
106
+ view: (state, server) => \`
107
+ <main id="main-content">
108
+ <h1>Payment successful</h1>
109
+ \${server.session
110
+ ? \`<p>Thank you! Your order reference is <strong>\${server.session.id}</strong>.</p>\`
111
+ : '<p>Thank you for your purchase.</p>'
112
+ }
113
+ <a href="/">Back to home</a>
114
+ </main>
115
+ \`,
116
+ }`, 'js'))}
117
+
118
+ ${codeBlock(highlight(`// src/pages/checkout/cancel.js
119
+ export default {
120
+ route: '/checkout/cancel',
121
+ meta: { title: 'Payment cancelled', styles: ['/app.css'] },
122
+ state: {},
123
+ view: () => \`
124
+ <main id="main-content">
125
+ <h1>Payment cancelled</h1>
126
+ <p>No charge was made.</p>
127
+ <a href="/pricing">Back to pricing</a>
128
+ </main>
129
+ \`,
130
+ }`, 'js'))}
131
+
132
+ ${section('webhooks', 'Webhook handler')}
133
+ <p>Stripe sends signed POST requests to your webhook endpoint. Use a raw response spec to verify the signature and handle events. The raw body is required for signature verification — access it via <code>ctx.rawBody</code> if your server is configured to populate it, or read it from the request stream.</p>
134
+ ${codeBlock(highlight(`// src/pages/webhooks/stripe.js
135
+ import Stripe from 'stripe'
136
+
137
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
138
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
139
+
140
+ export default {
141
+ route: '/webhooks/stripe',
142
+ contentType: 'application/json',
143
+
144
+ server: {
145
+ event: async (ctx) => {
146
+ const sig = ctx.headers['stripe-signature']
147
+ const body = ctx.rawBody // raw Buffer — see note below
148
+
149
+ try {
150
+ return stripe.webhooks.constructEvent(body, sig, webhookSecret)
151
+ } catch (err) {
152
+ return { error: err.message }
153
+ }
154
+ },
155
+ },
156
+
157
+ render: (ctx, server) => {
158
+ if (server.event.error) {
159
+ ctx.setHeader('X-Webhook-Error', server.event.error)
160
+ return JSON.stringify({ error: server.event.error })
161
+ }
162
+
163
+ const event = server.event
164
+
165
+ if (event.type === 'checkout.session.completed') {
166
+ const session = event.data.object
167
+ // fulfil the order...
168
+ }
169
+
170
+ if (event.type === 'customer.subscription.deleted') {
171
+ // handle cancellation...
172
+ }
173
+
174
+ return JSON.stringify({ received: true })
175
+ },
176
+ }`, 'js'))}
177
+
178
+ ${callout('warning', 'Stripe signature verification requires the raw request body before JSON parsing. Configure your Pulse server with <code>onRequest</code> to capture <code>ctx.rawBody</code> for the webhook route, or use a dedicated webhook path handled before Pulse\'s request pipeline.')}
179
+
180
+ ${section('reference', 'Pattern summary')}
181
+ ${table(
182
+ ['Concern', 'Pulse primitive'],
183
+ [
184
+ ['Initiate checkout', '<code>action.run</code> — calls Stripe API server-side, returns checkout URL'],
185
+ ['Redirect to Stripe', '<code>action.onSuccess</code> — sets <code>window.location.href</code>'],
186
+ ['Confirm payment', '<code>spec.server</code> on success page — retrieves session from Stripe'],
187
+ ['Handle webhooks', 'Raw response spec with <code>render</code> returning JSON'],
188
+ ['Verify signature', '<code>spec.server</code> fetcher using <code>stripe.webhooks.constructEvent</code>'],
189
+ ]
190
+ )}
191
+ `,
192
+ }),
193
+ }
@@ -0,0 +1,323 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/supabase')
6
+
7
+ export default {
8
+ route: '/supabase',
9
+ meta: {
10
+ title: 'Supabase — Pulse Docs',
11
+ description: 'Integrate Supabase database queries, authentication, and file storage with Pulse server fetchers, actions, and guard.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/supabase',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Supabase')}
21
+ ${lead('Supabase provides Postgres, authentication, and file storage. In Pulse, all Supabase queries run in server fetchers — credentials stay on the server, query results are filtered before serialisation, and <code>guard</code> enforces session checks before any data is fetched.')}
22
+
23
+ ${section('setup', 'Setup')}
24
+ ${codeBlock(highlight(`npm install @supabase/supabase-js`, 'bash'))}
25
+ ${codeBlock(highlight(`# .env
26
+ SUPABASE_URL=https://your-project.supabase.co
27
+ SUPABASE_ANON_KEY=your-anon-key
28
+ SUPABASE_SERVICE_KEY=your-service-role-key # server-side only`, 'bash'))}
29
+ <p>Create two client helpers — one for public queries (respects Row Level Security), one for admin operations:</p>
30
+ ${codeBlock(highlight(`// src/lib/supabase.js
31
+ import { createClient } from '@supabase/supabase-js'
32
+
33
+ const { SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_KEY } = process.env
34
+
35
+ // Public client — use in server fetchers for user-scoped queries
36
+ export function supabase(accessToken) {
37
+ const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
38
+ global: accessToken
39
+ ? { headers: { Authorization: \`Bearer \${accessToken}\` } }
40
+ : {},
41
+ })
42
+ return client
43
+ }
44
+
45
+ // Admin client — bypasses RLS; use only for trusted server operations
46
+ export const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY)`, 'js'))}
47
+ ${callout('warning', 'The service key bypasses Row Level Security and has full database access. Keep it server-side only — in environment variables that are never sent to the browser or included in logs.')}
48
+
49
+ ${section('querying', 'Querying data')}
50
+ <p>Call Supabase inside <code>server</code> fetchers — they run on the server before every render. The result is passed to <code>view</code> as the second argument.</p>
51
+ ${codeBlock(highlight(`// src/pages/posts.js
52
+ import { supabase } from '../lib/supabase.js'
53
+ import { escHtml } from '@invisibleloop/pulse/html'
54
+
55
+ export default {
56
+ route: '/posts',
57
+
58
+ server: {
59
+ posts: async () => {
60
+ const { data, error } = await supabase().from('posts')
61
+ .select('id, title, slug, created_at')
62
+ .order('created_at', { ascending: false })
63
+ .limit(20)
64
+
65
+ if (error) throw new Error(error.message)
66
+ return data
67
+ },
68
+ },
69
+
70
+ state: {},
71
+
72
+ view: (_state, server) => \`
73
+ <main id="main-content">
74
+ <h1>Posts</h1>
75
+ <ul>
76
+ \${server.posts.map(p => \`
77
+ <li><a href="/posts/\${escHtml(p.slug)}">\${escHtml(p.title)}</a></li>
78
+ \`).join('')}
79
+ </ul>
80
+ </main>
81
+ \`,
82
+ }`, 'js'))}
83
+ ${callout('tip', 'Add <code>serverTtl</code> to cache the Supabase query result in-process. A 60-second TTL on a public listing page means one database hit per minute across all visitors — Supabase stays fast even under load.')}
84
+
85
+ ${section('auth-setup', 'Authentication')}
86
+ <p>Supabase Auth issues a JWT access token and a refresh token on login. Store both in <code>httpOnly</code> cookies — they are never accessible to JavaScript and survive page navigations.</p>
87
+
88
+ ${section('login', 'Login page')}
89
+ ${codeBlock(highlight(`// src/pages/auth/login.js
90
+ import { supabase } from '../../lib/supabase.js'
91
+ import { escHtml } from '@invisibleloop/pulse/html'
92
+
93
+ export default {
94
+ route: '/login',
95
+
96
+ guard: async (ctx) => {
97
+ // Already logged in — send to dashboard
98
+ if (ctx.cookies.access_token) return { redirect: '/dashboard' }
99
+ },
100
+
101
+ state: { status: 'idle', error: '' },
102
+
103
+ view: (state) => \`
104
+ <main id="main-content">
105
+ <h1>Sign in</h1>
106
+ <form data-action="login">
107
+ <label for="email">Email</label>
108
+ <input id="email" name="email" type="email" required autocomplete="email">
109
+
110
+ <label for="password">Password</label>
111
+ <input id="password" name="password" type="password" required autocomplete="current-password">
112
+
113
+ \${state.error ? \`<p role="alert">\${escHtml(state.error)}</p>\` : ''}
114
+
115
+ <button type="submit">
116
+ \${state.status === 'loading' ? 'Signing in…' : 'Sign in'}
117
+ </button>
118
+ </form>
119
+ </main>
120
+ \`,
121
+
122
+ actions: {
123
+ login: {
124
+ onStart: () => ({ status: 'loading', error: '' }),
125
+
126
+ run: async (_state, _server, formData) => {
127
+ const { data, error } = await supabase().auth.signInWithPassword({
128
+ email: formData.get('email'),
129
+ password: formData.get('password'),
130
+ })
131
+ if (error) throw new Error(error.message)
132
+ return data.session
133
+ },
134
+
135
+ onSuccess: (state, session, ctx) => {
136
+ const opts = { httpOnly: true, sameSite: 'Lax', path: '/' }
137
+ ctx.setCookie('access_token', session.access_token, { ...opts, maxAge: 3600 })
138
+ ctx.setCookie('refresh_token', session.refresh_token, { ...opts, maxAge: 604800 })
139
+ ctx.setHeader('Location', '/dashboard')
140
+ return { status: 'success' }
141
+ },
142
+
143
+ onError: (_state, err) => ({
144
+ status: 'idle',
145
+ error: err.message || 'Sign in failed',
146
+ }),
147
+ },
148
+ },
149
+ }`, 'js'))}
150
+
151
+ ${section('guard', 'Protecting routes')}
152
+ <p>Use <code>guard</code> to verify the session before any server data is fetched. Pass the token to your fetchers so Supabase enforces Row Level Security for that user.</p>
153
+ ${codeBlock(highlight(`// src/pages/dashboard.js
154
+ import { supabase, admin } from '../lib/supabase.js'
155
+
156
+ export default {
157
+ route: '/dashboard',
158
+
159
+ guard: async (ctx) => {
160
+ const token = ctx.cookies.access_token
161
+ if (!token) return { redirect: '/login' }
162
+
163
+ // Verify the token is still valid
164
+ const { error } = await supabase(token).auth.getUser()
165
+ if (error) return { redirect: '/login' }
166
+ },
167
+
168
+ server: {
169
+ // Token is available in guard ctx — pass it through server state or
170
+ // re-read from cookies in the fetcher
171
+ profile: async (ctx) => {
172
+ const { data } = await supabase(ctx.cookies.access_token)
173
+ .from('profiles')
174
+ .select('name, plan')
175
+ .single()
176
+ return data
177
+ },
178
+ },
179
+
180
+ state: {},
181
+ view: (_state, server) => \`
182
+ <main id="main-content">
183
+ <h1>Dashboard</h1>
184
+ <p>Welcome, \${server.profile.name}</p>
185
+ </main>
186
+ \`,
187
+ }`, 'js'))}
188
+ ${callout('note', 'Row Level Security is enforced by Postgres, not by your application code. A policy that checks <code>auth.uid() = user_id</code> means a bug in your server code cannot accidentally expose another user\'s data — the database rejects the query before it returns anything.')}
189
+
190
+ ${section('logout', 'Logout')}
191
+ ${codeBlock(highlight(`// src/pages/auth/logout.js
192
+ export default {
193
+ route: '/logout',
194
+ contentType: 'text/html',
195
+
196
+ render: (ctx) => {
197
+ const opts = { httpOnly: true, sameSite: 'Lax', path: '/', maxAge: 0 }
198
+ ctx.setCookie('access_token', '', opts)
199
+ ctx.setCookie('refresh_token', '', opts)
200
+ return { redirect: '/login' }
201
+ },
202
+ }`, 'js'))}
203
+
204
+ ${section('signup', 'Sign up')}
205
+ ${codeBlock(highlight(`// src/pages/auth/signup.js
206
+ import { supabase } from '../../lib/supabase.js'
207
+ import { escHtml } from '@invisibleloop/pulse/html'
208
+
209
+ export default {
210
+ route: '/signup',
211
+
212
+ state: { status: 'idle', error: '' },
213
+
214
+ view: (state) => \`
215
+ <main id="main-content">
216
+ <h1>Create account</h1>
217
+ \${state.status === 'success'
218
+ ? '<p role="status">Check your email to confirm your account.</p>'
219
+ : \`
220
+ <form data-action="signup">
221
+ <label for="email">Email</label>
222
+ <input id="email" name="email" type="email" required>
223
+
224
+ <label for="password">Password</label>
225
+ <input id="password" name="password" type="password" required minlength="8">
226
+
227
+ \${state.error ? \`<p role="alert">\${escHtml(state.error)}</p>\` : ''}
228
+
229
+ <button type="submit">
230
+ \${state.status === 'loading' ? 'Creating account…' : 'Create account'}
231
+ </button>
232
+ </form>
233
+ \`}
234
+ </main>
235
+ \`,
236
+
237
+ actions: {
238
+ signup: {
239
+ onStart: () => ({ status: 'loading', error: '' }),
240
+
241
+ run: async (_state, _server, formData) => {
242
+ const { error } = await supabase().auth.signUp({
243
+ email: formData.get('email'),
244
+ password: formData.get('password'),
245
+ })
246
+ if (error) throw new Error(error.message)
247
+ },
248
+
249
+ onSuccess: () => ({ status: 'success', error: '' }),
250
+ onError: (_state, err) => ({ status: 'idle', error: err.message }),
251
+ },
252
+ },
253
+ }`, 'js'))}
254
+
255
+ ${section('storage', 'File storage')}
256
+ <p>Upload files to Supabase Storage from an action. The file arrives in <code>FormData</code> — convert it to an <code>ArrayBuffer</code> and pass it to the storage client.</p>
257
+ ${codeBlock(highlight(`// src/pages/upload.js
258
+ import { admin } from '../lib/supabase.js'
259
+ import { escHtml } from '@invisibleloop/pulse/html'
260
+
261
+ export default {
262
+ route: '/upload',
263
+
264
+ state: { status: 'idle', url: '', error: '' },
265
+
266
+ view: (state) => \`
267
+ <main id="main-content">
268
+ <h1>Upload file</h1>
269
+ <form data-action="upload" enctype="multipart/form-data">
270
+ <input name="file" type="file" accept="image/*" required>
271
+ <button type="submit">
272
+ \${state.status === 'loading' ? 'Uploading…' : 'Upload'}
273
+ </button>
274
+ </form>
275
+ \${state.url ? \`<img src="\${escHtml(state.url)}" alt="Uploaded file" width="400" height="300">\` : ''}
276
+ \${state.error ? \`<p role="alert">\${escHtml(state.error)}</p>\` : ''}
277
+ </main>
278
+ \`,
279
+
280
+ actions: {
281
+ upload: {
282
+ onStart: () => ({ status: 'loading', error: '' }),
283
+
284
+ run: async (_state, _server, formData) => {
285
+ const file = formData.get('file')
286
+ const buffer = await file.arrayBuffer()
287
+ const ext = file.name.split('.').pop()
288
+ const path = \`\${crypto.randomUUID()}.\${ext}\`
289
+
290
+ const { error } = await admin.storage
291
+ .from('uploads')
292
+ .upload(path, buffer, { contentType: file.type })
293
+
294
+ if (error) throw new Error(error.message)
295
+
296
+ const { data } = admin.storage.from('uploads').getPublicUrl(path)
297
+ return data.publicUrl
298
+ },
299
+
300
+ onSuccess: (_state, url) => ({ status: 'idle', url }),
301
+ onError: (_state, err) => ({ status: 'idle', error: err.message }),
302
+ },
303
+ },
304
+ }`, 'js'))}
305
+
306
+ ${section('rls', 'Row Level Security')}
307
+ <p>Always enable RLS on tables that hold user data. A minimal policy that lets users read only their own rows:</p>
308
+ ${codeBlock(highlight(`-- Enable RLS on the table
309
+ alter table posts enable row level security;
310
+
311
+ -- Users can only select their own posts
312
+ create policy "users read own posts"
313
+ on posts for select
314
+ using (auth.uid() = user_id);
315
+
316
+ -- Users can only insert rows for themselves
317
+ create policy "users insert own posts"
318
+ on posts for insert
319
+ with check (auth.uid() = user_id);`, 'bash'))}
320
+ ${callout('tip', 'The public client (with the user\'s access token) applies Row Level Security — queries are automatically scoped to that user\'s data. The admin client bypasses RLS entirely, which is what you want for webhook handlers, background jobs, and admin operations, but not for user-scoped queries.')}
321
+ `,
322
+ }),
323
+ }