@pylonsync/create-pylon 0.3.332 → 0.3.334

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 (61) hide show
  1. package/package.json +1 -1
  2. package/templates/ARCHETYPES.md +72 -74
  3. package/templates/_root/AGENTS.md +16 -16
  4. package/templates/_root/README.md +3 -3
  5. package/templates/agency/AGENTS.md +30 -30
  6. package/templates/agency/CLAUDE.md +3 -3
  7. package/templates/agency/README.md +13 -14
  8. package/templates/agency/lib/site.config.ts +9 -10
  9. package/templates/ai-chat/AGENTS.md +30 -30
  10. package/templates/ai-chat/CLAUDE.md +3 -3
  11. package/templates/ai-chat/README.md +10 -11
  12. package/templates/ai-chat/lib/site.config.ts +3 -4
  13. package/templates/ai-studio/AGENTS.md +30 -30
  14. package/templates/ai-studio/CLAUDE.md +3 -3
  15. package/templates/ai-studio/README.md +11 -12
  16. package/templates/ai-studio/lib/site.config.ts +5 -6
  17. package/templates/barebones/AGENTS.md +30 -30
  18. package/templates/barebones/CLAUDE.md +3 -3
  19. package/templates/barebones/README.md +5 -6
  20. package/templates/chat/AGENTS.md +30 -30
  21. package/templates/chat/CLAUDE.md +3 -3
  22. package/templates/chat/README.md +4 -6
  23. package/templates/consumer/AGENTS.md +30 -30
  24. package/templates/consumer/CLAUDE.md +3 -3
  25. package/templates/consumer/README.md +5 -6
  26. package/templates/creator/AGENTS.md +30 -30
  27. package/templates/creator/CLAUDE.md +3 -3
  28. package/templates/creator/README.md +11 -13
  29. package/templates/creator/lib/site.config.ts +6 -8
  30. package/templates/default/AGENTS.md +30 -30
  31. package/templates/default/CLAUDE.md +3 -3
  32. package/templates/default/README.md +8 -10
  33. package/templates/default/app/auth-shell.tsx +2 -2
  34. package/templates/default/lib/site.config.ts +26 -29
  35. package/templates/directory/AGENTS.md +30 -30
  36. package/templates/directory/CLAUDE.md +3 -3
  37. package/templates/directory/README.md +10 -14
  38. package/templates/directory/lib/site.config.ts +7 -9
  39. package/templates/local-service/AGENTS.md +30 -30
  40. package/templates/local-service/CLAUDE.md +3 -3
  41. package/templates/local-service/README.md +12 -15
  42. package/templates/local-service/lib/site.config.ts +8 -9
  43. package/templates/marketplace/AGENTS.md +30 -30
  44. package/templates/marketplace/CLAUDE.md +3 -3
  45. package/templates/marketplace/README.md +7 -8
  46. package/templates/marketplace/app/page.tsx +2 -2
  47. package/templates/restaurant/AGENTS.md +30 -30
  48. package/templates/restaurant/CLAUDE.md +3 -3
  49. package/templates/restaurant/README.md +10 -13
  50. package/templates/restaurant/lib/site.config.ts +7 -8
  51. package/templates/shop/AGENTS.md +30 -30
  52. package/templates/shop/CLAUDE.md +3 -3
  53. package/templates/shop/README.md +11 -13
  54. package/templates/shop/lib/site.config.ts +4 -5
  55. package/templates/todo/AGENTS.md +30 -30
  56. package/templates/todo/CLAUDE.md +3 -3
  57. package/templates/todo/README.md +4 -6
  58. package/templates/waitlist/AGENTS.md +30 -30
  59. package/templates/waitlist/CLAUDE.md +3 -3
  60. package/templates/waitlist/README.md +12 -16
  61. package/templates/waitlist/lib/site.config.ts +11 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.332",
3
+ "version": "0.3.334",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,26 +1,25 @@
1
1
  # Landing-page archetype templates
2
2
 
3
- Default templates Mast picks from when it builds a site for a new business. Each
4
- is a real `create-pylon` template (landing + dashboard + a live backend), so
5
- they're useful to every Pylon user too Mast just adds a classifier that maps a
6
- business description to one of these and fills its config.
3
+ Mast selects one of these `create-pylon` templates when it builds a site for a
4
+ new business. Each template includes a landing page, dashboard, and live
5
+ backend. Mast maps the business description to a template and fills its config.
7
6
 
8
7
  ## Principles
9
- - **An archetype is a section structure + one real realtime feature, not just
10
- copy/colors.** Anyone can have an LLM emit a static landing page now. The edge
11
- is that the generated site has a live backend doing something. Every template
12
- ships with exactly one genuinely-realtime hook.
8
+
9
+ - **Each archetype defines a section structure and one realtime feature.** The
10
+ generated site includes a working backend, and every template exposes one
11
+ live interaction.
13
12
  - **Shared foundation.** All templates share the brand/colors/seo config base
14
13
  (see the default template's `lib/site.config.ts`) and the marketing component
15
14
  kit (`WRAP`, `Eyebrow`, `SectionHead`, `FeatureGrid`, `Shot`, etc.). An
16
15
  archetype adds its own sections, config slots, and backend entities.
17
- - **Customization = generate one `site.config.ts`** (plus optional seed rows for
18
- archetypes with lists, e.g. services/menu). Same contract as the default.
16
+ - **Customization happens in one `site.config.ts`.** Archetypes with lists,
17
+ such as services or menus, may also include seed rows. This matches the
18
+ default template's contract.
19
19
  - **Privacy is part of the spec.** Landing pages are public. Any entity holding
20
- visitor PII (emails, phones) must deny client reads writes go through a
21
- server action, and only aggregates (a count) or non-PII fields (busy time
22
- slots) are exposed to the page. Never let a marketing site leak its own
23
- customers' emails.
20
+ visitor PII (emails, phones) must deny client reads. Writes go through a
21
+ server action. Pages may expose aggregates such as counts or non-PII fields
22
+ such as busy time slots, but never customer emails.
24
23
 
25
24
  ## Shared config base
26
25
  ```ts
@@ -40,27 +39,26 @@ Input: the business description. Output:
40
39
  seed?: { services?: [...]; menu?: [...] } // list rows for archetypes that have them
41
40
  }
42
41
  ```
43
- Ambiguous or clearly pre-revenue `waitlist` (safe universal default). Known
44
- operating business the matching archetype.
42
+ Use `waitlist` for ambiguous or pre-revenue businesses. Use the matching
43
+ archetype for a known operating business.
45
44
 
46
45
  ---
47
46
 
48
- # 1. `waitlist` pre-launch / coming-soon ★ build first (dogfood target)
47
+ # 1. `waitlist`: pre-launch or coming soon
49
48
 
50
49
  **Who:** validating demand, "I just started," pre-revenue, or fallback when the
51
50
  business type is unclear.
52
51
 
53
52
  **Sections (single page):**
54
- 1. Hero badge ("Coming soon"), headline, subcopy, **email capture form + live
53
+ 1. Hero: badge ("Coming soon"), headline, subcopy, **email capture form + live
55
54
  signup counter** ("1,247 people waiting").
56
- 2. Value props 3 items (what it is / who it's for / why now).
57
- 3. Social proof optional logos or quotes.
58
- 4. FAQ optional, short.
55
+ 2. Value props: 3 items (what it is / who it's for / why now).
56
+ 3. Social proof: optional logos or quotes.
57
+ 4. FAQ: optional, short.
59
58
  5. Footer.
60
59
 
61
- **Realtime feature:** the signup counter ticks up for everyone with the page
62
- open. Open two tabs, submit in one, the other increments without refresh. That's
63
- the whole "it's a real live app" proof.
60
+ **Realtime feature:** the signup counter updates on every open page. Submit in
61
+ one tab and the other increments without a refresh.
64
62
 
65
63
  **Config (`WaitlistConfig`):**
66
64
  ```ts
@@ -80,9 +78,9 @@ Signup = entity("Signup", {
80
78
  createdAt: field.datetime(),
81
79
  }, { indexes: [{ name: "by_email", fields: ["email"], unique: true }] });
82
80
  ```
83
- - `joinWaitlist({ email })` public action: validate + lowercase + dedupe +
81
+ - `joinWaitlist({ email })`: public action: validate + lowercase + dedupe +
84
82
  rate-limit, insert. Returns `{ ok, alreadyJoined? }`. Never returns other rows.
85
- - `waitlistCount()` query returning `count(Signup)`; the page live-subscribes
83
+ - `waitlistCount()`: query returning `count(Signup)`; the page live-subscribes
86
84
  so the number updates on every insert.
87
85
  - **Privacy:** `Signup` policy denies ALL client read/write (like
88
86
  `cliAuthCodePolicy`). The page only ever sees the count, never emails.
@@ -92,25 +90,25 @@ export.
92
90
 
93
91
  ---
94
92
 
95
- # 2. `local-service` appointment businesses (booking) ★ build second
93
+ # 2. `local-service`: appointment businesses
96
94
 
97
95
  **Who:** salon, barber, trades (plumber/electrician/cleaner), trainer, clinic,
98
96
  studio, tutor — anyone who sells time slots.
99
97
 
100
98
  **Sections:**
101
- 1. Hero business name, tagline, **"Book now"** CTA, hero image, quick facts
99
+ 1. Hero: business name, tagline, **"Book now"** CTA, hero image, quick facts
102
100
  (hours / area / phone).
103
- 2. Services + prices list/grid: name, duration, price.
104
- 3. **Booking** pick service → pick a time from **live availability** → name /
101
+ 2. Services + prices: list/grid: name, duration, price.
102
+ 3. **Booking**: pick service → pick a time from **live availability** → name /
105
103
  email / phone → confirm.
106
104
  4. Reviews.
107
- 5. Hours + location address, map embed, hours, contact.
108
- 6. Gallery optional.
109
- 7. FAQ optional. Footer.
105
+ 5. Hours + location: address, map embed, hours, contact.
106
+ 6. Gallery: optional.
107
+ 7. FAQ: optional. Footer.
110
108
 
111
109
  **Realtime feature:** live slot availability. The time picker subscribes to the
112
- day's bookings; the moment someone books a slot it greys out for everyone else
113
- no double-booking. Server also re-checks at insert time to close the race.
110
+ day's bookings, so a booked slot greys out for everyone else. The server also
111
+ rechecks availability at insert time to prevent a race.
114
112
 
115
113
  **Config (`LocalServiceConfig`):**
116
114
  ```ts
@@ -143,10 +141,10 @@ Booking = entity("Booking", {
143
141
  createdAt: field.datetime();
144
142
  }, { indexes: [{ name: "by_start", fields: ["startsAt"] }] });
145
143
  ```
146
- - `createBooking({ serviceId, startsAt, customer })` public action: server-side
144
+ - `createBooking({ serviceId, startsAt, customer })`: public action: server-side
147
145
  re-check that the slot is still free (overlap query) before insert, to close
148
146
  the race the live UI already mostly prevents.
149
- - `bookedSlotsForRange({ from, to })` query returning only `{ startsAt, endsAt }`
147
+ - `bookedSlotsForRange({ from, to })`: query returning only `{ startsAt, endsAt }`
150
148
  (NO customer fields); the picker subscribes and computes free slots = hours −
151
149
  booked. **Privacy:** `Booking` denies client read of full rows; only this
152
150
  PII-stripped projection is exposed. `Service` is public-read (it's menu data).
@@ -156,11 +154,11 @@ weekly hours.
156
154
 
157
155
  ---
158
156
 
159
- # 3. `saas` app / tool / digital product (≈ the current default, renamed)
157
+ # 3. `saas`: app, tool, or digital product
160
158
 
161
159
  **Who:** software founders ("I'm building an app").
162
160
  **Sections:** hero + dashboard preview, logo cloud, feature sections
163
- (products), pricing tiers, testimonials, FAQ. (Already built this is the
161
+ (products), pricing tiers, testimonials, FAQ. (Already built; this is the
164
162
  refactored `default` template; rename `default` → `saas`, keep `default` as an
165
163
  alias.)
166
164
  **Realtime feature:** the live dashboard behind "Open dashboard" (the workspace
@@ -169,7 +167,7 @@ itself).
169
167
 
170
168
  ---
171
169
 
172
- # 4. `restaurant` food & hospitality
170
+ # 4. `restaurant`: food and hospitality
173
171
 
174
172
  **Who:** restaurant, cafe, bar, food truck, bakery.
175
173
  **Sections:** hero (name + "Reserve" / "Order"), **menu** (sections → items with
@@ -184,7 +182,7 @@ tags? }[] }[] }`, `reservations` (reuse booking shape), `location`, `hours`.
184
182
 
185
183
  ---
186
184
 
187
- # 5. `shop` DTC product / small store
185
+ # 5. `shop`: DTC product or small store
188
186
 
189
187
  **Who:** product brands, makers, single-product or small-catalog sellers.
190
188
  **Sections:** hero, featured products / product grid, value props, reviews,
@@ -198,14 +196,13 @@ lines share an `orderGroupId`).
198
196
  **Checkout:** real **Stripe Checkout** via `@pylonsync/stripe`'s `stripeRequest`
199
197
  + `verifyStripeSignature` (one-time `price_data` line items; signed webhook at
200
198
  `/api/webhooks/stripeWebhook` settles paid / releases held stock on expiry).
201
- Stock is held under a per-product advisory lock at checkout so the cart can't
202
- oversell. **Graceful degradation:** with no `STRIPE_SECRET_KEY` the order is held
203
- as `reserved` for the owner to follow up the store boots and demos live
204
- inventory with zero config.
199
+ Stock is held under a per-product advisory lock at checkout so the cart cannot
200
+ oversell. Without `STRIPE_SECRET_KEY`, the order remains `reserved` for the
201
+ owner to follow up. This keeps the live-inventory demo usable without Stripe.
205
202
 
206
203
  ---
207
204
 
208
- # 6. `creator` personal brand / coach / consultant
205
+ # 6. `creator`: personal brand, coach, or consultant
209
206
 
210
207
  **Who:** solo creators, coaches, consultants, freelancers, newsletter authors.
211
208
  **Sections:** hero (you), about, offerings/services, portfolio or testimonials,
@@ -220,14 +217,14 @@ local-service).
220
217
 
221
218
  ---
222
219
 
223
- # 7. `agency` design / dev / marketing studio
220
+ # 7. `agency`: design, development, or marketing studio
224
221
 
225
222
  **Who:** boutique product/design/dev/marketing studios that take on a limited
226
223
  number of clients at once.
227
224
  **Sections:** hero (+ live availability pill), logo cloud, services, **work /
228
225
  case-study grid** (marked project-shot placeholders), process, **team** (marked
229
226
  headshot placeholders), testimonials, **project inquiry form** (#contact), footer.
230
- **Realtime feature:** scarcity — a public `Capacity` row holds the booking window
227
+ **Realtime feature:** a public `Capacity` row holds the booking window
231
228
  + open project slots; the hero pill (`db.useQuery("Capacity")`) shows "N slots
232
229
  open" live, and the owner booking a lead from the dashboard decrements it for
233
230
  everyone instantly (same shape as `shop` inventory).
@@ -236,12 +233,12 @@ everyone instantly (same shape as `shop` inventory).
236
233
  `contact: { projectTypes[]; budgets[]; confirmationMessage }`.
237
234
  **Entities:** `Inquiry` (PII deny-all: name/email/company/budget/message + status),
238
235
  `Capacity` (public-read single row, live openSlots), `User`.
239
- **Placeholders:** hero photo, each case-study project shot, each team headshot —
240
- all clearly marked via the shared `ImagePlaceholder`.
236
+ **Placeholders:** the hero photo, each case-study project shot, and each team
237
+ headshot use the shared `ImagePlaceholder`.
241
238
 
242
239
  ---
243
240
 
244
- # 8. `marketplace` two-sided buy/sell platform (ported from examples/market)
241
+ # 8. `marketplace`: two-sided buy and sell platform
245
242
 
246
243
  **Who:** local/vertical marketplaces, classifieds, gear resale, any buyer↔seller
247
244
  platform.
@@ -254,20 +251,20 @@ private `Watch`). List in one tab → it appears in another instantly.
254
251
  category, condition, status, seed-gradient photo), `Offer` (`buyerId:
255
252
  field.owner()`, amount, status), `Watch` (private). Public-read listings/offers,
256
253
  owner-scoped writes; accept-marks-sold-and-declines-siblings in
257
- `respondToOffer`. **Multi-user** (email/password, no single owner) — unlike the
258
- single-tenant archetypes above.
259
- **Note:** ported from `examples/market` (not config-driven — rebrand in
260
- `app/layout.tsx` + `functions/seedMarket.ts`). Listing photos are generated
254
+ `respondToOffer`. It is multi-user through email/password, unlike the
255
+ single-owner archetypes above.
256
+ **Note:** ported from `examples/market`. Rebrand it in
257
+ `app/layout.tsx` and `functions/seedMarket.ts`. Listing photos are generated
261
258
  gradients; swap for real `<img>` + `/api/files` upload for production.
262
259
 
263
260
  ---
264
261
 
265
- # 9. `directory` curated, searchable listing site
262
+ # 9. `directory`: curated, searchable listing site
266
263
 
267
264
  **Who:** "best X" lists, tool/company/local directories, awesome-lists with a UI.
268
265
  **Sections:** hero + a live faceted-search browse (`#browse`), a `/submit` page,
269
266
  a `/dashboard` moderation queue.
270
- **Realtime feature:** the showcase for Pylon **FTS + facets** `db.useSearch`
267
+ **Realtime feature:** Pylon FTS and facets. `db.useSearch`
271
268
  re-runs on every keystroke AND on every write, so it doubles as the live layer;
272
269
  public `upvote` bumps `Listing.votes` and the count ticks up across all tabs.
273
270
  **Entities:** `Listing` (public-read, no PII; `search: { text, facets, sortable }`
@@ -281,16 +278,16 @@ examples/store/client/Catalog.tsx).
281
278
 
282
279
  ---
283
280
 
284
- # 10. `ai-chat` streaming AI assistant
281
+ # 10. `ai-chat`: streaming AI assistant
285
282
 
286
283
  **Who:** any LLM chat product / internal assistant.
287
- **Sections:** a full-screen chat conversation sidebar + streaming thread +
284
+ **Sections:** a full-screen chat with a conversation sidebar, streaming thread,
288
285
  composer (`/`), optional `/login`.
289
286
  **Realtime feature:** token streaming from the built-in `POST /api/ai/stream`
290
287
  (SSE; the API key never leaves the server), PLUS owner-scoped `Conversation` +
291
288
  `Message` synced across the user's tabs via `db.useQuery`.
292
289
  **Entities:** `Conversation` + `Message` (both `userId: field.owner()`,
293
- owner-scoped read/write private per user; messages written with optimistic
290
+ owner-scoped read/write, private per user; messages written with optimistic
294
291
  `db.insert`, no custom functions) + `User`. Multi-user (guest or signed-in).
295
292
  **Config:** `PYLON_AI_PROVIDER` + `PYLON_AI_API_KEY` + `PYLON_AI_MODEL` enable
296
293
  replies; without them the app boots and shows a friendly "configure AI" notice.
@@ -299,34 +296,35 @@ replies; without them the app boots and shows a friendly "configure AI" notice.
299
296
 
300
297
  ---
301
298
 
302
- # 11. `ai-studio` generative media studio (image / audio / video)
299
+ # 11. `ai-studio`: generative media studio
303
300
 
304
301
  **Who:** AI image/audio/video generators, creative tools.
305
302
  **Sections:** a prompt bar + medium selector over a live gallery (`/`), optional
306
303
  `/login`.
307
- **Realtime feature:** the generation gallery the `generate` action inserts a
304
+ **Realtime feature:** the generation gallery. The `generate` action inserts a
308
305
  `pending` Generation (card appears instantly), runs the provider call
309
306
  server-side, then flips the row to `done`/`failed`; `db.useQuery` syncs that to
310
307
  every open tab so the card updates live.
311
- **Entities:** `Generation` (owner-scoped READ, `allowInsert:"false"` only the
308
+ **Entities:** `Generation` (owner-scoped READ, `allowInsert:"false"`; only the
312
309
  server pipeline writes it) + `User`. Multi-user (guest or signed-in).
313
310
  **Functions:** `generate` (public action) brackets the provider call with
314
- internal `_createGeneration` / `_finishGeneration` mutations. Image OpenAI
315
- Images, audio OpenAI TTS (both when `OPENAI_API_KEY` is set), video a labeled
316
- extension point. **No key a clearly-labeled placeholder** (SVG for image) so
317
- the flow + gallery work with zero config. Key + media stay server-side.
311
+ internal `_createGeneration` / `_finishGeneration` mutations. Image uses OpenAI
312
+ Images, audio uses OpenAI TTS when `OPENAI_API_KEY` is set, and video is a
313
+ labeled extension point. Without a key, the app returns a clearly labeled
314
+ placeholder so the flow and gallery remain testable. Keys and media stay
315
+ server-side.
318
316
  **Note:** image uses the provider's hosted URL (small to sync, ~1h TTL);
319
317
  persist via `/api/files` for permanence. lib/studio.ts holds the placeholder gen.
320
318
 
321
319
  ---
322
320
 
323
321
  ## Build order
324
- 1. **`waitlist`** smallest, proves the whole pipeline, already the dogfood
325
- target. (`Signup` + counter + `joinWaitlist`.)
326
- 2. **`local-service`** biggest SMB market; booking is the strongest realtime
327
- demo. (`Service` + `Booking` + live availability.)
328
- 3. **`saas`** rename/finish the refactored default.
329
- 4. **`restaurant`**, **`shop`**, **`creator`** — breadth; each heavily reuses the
322
+ 1. **`waitlist`:** smallest template and current dogfood target (`Signup`,
323
+ counter, and `joinWaitlist`).
324
+ 2. **`local-service`:** booking template with `Service`, `Booking`, and live
325
+ availability.
326
+ 3. **`saas`:** rename and finish the refactored default.
327
+ 4. **`restaurant`**, **`shop`**, **`creator`:** each heavily reuses the
330
328
  `booking`/`signup` engines above, so they're mostly config + a couple of
331
329
  bespoke sections.
332
330
 
@@ -336,4 +334,4 @@ persist via `/api/files` for permanence. lib/studio.ts holds the placeholder gen
336
334
  creator (call booking).
337
335
  - `Product` grid + inventory → shop.
338
336
  - Marketing component kit + BaseConfig → all.
339
- Two real backend engines (signup, booking) + one (product) cover every archetype.
337
+ The signup, booking, and product engines cover every archetype above.
@@ -1,35 +1,35 @@
1
- # AGENTS.md working in a Pylon project
1
+ # AGENTS.md: working in a Pylon project
2
2
 
3
- Operating rules for a coding agent in this Pylon app. You — the agent — are a first-class user of Pylon: one Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from one process on one port, so you build, run, and ship a whole app without wiring services together or leaving the codebase. This is production infrastructure, not a sandbox — real auth, SQLite or Postgres, row-level policies, jobs, search, and one-command deploy — so build like it ships. You declare entities, policies, and server functions in TypeScript; the binary does the rest. The full API reference is the **llms-full.txt** at https://docs.pylonsync.com/llms-full.txt — read it before guessing an API name.
3
+ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from one Rust process on one port. Treat this app as production infrastructure: it supports real auth, SQLite or Postgres, row-level policies, jobs, search, and one-command deploys. Declare entities, policies, and server functions in TypeScript; the binary handles the runtime. Read the full API reference at https://docs.pylonsync.com/llms-full.txt before guessing an API name.
4
4
 
5
5
  ## Directory conventions
6
6
 
7
7
  **Unified SSR app:**
8
- - `app.ts` data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
9
- - `app/` file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
10
- - `app/globals.css` Tailwind v4 entrypoint (auto-compiled and injected).
11
- - `functions/` server functions, one per file, `default`-exported.
12
- - `.pylon/` local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
8
+ - `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
9
+ - `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
10
+ - `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
11
+ - `functions/`: server functions, one per file, `default`-exported.
12
+ - `.pylon/`: local development state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
13
13
 
14
- **Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated do not hand-edit.
14
+ **Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated; do not hand-edit them.
15
15
 
16
16
  ## The core authoring loop
17
17
 
18
- 1. **Define an entity** `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
19
- 2. **Write a policy** `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
20
- 3. **Author a function** in `functions/<name>.ts` `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
21
- 4. **Read it on the client** `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
18
+ 1. **Define an entity:** `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update; use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
19
+ 2. **Write a policy:** `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). Omitted actions deny by default. `pylon lint` flags wide-open development policies such as `allow*: "true"`; tighten them before shipping.
20
+ 3. **Author a function** in `functions/<name>.ts`: `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
21
+ 4. **Read it on the client:** `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
22
22
 
23
23
  ## Key gotchas
24
24
 
25
- - **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access enforce trust with `ctx.auth` checks inside the handler, not policies.
25
+ - **Policies deny by default; server functions bypass them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full database access, so enforce trust with `ctx.auth` checks inside the handler.
26
26
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
27
- - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
28
- - **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` use `ctx.runQuery` / `ctx.runMutation`.
27
+ - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` or `<Suspense>`. The HTTP head commits when the shell is ready. Status, headers, and cookies set from a suspended subtree are lost, and `redirect` or `notFound` thrown below a Suspense boundary are swallowed.
28
+ - **`ctx.llm` and `ctx.connections` are available on mutations and actions, not queries.** An `action` has no direct `ctx.db`; use `ctx.runQuery` or `ctx.runMutation`.
29
29
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
30
30
  - **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
31
31
 
32
- ## Use the CLI — don't guess
32
+ ## Use the CLI
33
33
 
34
34
  | Need | Command |
35
35
  |---|---|
@@ -25,10 +25,10 @@ bun install # or pnpm install / yarn / npm install
25
25
  bun run dev
26
26
  ```
27
27
 
28
- - **api** on http://localhost:4321 Pylon control plane
29
- - **web** on http://localhost:3000 Next.js (if scaffolded)
28
+ - **api:** http://localhost:4321, Pylon control plane
29
+ - **web:** http://localhost:3000, Next.js (if scaffolded)
30
30
  - **expo** runs Metro on a separate port (if scaffolded)
31
- - **mobile** lives in `apps/mobile/` open in Xcode or run `swift run`
31
+ - **mobile** lives in `apps/mobile/`; open it in Xcode or run `swift run`
32
32
 
33
33
  ## What to do next
34
34
 
@@ -1,47 +1,47 @@
1
- # AGENTS.md working in a Pylon project
1
+ # AGENTS.md: working in a Pylon project
2
2
 
3
- Operating rules for a coding agent in this Pylon app. You — the agent — are a first-class user of Pylon: one Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from one process on one port, so you build, run, and ship a whole app without wiring services together or leaving the codebase. This is production infrastructure, not a sandbox — real auth, SQLite or Postgres, row-level policies, jobs, search, and one-command deploy — so build like it ships. You declare entities, policies, and server functions in TypeScript; the binary does the rest. The full API reference is the **llms-full.txt** at https://docs.pylonsync.com/llms-full.txt — read it before guessing an API name.
3
+ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from one Rust process on one port. Treat this app as production infrastructure: it supports real auth, SQLite or Postgres, row-level policies, jobs, search, and one-command deploys. Declare entities, policies, and server functions in TypeScript; the binary handles the runtime. Read the full API reference at https://docs.pylonsync.com/llms-full.txt before guessing an API name.
4
4
 
5
5
  ## Directory conventions
6
6
 
7
7
  **Unified SSR app:**
8
- - `app.ts` data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
9
- - `app/` file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
10
- - `app/globals.css` Tailwind v4 entrypoint (auto-compiled and injected).
11
- - `functions/` server functions, one per file, `default`-exported.
12
- - `.pylon/` local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
8
+ - `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
9
+ - `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
10
+ - `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
11
+ - `functions/`: server functions, one per file, `default`-exported.
12
+ - `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
13
13
 
14
- **Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated do not hand-edit.
14
+ **Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated; do not hand-edit.
15
15
 
16
16
  ## The core authoring loop
17
17
 
18
- 1. **Define an entity** `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
19
- 2. **Write a policy** `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
20
- 3. **Author a function** in `functions/<name>.ts` `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
21
- 4. **Read it on the client** `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
18
+ 1. **Define an entity:** `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update; use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
19
+ 2. **Write a policy:** `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). Omitted actions deny by default. `pylon lint` flags wide-open development policies such as `allow*: "true"`; tighten them before shipping.
20
+ 3. **Author a function** in `functions/<name>.ts`: `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
21
+ 4. **Read it on the client:** `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
22
22
 
23
23
  ## Key gotchas
24
24
 
25
- - **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access enforce trust with `ctx.auth` checks inside the handler, not policies.
26
- - **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` they're server-only and stripped from hydration, so reading them in the render would mismatch.
27
- - **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
28
- - **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet use urlencoded forms + `/api/files`.
29
- - **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
30
- - **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
31
- - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
32
- - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
33
- - **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"` `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
25
+ - **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access; enforce trust with `ctx.auth` checks inside the handler, not policies.
26
+ - **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps`; they're server-only and stripped from hydration, so reading them in the render would mismatch.
27
+ - **Anonymous output caching is opt-in and conditional.** `export const revalidate = 60` makes a page CDN-cacheable (`public, s-maxage=60`) only when the render is auth-independent: it must not read `props.auth`, set a cookie, or run with strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. When any condition fails, the page is `no-cache`. Eligible renders also use the origin disk cache at `.pylon/.cache/ssr`: a cookie-less GET without a query string is served from disk for the TTL and rerendered when stale. The cache is namespaced per deploy, cleared by each build, disabled in `pylon dev`, and invalidated by the `revalidate` TTL or the next deploy.
28
+ - **No-JS forms use `route.ts` and `<Form>`.** Add `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` from `@pylonsync/react` with plain `<input name=...>`. It uses native POST handler redirect without JavaScript and no-reload enhancement with JavaScript. The handler's `db` is read+write under the mutation trust model, so gate it on `auth`. CSRF protection is automatic through the Origin gate and SameSite=Lax. Multipart uploads are not supported yet; use URL-encoded forms and `/api/files`.
29
+ - **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends; a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
30
+ - **`export const streaming = true` streams a page's inner `<Suspense>` boundaries.** Without it or a `loading.tsx`, the page is buffered until all suspended children resolve. With it, the shell and fallbacks flush immediately, then each boundary streams its content. Streaming commits the HTTP head before suspended subtrees finish, so the page is never CDN- or disk-cacheable; do not combine it with `export const revalidate`. Calls to `response.setStatus`, `setCookie`, `redirect`, or `notFound` only take effect during the synchronous shell render. A call from a suspended subtree is dropped and logged. An error from a deep `<Suspense>` child resolves through the nearest `error.tsx` at HTTP 200 rather than 5xx. Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
31
+ - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
32
+ - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
33
+ - **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
34
34
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
35
- - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
36
- - **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` use `ctx.runQuery` / `ctx.runMutation`.
35
+ - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
36
+ - **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
37
37
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
38
- - **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`. Deferred (one-shot) execution is `ctx.scheduler.runAfter/runAt/cancel`. Recurring work is a **cron**: `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })` (import `cron` from `@pylonsync/sdk`) it fires the named function (make it `internal: true`) on the schedule; the function runs with anonymous auth its own `ctx.db.*` is server-side (not policy-gated), so write directly; only `ctx.auth.elevate({ admin: true, reason: "..." })` (reason mandatory) to chain an `internal: true` function via `ctx.scheduler`.
38
+ - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
39
39
 
40
40
  ## Testing
41
41
 
42
- `pylon test` discovers every `*.test.ts` / `*.test.tsx` file under `tests/` (or `functions/`) and runs it with **Bun's test runner** (`import { test, expect } from "bun:test"`). Run the suite with `pylon test` (or `npm test`); filter with `pylon test <substring>`. This template ships `bunfig.toml` + `tests/setup.ts` (registers happy-dom) so component tests render out of the box, plus starter tests under `tests/` replace them with your own.
42
+ `pylon test` discovers every `*.test.ts` / `*.test.tsx` file under `tests/` (or `functions/`) and runs it with **Bun's test runner** (`import { test, expect } from "bun:test"`). Run the suite with `pylon test` (or `npm test`); filter with `pylon test <substring>`. This template ships `bunfig.toml` + `tests/setup.ts` (registers happy-dom) so component tests render out of the box, plus starter tests under `tests/`; replace them with your own.
43
43
 
44
- **Tier 1 pure logic (reach for this first).** Keep the decisions that matter — access/plan gating, pricing, credit math, validation, formatting in pure functions in `lib/`, and test those exhaustively. No server, instant, and it's where the real bugs live. Keep your `query`/`mutation`/`action` handlers as thin wrappers around them, so the logic is testable without a running app.
44
+ **Tier 1: pure logic (start here).** Keep access and plan gating, pricing, credit math, validation, and formatting in pure functions under `lib/`, and test them exhaustively. These tests need no server and run instantly. Keep `query`, `mutation`, and `action` handlers as thin wrappers so their decision logic remains testable without a running app.
45
45
 
46
46
  ```ts
47
47
  import { expect, test } from "bun:test";
@@ -52,7 +52,7 @@ test("unknown slug → undefined", () => {
52
52
  });
53
53
  ```
54
54
 
55
- **Tier 2 React components.** `@testing-library/react` + happy-dom are already wired (`tests/setup.ts`). Render and assert. The template uses the classic JSX transform, so add `import React from "react"` in `.tsx` tests. For a component that reads Pylon data hooks, **mock the boundary**, then dynamic-`import` the component so the mock is in place first:
55
+ **Tier 2: React components.** `@testing-library/react` and happy-dom are already wired through `tests/setup.ts`. Render and assert. The template uses the classic JSX transform, so add `import React from "react"` in `.tsx` tests. For a component that reads Pylon data hooks, mock the boundary, then dynamically import the component so the mock is in place first:
56
56
 
57
57
  ```tsx
58
58
  import { test, expect, mock } from "bun:test";
@@ -70,7 +70,7 @@ test("renders orgs from the query", () => {
70
70
  });
71
71
  ```
72
72
 
73
- **Tier 3 functions over HTTP (only when Tier 1 can't cover it).** A handler's full behavior (policies, `ctx.db`, auth) lives in the running app. Start `pylon dev` in another terminal and call the API; `resetDb()` from `@pylonsync/functions` clears the in-memory DB between cases (no-ops if the server's down, refuses production).
73
+ **Tier 3: functions over HTTP.** Use this tier when pure logic tests cannot cover the behavior. A handler's policies, `ctx.db` calls, and auth run in the app. Start `pylon dev` in another terminal and call the API. `resetDb()` from `@pylonsync/functions` clears the in-memory database between cases; it does nothing when the server is down and refuses to run in production.
74
74
 
75
75
  ```ts
76
76
  import { afterEach, expect, test } from "bun:test";
@@ -90,9 +90,9 @@ test("createThing then read it back", async () => {
90
90
  });
91
91
  ```
92
92
 
93
- `pylon test:security` is a separate adversarial probe it hits a running app and reports auth/policy holes (run `pylon dev`, then `pylon test:security`).
93
+ `pylon test:security` is a separate adversarial probe; it hits a running app and reports auth/policy holes (run `pylon dev`, then `pylon test:security`).
94
94
 
95
- ## Use the CLI — don't guess
95
+ ## Use the CLI
96
96
 
97
97
  | Need | Command |
98
98
  |---|---|
@@ -1,7 +1,7 @@
1
1
  # CLAUDE.md
2
2
 
3
- Agent guidance for this Pylon project lives in **AGENTS.md** the cross-editor
4
- standard that Claude Code, Cursor, Codex, and others all read. It's imported
5
- below so Claude Code picks it up automatically:
3
+ Agent guidance for this Pylon project lives in **AGENTS.md**, the cross-editor
4
+ standard read by Claude Code, Cursor, Codex, and other agents. The import below
5
+ makes it available to Claude Code:
6
6
 
7
7
  @AGENTS.md