@pylonsync/create-pylon 0.3.273 → 0.3.275

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 (323) hide show
  1. package/bin/create-pylon.js +80 -0
  2. package/package.json +1 -1
  3. package/templates/ARCHETYPES.md +339 -0
  4. package/templates/agency/.env.example +12 -0
  5. package/templates/agency/AGENTS.md +61 -0
  6. package/templates/agency/README.md +90 -0
  7. package/templates/agency/app/auth-form.tsx +129 -0
  8. package/templates/agency/app/contact-form.tsx +258 -0
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
  10. package/templates/agency/app/dashboard/page.tsx +70 -0
  11. package/templates/agency/app/error.tsx +26 -0
  12. package/templates/agency/app/globals.css +148 -0
  13. package/templates/agency/app/layout.tsx +174 -0
  14. package/templates/agency/app/login/page.tsx +39 -0
  15. package/templates/agency/app/not-found.tsx +19 -0
  16. package/templates/agency/app/page.tsx +207 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/sitemap.ts +9 -0
  19. package/templates/agency/app.ts +135 -0
  20. package/templates/agency/components/marketing.tsx +148 -0
  21. package/templates/agency/components/section-scroller.tsx +35 -0
  22. package/templates/agency/components/ui/button.tsx +56 -0
  23. package/templates/agency/components/ui/card.tsx +90 -0
  24. package/templates/agency/components.json +20 -0
  25. package/templates/agency/functions/bookInquiry.ts +42 -0
  26. package/templates/agency/functions/declineInquiry.ts +41 -0
  27. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  28. package/templates/agency/functions/seedCapacity.ts +26 -0
  29. package/templates/agency/functions/setCapacity.ts +32 -0
  30. package/templates/agency/functions/submitInquiry.ts +55 -0
  31. package/templates/agency/gitignore +10 -0
  32. package/templates/agency/lib/agency.ts +27 -0
  33. package/templates/agency/lib/owner.ts +26 -0
  34. package/templates/agency/lib/site.config.ts +239 -0
  35. package/templates/agency/lib/utils.ts +10 -0
  36. package/templates/agency/package.json +34 -0
  37. package/templates/agency/tsconfig.json +18 -0
  38. package/templates/ai-chat/.env.example +33 -0
  39. package/templates/ai-chat/AGENTS.md +61 -0
  40. package/templates/ai-chat/README.md +99 -0
  41. package/templates/ai-chat/app/auth-form.tsx +124 -0
  42. package/templates/ai-chat/app/chat-client.tsx +414 -0
  43. package/templates/ai-chat/app/error.tsx +26 -0
  44. package/templates/ai-chat/app/globals.css +148 -0
  45. package/templates/ai-chat/app/layout.tsx +75 -0
  46. package/templates/ai-chat/app/login/page.tsx +39 -0
  47. package/templates/ai-chat/app/not-found.tsx +19 -0
  48. package/templates/ai-chat/app/page.tsx +23 -0
  49. package/templates/ai-chat/app.ts +121 -0
  50. package/templates/ai-chat/components.json +20 -0
  51. package/templates/ai-chat/gitignore +10 -0
  52. package/templates/ai-chat/lib/site.config.ts +103 -0
  53. package/templates/ai-chat/lib/utils.ts +10 -0
  54. package/templates/ai-chat/package.json +34 -0
  55. package/templates/ai-chat/tsconfig.json +18 -0
  56. package/templates/ai-studio/.env.example +19 -0
  57. package/templates/ai-studio/AGENTS.md +61 -0
  58. package/templates/ai-studio/README.md +83 -0
  59. package/templates/ai-studio/app/auth-form.tsx +124 -0
  60. package/templates/ai-studio/app/error.tsx +26 -0
  61. package/templates/ai-studio/app/globals.css +148 -0
  62. package/templates/ai-studio/app/layout.tsx +75 -0
  63. package/templates/ai-studio/app/login/page.tsx +39 -0
  64. package/templates/ai-studio/app/not-found.tsx +19 -0
  65. package/templates/ai-studio/app/page.tsx +34 -0
  66. package/templates/ai-studio/app/studio-client.tsx +214 -0
  67. package/templates/ai-studio/app.ts +108 -0
  68. package/templates/ai-studio/components.json +20 -0
  69. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  70. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  71. package/templates/ai-studio/functions/generate.ts +42 -0
  72. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  73. package/templates/ai-studio/gitignore +10 -0
  74. package/templates/ai-studio/lib/site.config.ts +80 -0
  75. package/templates/ai-studio/lib/studio.ts +52 -0
  76. package/templates/ai-studio/lib/utils.ts +10 -0
  77. package/templates/ai-studio/package.json +34 -0
  78. package/templates/ai-studio/tsconfig.json +18 -0
  79. package/templates/creator/.env.example +12 -0
  80. package/templates/creator/AGENTS.md +61 -0
  81. package/templates/creator/README.md +67 -0
  82. package/templates/creator/app/auth-form.tsx +129 -0
  83. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  84. package/templates/creator/app/dashboard/page.tsx +70 -0
  85. package/templates/creator/app/error.tsx +26 -0
  86. package/templates/creator/app/globals.css +148 -0
  87. package/templates/creator/app/layout.tsx +160 -0
  88. package/templates/creator/app/login/page.tsx +39 -0
  89. package/templates/creator/app/newsletter-signup.tsx +162 -0
  90. package/templates/creator/app/not-found.tsx +19 -0
  91. package/templates/creator/app/page.tsx +160 -0
  92. package/templates/creator/app/robots.ts +12 -0
  93. package/templates/creator/app/sitemap.ts +9 -0
  94. package/templates/creator/app.ts +134 -0
  95. package/templates/creator/components/marketing.tsx +148 -0
  96. package/templates/creator/components/section-scroller.tsx +35 -0
  97. package/templates/creator/components/ui/button.tsx +56 -0
  98. package/templates/creator/components/ui/card.tsx +90 -0
  99. package/templates/creator/components.json +20 -0
  100. package/templates/creator/functions/subscribe.ts +82 -0
  101. package/templates/creator/functions/subscriberStats.ts +75 -0
  102. package/templates/creator/gitignore +10 -0
  103. package/templates/creator/lib/owner.ts +26 -0
  104. package/templates/creator/lib/site.config.ts +173 -0
  105. package/templates/creator/lib/stats.ts +30 -0
  106. package/templates/creator/lib/utils.ts +10 -0
  107. package/templates/creator/package.json +34 -0
  108. package/templates/creator/tsconfig.json +18 -0
  109. package/templates/default/app/layout.tsx +26 -27
  110. package/templates/default/app/page.tsx +90 -274
  111. package/templates/default/lib/products.ts +9 -122
  112. package/templates/default/lib/site.config.ts +739 -0
  113. package/templates/default/lib/site.ts +14 -261
  114. package/templates/directory/.env.example +12 -0
  115. package/templates/directory/AGENTS.md +61 -0
  116. package/templates/directory/README.md +80 -0
  117. package/templates/directory/app/auth-form.tsx +129 -0
  118. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  119. package/templates/directory/app/dashboard/page.tsx +70 -0
  120. package/templates/directory/app/directory-browse.tsx +328 -0
  121. package/templates/directory/app/error.tsx +26 -0
  122. package/templates/directory/app/globals.css +148 -0
  123. package/templates/directory/app/layout.tsx +171 -0
  124. package/templates/directory/app/login/page.tsx +39 -0
  125. package/templates/directory/app/not-found.tsx +19 -0
  126. package/templates/directory/app/page.tsx +50 -0
  127. package/templates/directory/app/robots.ts +12 -0
  128. package/templates/directory/app/sitemap.ts +9 -0
  129. package/templates/directory/app/submit/page.tsx +30 -0
  130. package/templates/directory/app/submit-form.tsx +151 -0
  131. package/templates/directory/app.ts +146 -0
  132. package/templates/directory/components/marketing.tsx +148 -0
  133. package/templates/directory/components/section-scroller.tsx +35 -0
  134. package/templates/directory/components/ui/button.tsx +56 -0
  135. package/templates/directory/components/ui/card.tsx +90 -0
  136. package/templates/directory/components.json +20 -0
  137. package/templates/directory/functions/approveSubmission.ts +45 -0
  138. package/templates/directory/functions/rejectSubmission.ts +20 -0
  139. package/templates/directory/functions/seedListings.ts +33 -0
  140. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  141. package/templates/directory/functions/submitListing.ts +63 -0
  142. package/templates/directory/functions/upvote.ts +24 -0
  143. package/templates/directory/gitignore +10 -0
  144. package/templates/directory/lib/directory.ts +45 -0
  145. package/templates/directory/lib/owner.ts +26 -0
  146. package/templates/directory/lib/site.config.ts +130 -0
  147. package/templates/directory/lib/utils.ts +10 -0
  148. package/templates/directory/package.json +34 -0
  149. package/templates/directory/tsconfig.json +18 -0
  150. package/templates/local-service/.env.example +12 -0
  151. package/templates/local-service/AGENTS.md +61 -0
  152. package/templates/local-service/README.md +82 -0
  153. package/templates/local-service/app/auth-form.tsx +129 -0
  154. package/templates/local-service/app/booking-widget.tsx +399 -0
  155. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  156. package/templates/local-service/app/dashboard/page.tsx +63 -0
  157. package/templates/local-service/app/error.tsx +26 -0
  158. package/templates/local-service/app/globals.css +148 -0
  159. package/templates/local-service/app/layout.tsx +151 -0
  160. package/templates/local-service/app/login/page.tsx +39 -0
  161. package/templates/local-service/app/not-found.tsx +19 -0
  162. package/templates/local-service/app/page.tsx +233 -0
  163. package/templates/local-service/app/robots.ts +12 -0
  164. package/templates/local-service/app/sitemap.ts +9 -0
  165. package/templates/local-service/app.ts +131 -0
  166. package/templates/local-service/components/marketing.tsx +162 -0
  167. package/templates/local-service/components/section-scroller.tsx +35 -0
  168. package/templates/local-service/components/ui/button.tsx +56 -0
  169. package/templates/local-service/components/ui/card.tsx +90 -0
  170. package/templates/local-service/components.json +20 -0
  171. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  172. package/templates/local-service/functions/cancelBooking.ts +27 -0
  173. package/templates/local-service/functions/confirmBooking.ts +18 -0
  174. package/templates/local-service/functions/createBooking.ts +98 -0
  175. package/templates/local-service/gitignore +10 -0
  176. package/templates/local-service/lib/booking.ts +24 -0
  177. package/templates/local-service/lib/owner.ts +26 -0
  178. package/templates/local-service/lib/site.config.ts +232 -0
  179. package/templates/local-service/lib/slots.ts +97 -0
  180. package/templates/local-service/lib/utils.ts +10 -0
  181. package/templates/local-service/package.json +34 -0
  182. package/templates/local-service/tsconfig.json +18 -0
  183. package/templates/marketplace/.env.example +9 -0
  184. package/templates/marketplace/AGENTS.md +61 -0
  185. package/templates/marketplace/README.md +78 -0
  186. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  187. package/templates/marketplace/app/error.tsx +26 -0
  188. package/templates/marketplace/app/globals.css +64 -0
  189. package/templates/marketplace/app/layout.tsx +60 -0
  190. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  191. package/templates/marketplace/app/me/page.tsx +15 -0
  192. package/templates/marketplace/app/not-found.tsx +20 -0
  193. package/templates/marketplace/app/page.tsx +159 -0
  194. package/templates/marketplace/app/robots.ts +12 -0
  195. package/templates/marketplace/app/sell/page.tsx +26 -0
  196. package/templates/marketplace/app/sitemap.ts +14 -0
  197. package/templates/marketplace/app.ts +190 -0
  198. package/templates/marketplace/client/AuthNav.tsx +46 -0
  199. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  200. package/templates/marketplace/client/LoginCard.tsx +130 -0
  201. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  202. package/templates/marketplace/client/MyMarket.tsx +180 -0
  203. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  204. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  205. package/templates/marketplace/client/SellForm.tsx +160 -0
  206. package/templates/marketplace/client/WatchButton.tsx +88 -0
  207. package/templates/marketplace/client/market.ts +341 -0
  208. package/templates/marketplace/functions/buyNow.ts +78 -0
  209. package/templates/marketplace/functions/makeOffer.ts +65 -0
  210. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  211. package/templates/marketplace/functions/seedMarket.ts +90 -0
  212. package/templates/marketplace/gitignore +10 -0
  213. package/templates/marketplace/package.json +35 -0
  214. package/templates/marketplace/tsconfig.json +14 -0
  215. package/templates/marketplace/ui/badge.tsx +30 -0
  216. package/templates/marketplace/ui/button.tsx +49 -0
  217. package/templates/marketplace/ui/card.tsx +48 -0
  218. package/templates/marketplace/ui/input.tsx +17 -0
  219. package/templates/marketplace/ui/label.tsx +18 -0
  220. package/templates/marketplace/ui/textarea.tsx +17 -0
  221. package/templates/marketplace/ui/tokens.css +32 -0
  222. package/templates/marketplace/ui/utils.ts +6 -0
  223. package/templates/restaurant/.env.example +12 -0
  224. package/templates/restaurant/AGENTS.md +61 -0
  225. package/templates/restaurant/README.md +77 -0
  226. package/templates/restaurant/app/auth-form.tsx +129 -0
  227. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  228. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  229. package/templates/restaurant/app/error.tsx +26 -0
  230. package/templates/restaurant/app/globals.css +148 -0
  231. package/templates/restaurant/app/layout.tsx +151 -0
  232. package/templates/restaurant/app/login/page.tsx +39 -0
  233. package/templates/restaurant/app/not-found.tsx +19 -0
  234. package/templates/restaurant/app/page.tsx +194 -0
  235. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  236. package/templates/restaurant/app/robots.ts +12 -0
  237. package/templates/restaurant/app/sitemap.ts +9 -0
  238. package/templates/restaurant/app.ts +115 -0
  239. package/templates/restaurant/components/marketing.tsx +162 -0
  240. package/templates/restaurant/components/section-scroller.tsx +35 -0
  241. package/templates/restaurant/components/ui/button.tsx +56 -0
  242. package/templates/restaurant/components/ui/card.tsx +90 -0
  243. package/templates/restaurant/components.json +20 -0
  244. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  245. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  246. package/templates/restaurant/functions/createReservation.ts +92 -0
  247. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  248. package/templates/restaurant/gitignore +10 -0
  249. package/templates/restaurant/lib/owner.ts +26 -0
  250. package/templates/restaurant/lib/reservation.ts +22 -0
  251. package/templates/restaurant/lib/site.config.ts +218 -0
  252. package/templates/restaurant/lib/slots.ts +55 -0
  253. package/templates/restaurant/lib/utils.ts +10 -0
  254. package/templates/restaurant/package.json +34 -0
  255. package/templates/restaurant/tsconfig.json +18 -0
  256. package/templates/shop/.env.example +32 -0
  257. package/templates/shop/AGENTS.md +61 -0
  258. package/templates/shop/README.md +102 -0
  259. package/templates/shop/app/auth-form.tsx +129 -0
  260. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  261. package/templates/shop/app/dashboard/page.tsx +59 -0
  262. package/templates/shop/app/error.tsx +26 -0
  263. package/templates/shop/app/globals.css +148 -0
  264. package/templates/shop/app/layout.tsx +160 -0
  265. package/templates/shop/app/login/page.tsx +39 -0
  266. package/templates/shop/app/not-found.tsx +19 -0
  267. package/templates/shop/app/page.tsx +95 -0
  268. package/templates/shop/app/robots.ts +12 -0
  269. package/templates/shop/app/shop-client.tsx +436 -0
  270. package/templates/shop/app/sitemap.ts +9 -0
  271. package/templates/shop/app/success/page.tsx +33 -0
  272. package/templates/shop/app.ts +134 -0
  273. package/templates/shop/components/marketing.tsx +96 -0
  274. package/templates/shop/components/section-scroller.tsx +35 -0
  275. package/templates/shop/components/ui/button.tsx +56 -0
  276. package/templates/shop/components/ui/card.tsx +90 -0
  277. package/templates/shop/components.json +20 -0
  278. package/templates/shop/functions/cancelOrder.ts +33 -0
  279. package/templates/shop/functions/checkout.ts +130 -0
  280. package/templates/shop/functions/fulfillOrder.ts +17 -0
  281. package/templates/shop/functions/markGroupPaid.ts +26 -0
  282. package/templates/shop/functions/ordersForOwner.ts +28 -0
  283. package/templates/shop/functions/releaseGroup.ts +36 -0
  284. package/templates/shop/functions/reserveCart.ts +87 -0
  285. package/templates/shop/functions/restockProduct.ts +23 -0
  286. package/templates/shop/functions/seedProducts.ts +30 -0
  287. package/templates/shop/functions/stripeWebhook.ts +72 -0
  288. package/templates/shop/gitignore +10 -0
  289. package/templates/shop/lib/owner.ts +26 -0
  290. package/templates/shop/lib/shop.ts +45 -0
  291. package/templates/shop/lib/site.config.ts +198 -0
  292. package/templates/shop/lib/utils.ts +10 -0
  293. package/templates/shop/package.json +35 -0
  294. package/templates/shop/tsconfig.json +18 -0
  295. package/templates/waitlist/.env.example +12 -0
  296. package/templates/waitlist/AGENTS.md +61 -0
  297. package/templates/waitlist/README.md +81 -0
  298. package/templates/waitlist/app/auth-form.tsx +129 -0
  299. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  300. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  301. package/templates/waitlist/app/error.tsx +26 -0
  302. package/templates/waitlist/app/globals.css +148 -0
  303. package/templates/waitlist/app/layout.tsx +158 -0
  304. package/templates/waitlist/app/login/page.tsx +39 -0
  305. package/templates/waitlist/app/not-found.tsx +19 -0
  306. package/templates/waitlist/app/page.tsx +119 -0
  307. package/templates/waitlist/app/robots.ts +12 -0
  308. package/templates/waitlist/app/sitemap.ts +9 -0
  309. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  310. package/templates/waitlist/app.ts +134 -0
  311. package/templates/waitlist/components/marketing.tsx +96 -0
  312. package/templates/waitlist/components/ui/button.tsx +56 -0
  313. package/templates/waitlist/components/ui/card.tsx +90 -0
  314. package/templates/waitlist/components.json +20 -0
  315. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  316. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  317. package/templates/waitlist/gitignore +10 -0
  318. package/templates/waitlist/lib/owner.ts +26 -0
  319. package/templates/waitlist/lib/site.config.ts +178 -0
  320. package/templates/waitlist/lib/stats.ts +30 -0
  321. package/templates/waitlist/lib/utils.ts +10 -0
  322. package/templates/waitlist/package.json +34 -0
  323. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,134 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // waitlist — a pre-launch / coming-soon landing page with a LIVE signup
12
+ // counter. The whole point is the realtime hook: open the page in two tabs,
13
+ // submit an email in one, and the counter on the other ticks up with no
14
+ // refresh. That's the proof it's a real live app and not a static page.
15
+ //
16
+ // The data model is deliberately tiny — two entities:
17
+ // • Signup — one row per email. Holds visitor PII, so it denies ALL client
18
+ // reads/writes (writes go through the joinWaitlist mutation; the
19
+ // public page only ever sees an aggregate count, never an email).
20
+ // • User — the business owner's account (email/password is built in), so
21
+ // the owner can sign in to the dashboard and see their signups.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ // One waitlist signup. `email` is the only PII; `createdAt` powers the
25
+ // signups-over-time chart on the dashboard. The unique index on email dedupes
26
+ // at the database level — a duplicate insert is rejected even under a race, so
27
+ // joinWaitlist can treat the conflict as "already joined".
28
+ const Signup = entity(
29
+ "Signup",
30
+ {
31
+ email: field.string(),
32
+ createdAt: field.datetime().defaultNow(),
33
+ },
34
+ {
35
+ indexes: [
36
+ { name: "by_email", fields: ["email"], unique: true },
37
+ { name: "by_created", fields: ["createdAt"], unique: false },
38
+ ],
39
+ },
40
+ );
41
+
42
+ // A single-row, PII-FREE aggregate the public page can safely read live. It
43
+ // holds only the signup count — no emails. `joinWaitlist` keeps it in sync with
44
+ // the real Signup count on every new join. The landing page subscribes with
45
+ // `db.useQuery("WaitlistStat")`, which syncs across every open tab through the
46
+ // replica — so the counter ticks up everywhere the instant someone joins. This
47
+ // is the cross-tab-safe realtime primitive (entity sync), not a per-connection
48
+ // server-query subscription.
49
+ const WaitlistStat = entity(
50
+ "WaitlistStat",
51
+ {
52
+ count: field.int().default(0),
53
+ updatedAt: field.datetime().defaultNow(),
54
+ },
55
+ {},
56
+ );
57
+
58
+ // The business owner's account. Email/password auth is built in against an
59
+ // entity named "User" (passwordHash is server-only; the register route stamps
60
+ // avatarColor). The dashboard is gated to the owner — see PYLON_OWNER_EMAIL in
61
+ // functions/waitlistStats.ts and app/dashboard/page.tsx.
62
+ const User = entity(
63
+ "User",
64
+ {
65
+ email: field.string(),
66
+ displayName: field.string().optional(),
67
+ passwordHash: field.string().serverOnly().optional(),
68
+ avatarColor: field.string().optional(),
69
+ // Set when the owner verifies their email; unused by the waitlist flow but
70
+ // declared so the framework's email-verification routes have a column.
71
+ emailVerified: field.datetime().optional(),
72
+ createdAt: field.datetime().defaultNow(),
73
+ },
74
+ { indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
75
+ );
76
+
77
+ // PRIVACY — the heart of the spec. Signup holds visitor emails, so it denies
78
+ // EVERY client read and write. No `db.useQuery("Signup")` can ever pull a row,
79
+ // and no client can insert/update/delete directly. Writes happen only inside
80
+ // the server-side `joinWaitlist` mutation (functions bypass policies); reads
81
+ // happen only inside `waitlistCount` (returns a bare integer) and the
82
+ // owner-gated `waitlistStats`. A marketing site must never leak its own
83
+ // customers' emails — this policy is what guarantees it.
84
+ const signupPolicy = policy({
85
+ name: "signup_private",
86
+ entity: "Signup",
87
+ allowRead: "false",
88
+ allowInsert: "false",
89
+ allowUpdate: "false",
90
+ allowDelete: "false",
91
+ });
92
+
93
+ // The aggregate count is public to READ (it's just a number — the whole point
94
+ // is that the landing page shows it live to everyone). Clients can't WRITE it;
95
+ // only the joinWaitlist mutation maintains it server-side.
96
+ const waitlistStatPolicy = policy({
97
+ name: "waitlist_stat_public_read",
98
+ entity: "WaitlistStat",
99
+ allowRead: "true",
100
+ allowInsert: "false",
101
+ allowUpdate: "false",
102
+ allowDelete: "false",
103
+ });
104
+
105
+ // The owner reads their own User row (the dashboard resolves their email this
106
+ // way to check ownership). The auth subsystem owns all writes.
107
+ const userPolicy = policy({
108
+ name: "user_self",
109
+ entity: "User",
110
+ allowRead: "auth.userId == data.id",
111
+ allowInsert: "false",
112
+ allowUpdate: "false",
113
+ allowDelete: "false",
114
+ });
115
+
116
+ const manifest = buildManifest({
117
+ name: "__APP_NAME__",
118
+ version: "0.1.0",
119
+ entities: [Signup, WaitlistStat, User],
120
+ // joinWaitlist (public mutation) + waitlistStats (owner-gated query) live in
121
+ // functions/ and are discovered automatically — they don't need listing here.
122
+ queries: [],
123
+ actions: [],
124
+ policies: [signupPolicy, waitlistStatPolicy, userPolicy],
125
+ // Email/password is on by default against the User entity above. No orgs,
126
+ // no billing — a waitlist is single-tenant (one business, one owner).
127
+ auth: auth(),
128
+ routes: await discoverAppRoutes(),
129
+ });
130
+
131
+ // Emit the canonical manifest JSON to stdout — `pylon dev` captures this.
132
+ console.log(JSON.stringify(manifest, null, 2));
133
+
134
+ export default manifest;
@@ -0,0 +1,96 @@
1
+ import React from "react";
2
+
3
+ // Reusable presentational pieces for the landing page. All server-rendered —
4
+ // no client JS. Restyle here and the whole page follows. The brand accent
5
+ // (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
6
+ // app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
7
+
8
+ // Shared container: a contained, centered column.
9
+ export const WRAP = "mx-auto w-full max-w-3xl px-6";
10
+
11
+ export function Eyebrow({ children }: { children: React.ReactNode }) {
12
+ return (
13
+ <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
14
+ {children}
15
+ </p>
16
+ );
17
+ }
18
+
19
+ // "New / Coming soon"-style pill for the hero.
20
+ export function Badge({ children }: { children: React.ReactNode }) {
21
+ return (
22
+ <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
23
+ <span className="inline-block size-1.5 rounded-full bg-brand" />
24
+ {children}
25
+ </span>
26
+ );
27
+ }
28
+
29
+ export function Divider() {
30
+ return (
31
+ <div className={WRAP}>
32
+ <div className="border-t border-zinc-200/70" />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export function SectionHead({
38
+ eyebrow,
39
+ title,
40
+ body,
41
+ }: {
42
+ eyebrow: string;
43
+ title: string;
44
+ body?: string;
45
+ }) {
46
+ return (
47
+ <div>
48
+ <Eyebrow>{eyebrow}</Eyebrow>
49
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
50
+ {title}
51
+ </h2>
52
+ {body ? (
53
+ <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
54
+ {body}
55
+ </p>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // A grid of value props — icon + title + body.
62
+ export function FeatureGrid({
63
+ items,
64
+ }: {
65
+ items: { title: string; body: string; icon?: string }[];
66
+ }) {
67
+ return (
68
+ <div className="grid gap-6 sm:grid-cols-3">
69
+ {items.map((f) => (
70
+ <div key={f.title}>
71
+ {f.icon ? (
72
+ <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
73
+ {f.icon}
74
+ </span>
75
+ ) : null}
76
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
77
+ {f.title}
78
+ </h3>
79
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
80
+ {f.body}
81
+ </p>
82
+ </div>
83
+ ))}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ // Initials for testimonial avatars, so the cards look finished without a photo.
89
+ export function initials(name: string) {
90
+ return name
91
+ .split(/\s+/)
92
+ .map((w) => w[0])
93
+ .join("")
94
+ .slice(0, 2)
95
+ .toUpperCase();
96
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "rounded-xl border bg-card text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return (
23
+ <div
24
+ data-slot="card-header"
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLDivElement>) {
35
+ return (
36
+ <div
37
+ data-slot="card-title"
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function CardDescription({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) {
48
+ return (
49
+ <div
50
+ data-slot="card-description"
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardContent({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ data-slot="card-content"
64
+ className={cn("p-6 pt-0", className)}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function CardFooter({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center p-6 pt-0", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export {
84
+ Card,
85
+ CardHeader,
86
+ CardFooter,
87
+ CardTitle,
88
+ CardDescription,
89
+ CardContent,
90
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -0,0 +1,82 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // Normalize + sanity-check an email without pulling in a dependency. This is a
4
+ // pragmatic "looks like an email" check, not RFC 5322 — the unique index is the
5
+ // real integrity guard. We cap the length so a hostile caller can't stuff a
6
+ // megabyte string into the row.
7
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ function normalizeEmail(raw: string): string | null {
9
+ const email = raw.trim().toLowerCase();
10
+ if (email.length < 3 || email.length > 254) return null;
11
+ if (!EMAIL_RE.test(email)) return null;
12
+ return email;
13
+ }
14
+
15
+ // joinWaitlist — the ONLY way a Signup row is ever written. It's a `mutation`
16
+ // (not an `action`): mutations get `ctx.db` access and run as one atomic
17
+ // transaction, and the insert fires a change event that re-runs the live
18
+ // `waitlistCount` query for every open tab — that's what makes the counter tick
19
+ // up in realtime.
20
+ //
21
+ // `auth: "public"` because a landing-page visitor has no account. Public
22
+ // mutations are still rate-limited at the HTTP layer in production (the
23
+ // framework's rate_limit plugin); here we also validate + dedupe so the same
24
+ // email can't inflate the count.
25
+ //
26
+ // PRIVACY: this returns only `{ ok, alreadyJoined }`. It never returns a Signup
27
+ // row, an id, or anyone else's email.
28
+ export default mutation<{ email: string }, { ok: boolean; alreadyJoined: boolean }>({
29
+ auth: "public",
30
+ args: { email: v.string() },
31
+ async handler(ctx, args) {
32
+ const email = normalizeEmail(args.email);
33
+ if (!email) {
34
+ throw ctx.error("INVALID_ARGS", "Enter a valid email address.");
35
+ }
36
+
37
+ // Signup denies ALL client access by policy, so these go through
38
+ // `ctx.db.unsafe` — the explicit "this is an intentional cross-user write
39
+ // from a trusted handler" surface (also future-proofs against
40
+ // PYLON_STRICT_FN_POLICIES). The handler IS the only writer.
41
+ //
42
+ // Dedupe: if this email already joined, report success idempotently rather
43
+ // than erroring — re-submitting the same address is a no-op, not a failure.
44
+ const existing = await ctx.db.unsafe.lookup("Signup", "email", email);
45
+ if (existing) {
46
+ return { ok: true, alreadyJoined: true };
47
+ }
48
+
49
+ try {
50
+ await ctx.db.unsafe.insert("Signup", {
51
+ email,
52
+ createdAt: new Date().toISOString(),
53
+ });
54
+ } catch (e) {
55
+ // Lost a race to a concurrent insert of the same email — the unique index
56
+ // on `email` rejected the duplicate. That's still "you're on the list".
57
+ const msg = e instanceof Error ? e.message : String(e);
58
+ if (/unique|constraint|conflict|duplicate/i.test(msg)) {
59
+ return { ok: true, alreadyJoined: true };
60
+ }
61
+ throw e;
62
+ }
63
+
64
+ // Keep the public, PII-free WaitlistStat singleton in sync with the real
65
+ // count. We RECOUNT (rather than +1) so the number can never drift, and
66
+ // this whole handler is one transaction — on SQLite writers serialize, so
67
+ // the recount-then-write is consistent. The landing page reads this row via
68
+ // `db.useQuery`, which syncs the new value to every open tab.
69
+ const total = (await ctx.db.unsafe.list("Signup")).length;
70
+ const stat = (await ctx.db.unsafe.list("WaitlistStat"))[0] as
71
+ | { id: string }
72
+ | undefined;
73
+ const now = new Date().toISOString();
74
+ if (stat) {
75
+ await ctx.db.unsafe.update("WaitlistStat", stat.id, { count: total, updatedAt: now });
76
+ } else {
77
+ await ctx.db.unsafe.insert("WaitlistStat", { count: total, updatedAt: now });
78
+ }
79
+
80
+ return { ok: true, alreadyJoined: false };
81
+ },
82
+ });
@@ -0,0 +1,75 @@
1
+ import { query } from "@pylonsync/functions";
2
+ import { emailMatchesOwner } from "../lib/owner";
3
+ import type { SignupRow, WaitlistStatsResult } from "../lib/stats";
4
+
5
+ // waitlistStats — the owner's view of the raw signups, INCLUDING emails. This
6
+ // is the one function allowed to return PII, so it's gated hard: only the
7
+ // configured owner (PYLON_OWNER_EMAIL) gets data; anyone else is denied.
8
+ //
9
+ // The dashboard calls this with `callFn` and re-fetches whenever the live,
10
+ // public WaitlistStat count ticks — so the total / chart / list stay current as
11
+ // people join, while the emails themselves never travel over entity sync.
12
+ //
13
+ // `auth: "user"` means an anonymous caller never reaches the handler; the
14
+ // extra owner check is what stops a *different* signed-in user from reading the
15
+ // list. Functions bypass the Signup read policy, which is exactly why the gate
16
+ // has to live here.
17
+ function ymd(iso: string): string {
18
+ // Bucket by calendar day in UTC. `toISOString()` is always YYYY-MM-DDT…Z.
19
+ return new Date(iso).toISOString().slice(0, 10);
20
+ }
21
+
22
+ export default query({
23
+ auth: "user",
24
+ async handler(ctx): Promise<WaitlistStatsResult> {
25
+ // AuthInfo carries the userId, not the email — resolve the caller's email
26
+ // from their own User row (the User self-read policy allows that), then
27
+ // check it against the configured owner. A non-owner gets `authorized:
28
+ // false` and NOTHING else — no count, no emails. (We return a flag instead
29
+ // of throwing: a query has no `ctx.error`, and a bare throw reaches the
30
+ // client as a generic, message-stripped HANDLER_ERROR.)
31
+ const me = await ctx.db.get("User", ctx.auth.userId);
32
+ const email = (me?.email as string | undefined) ?? null;
33
+ if (!emailMatchesOwner(email, ctx.env.PYLON_OWNER_EMAIL)) {
34
+ return { authorized: false };
35
+ }
36
+
37
+ // Signup denies all client reads; this returns every row (owner-only), so
38
+ // it goes through the intentional cross-user read surface.
39
+ const rows = (await ctx.db.unsafe.list("Signup")) as unknown as SignupRow[];
40
+
41
+ const signups = rows
42
+ .map((r) => ({ id: r.id, email: r.email, createdAt: r.createdAt }))
43
+ .sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
44
+
45
+ // Daily buckets for the last 30 days, zero-filled so the chart is
46
+ // continuous even on days with no signups.
47
+ const byDay = new Map<string, number>();
48
+ for (const r of rows) {
49
+ const key = ymd(r.createdAt);
50
+ byDay.set(key, (byDay.get(key) ?? 0) + 1);
51
+ }
52
+ const now = new Date();
53
+ const todayKey = now.toISOString().slice(0, 10);
54
+ const daily: { date: string; count: number }[] = [];
55
+ for (let i = 29; i >= 0; i--) {
56
+ const d = new Date(now);
57
+ d.setUTCDate(d.getUTCDate() - i);
58
+ const key = d.toISOString().slice(0, 10);
59
+ daily.push({ date: key, count: byDay.get(key) ?? 0 });
60
+ }
61
+
62
+ const sevenDaysAgo = new Date(now);
63
+ sevenDaysAgo.setUTCDate(sevenDaysAgo.getUTCDate() - 7);
64
+ const last7 = rows.filter((r) => new Date(r.createdAt) >= sevenDaysAgo).length;
65
+
66
+ return {
67
+ authorized: true,
68
+ total: rows.length,
69
+ today: byDay.get(todayKey) ?? 0,
70
+ last7,
71
+ daily,
72
+ signups,
73
+ };
74
+ },
75
+ });
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .pylon/
3
+ pylon.manifest.json
4
+ pylon.client.ts
5
+ web/dist/
6
+ *.db
7
+ *.db-*
8
+ .env
9
+ .env.local
10
+ .DS_Store
@@ -0,0 +1,26 @@
1
+ // Who owns this waitlist? A waitlist is single-tenant — one business, one
2
+ // owner — so ownership is just "the email the owner signs in with", configured
3
+ // once via the PYLON_OWNER_EMAIL env var. The owner-only `waitlistStats`
4
+ // function reads that env (via `ctx.env`) and compares it here.
5
+ //
6
+ // Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
7
+ // dashboard stays locked. That's deliberate — an unset owner on a public site
8
+ // must not mean "everyone can read the signups". Set it in .env (see
9
+ // .env.example) before signing in.
10
+
11
+ export function normalizeOwner(raw: string | null | undefined): string | null {
12
+ const v = raw?.trim().toLowerCase();
13
+ return v && v.length > 0 ? v : null;
14
+ }
15
+
16
+ // Pure comparator — the caller supplies the configured owner value (from
17
+ // `ctx.env.PYLON_OWNER_EMAIL`), so the rule lives in one place and stays
18
+ // testable without reaching for the environment here.
19
+ export function emailMatchesOwner(
20
+ email: string | null | undefined,
21
+ ownerRaw: string | null | undefined,
22
+ ): boolean {
23
+ const owner = normalizeOwner(ownerRaw);
24
+ if (!owner) return false;
25
+ return (email ?? "").trim().toLowerCase() === owner;
26
+ }