@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.
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +72 -74
- package/templates/_root/AGENTS.md +16 -16
- package/templates/_root/README.md +3 -3
- package/templates/agency/AGENTS.md +30 -30
- package/templates/agency/CLAUDE.md +3 -3
- package/templates/agency/README.md +13 -14
- package/templates/agency/lib/site.config.ts +9 -10
- package/templates/ai-chat/AGENTS.md +30 -30
- package/templates/ai-chat/CLAUDE.md +3 -3
- package/templates/ai-chat/README.md +10 -11
- package/templates/ai-chat/lib/site.config.ts +3 -4
- package/templates/ai-studio/AGENTS.md +30 -30
- package/templates/ai-studio/CLAUDE.md +3 -3
- package/templates/ai-studio/README.md +11 -12
- package/templates/ai-studio/lib/site.config.ts +5 -6
- package/templates/barebones/AGENTS.md +30 -30
- package/templates/barebones/CLAUDE.md +3 -3
- package/templates/barebones/README.md +5 -6
- package/templates/chat/AGENTS.md +30 -30
- package/templates/chat/CLAUDE.md +3 -3
- package/templates/chat/README.md +4 -6
- package/templates/consumer/AGENTS.md +30 -30
- package/templates/consumer/CLAUDE.md +3 -3
- package/templates/consumer/README.md +5 -6
- package/templates/creator/AGENTS.md +30 -30
- package/templates/creator/CLAUDE.md +3 -3
- package/templates/creator/README.md +11 -13
- package/templates/creator/lib/site.config.ts +6 -8
- package/templates/default/AGENTS.md +30 -30
- package/templates/default/CLAUDE.md +3 -3
- package/templates/default/README.md +8 -10
- package/templates/default/app/auth-shell.tsx +2 -2
- package/templates/default/lib/site.config.ts +26 -29
- package/templates/directory/AGENTS.md +30 -30
- package/templates/directory/CLAUDE.md +3 -3
- package/templates/directory/README.md +10 -14
- package/templates/directory/lib/site.config.ts +7 -9
- package/templates/local-service/AGENTS.md +30 -30
- package/templates/local-service/CLAUDE.md +3 -3
- package/templates/local-service/README.md +12 -15
- package/templates/local-service/lib/site.config.ts +8 -9
- package/templates/marketplace/AGENTS.md +30 -30
- package/templates/marketplace/CLAUDE.md +3 -3
- package/templates/marketplace/README.md +7 -8
- package/templates/marketplace/app/page.tsx +2 -2
- package/templates/restaurant/AGENTS.md +30 -30
- package/templates/restaurant/CLAUDE.md +3 -3
- package/templates/restaurant/README.md +10 -13
- package/templates/restaurant/lib/site.config.ts +7 -8
- package/templates/shop/AGENTS.md +30 -30
- package/templates/shop/CLAUDE.md +3 -3
- package/templates/shop/README.md +11 -13
- package/templates/shop/lib/site.config.ts +4 -5
- package/templates/todo/AGENTS.md +30 -30
- package/templates/todo/CLAUDE.md +3 -3
- package/templates/todo/README.md +4 -6
- package/templates/waitlist/AGENTS.md +30 -30
- package/templates/waitlist/CLAUDE.md +3 -3
- package/templates/waitlist/README.md +12 -16
- 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.
|
|
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"
|
package/templates/ARCHETYPES.md
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
# Landing-page archetype templates
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
18
|
-
|
|
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
|
|
21
|
-
server action
|
|
22
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
53
|
+
1. Hero: badge ("Coming soon"), headline, subcopy, **email capture form + live
|
|
55
54
|
signup counter** ("1,247 people waiting").
|
|
56
|
-
2. Value props
|
|
57
|
-
3. Social proof
|
|
58
|
-
4. FAQ
|
|
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
|
|
62
|
-
|
|
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 })
|
|
81
|
+
- `joinWaitlist({ email })`: public action: validate + lowercase + dedupe +
|
|
84
82
|
rate-limit, insert. Returns `{ ok, alreadyJoined? }`. Never returns other rows.
|
|
85
|
-
- `waitlistCount()
|
|
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
|
|
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
|
|
99
|
+
1. Hero: business name, tagline, **"Book now"** CTA, hero image, quick facts
|
|
102
100
|
(hours / area / phone).
|
|
103
|
-
2. Services + prices
|
|
104
|
-
3. **Booking
|
|
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
|
|
108
|
-
6. Gallery
|
|
109
|
-
7. FAQ
|
|
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
|
|
113
|
-
|
|
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 })
|
|
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 })
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
202
|
-
oversell.
|
|
203
|
-
|
|
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
|
|
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
|
|
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:**
|
|
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
|
|
240
|
-
|
|
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
|
|
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`.
|
|
258
|
-
single-
|
|
259
|
-
**Note:** ported from `examples/market
|
|
260
|
-
`app/layout.tsx`
|
|
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
|
|
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:**
|
|
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
|
|
281
|
+
# 10. `ai-chat`: streaming AI assistant
|
|
285
282
|
|
|
286
283
|
**Who:** any LLM chat product / internal assistant.
|
|
287
|
-
**Sections:** a full-screen chat
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
315
|
-
Images, audio
|
|
316
|
-
extension point.
|
|
317
|
-
the flow
|
|
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
|
|
325
|
-
|
|
326
|
-
2. **`local-service
|
|
327
|
-
|
|
328
|
-
3. **`saas
|
|
329
|
-
4. **`restaurant`**, **`shop`**, **`creator
|
|
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
|
-
|
|
337
|
+
The signup, booking, and product engines cover every archetype above.
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
# AGENTS.md
|
|
1
|
+
# AGENTS.md: working in a Pylon project
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
9
|
-
- `app
|
|
10
|
-
- `app/globals.css
|
|
11
|
-
- `functions
|
|
12
|
-
- `.pylon
|
|
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
|
|
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
|
|
19
|
-
2. **Write a policy
|
|
20
|
-
3. **Author a function** in `functions/<name>.ts
|
|
21
|
-
4. **Read it on the client
|
|
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
|
|
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`
|
|
28
|
-
- **`ctx.llm` and `ctx.connections` are on
|
|
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
|
|
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
|
|
29
|
-
- **web
|
|
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
|
|
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
|
|
1
|
+
# AGENTS.md: working in a Pylon project
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
9
|
-
- `app
|
|
10
|
-
- `app/globals.css
|
|
11
|
-
- `functions
|
|
12
|
-
- `.pylon
|
|
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
|
|
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
|
|
19
|
-
2. **Write a policy
|
|
20
|
-
3. **Author a function** in `functions/<name>.ts
|
|
21
|
-
4. **Read it on the client
|
|
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
|
|
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
|
|
27
|
-
- **Anonymous output caching is opt-in
|
|
28
|
-
- **No-JS forms use `route.ts`
|
|
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
|
|
30
|
-
- **`export const streaming = true` streams a page's
|
|
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 }`)
|
|
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
|
|
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"
|
|
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
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4
|
-
standard
|
|
5
|
-
|
|
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
|