@pylonsync/create-pylon 0.3.333 → 0.3.335

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
@@ -1,12 +1,11 @@
1
1
  # __APP_NAME__
2
2
 
3
- A booking site for an appointment business (salon, barber, trainer, clinic,
4
- trades…) built with [Pylon](https://pylonsync.com) a server-rendered
5
- marketing page with **live slot availability** and a private owner dashboard,
6
- all served from one binary on one port. No Next.js, no separate API server.
3
+ A booking site for salons, barbers, trainers, clinics, trades, and other
4
+ appointment businesses. [Pylon](https://pylonsync.com) serves the marketing
5
+ page, live slot availability, and private owner dashboard from one process.
7
6
 
8
- The realtime point: the time picker shows what's *actually* free, and a slot
9
- greys out for everyone the instant someone books it.
7
+ The time picker shows current availability. A slot greys out on every open page
8
+ as soon as someone books it.
10
9
 
11
10
  ## Develop
12
11
 
@@ -14,9 +13,8 @@ greys out for everyone the instant someone books it.
14
13
  __RUN_DEV__
15
14
  ```
16
15
 
17
- Open http://localhost:4321. Pick a service and a time. Then **open a second
18
- tab**, book a slot in one, and watch that slot grey out in the other — with no
19
- refresh.
16
+ Open http://localhost:4321 and pick a service and time. Book a slot in one tab
17
+ and watch it grey out in another without refreshing.
20
18
 
21
19
  ## How the realtime works
22
20
 
@@ -29,7 +27,7 @@ refresh.
29
27
  - `functions/cancelBooking.ts` deletes the `BookedSlot`, which frees the time
30
28
  on every open picker instantly.
31
29
 
32
- ## Privacy — read this
30
+ ## Privacy
33
31
 
34
32
  The `Booking` entity holds the customer's name, email, and phone (PII), so its
35
33
  policy in `app.ts` **denies every client read and write**. The public page only
@@ -49,11 +47,10 @@ bookings.
49
47
 
50
48
  ## Rebrand + reconfigure it
51
49
 
52
- Everything lives in **`lib/site.config.ts`** — brand, colors, services (name /
53
- duration / price), weekly hours, slot length, lead time, reviews, location, FAQ.
54
- Edit that one file (or have a generator produce it) and the whole site and the
55
- booking engine reconfigures. Services and hours are config, not a database to
56
- manage.
50
+ Brand, colors, services, weekly hours, slot length, lead time, reviews,
51
+ location, and FAQ content live in **`lib/site.config.ts`**. Editing or
52
+ generating that file updates both the site and booking engine. Services and
53
+ hours stay in config rather than a separate database.
57
54
 
58
55
  ## Layout
59
56
 
@@ -1,11 +1,10 @@
1
- // THE single source of truth for everything business-specific. The landing
2
- // page, layout, AND the createBooking server function all read from here, so
3
- // services, prices, weekly hours, and lead time stay in lockstep — rebrand and
4
- // reconfigure the whole site by editing this ONE file.
1
+ // Business-specific copy and settings live here. The landing page, layout, and
2
+ // createBooking function all read the same services, prices, weekly hours, and
3
+ // lead time.
5
4
  //
6
5
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
7
6
  //
8
- // Fictional demo copy replace the values, keep the shape.
7
+ // Fictional demo copy. Replace the values and keep the shape.
9
8
 
10
9
  /* ----------------------------- types ----------------------------- */
11
10
 
@@ -108,7 +107,7 @@ export const siteConfig: LocalServiceConfig = {
108
107
  tagline: "Dallas · est. 2014",
109
108
  headline: "A proper haircut, booked in ten seconds.",
110
109
  subcopy:
111
- "Classic cuts, beard work, and hot-towel shaves from barbers who've been at it a while. Pick a time that's actually open — availability updates live and you're set.",
110
+ "Classic cuts, beard work, and hot-towel shaves from experienced barbers. The booking calendar updates as chairs fill.",
112
111
  ctaLabel: "Book a chair",
113
112
  quickFacts: {
114
113
  hours: "Tue–Sat, 9–6",
@@ -119,7 +118,7 @@ export const siteConfig: LocalServiceConfig = {
119
118
 
120
119
  services: {
121
120
  eyebrow: "Services",
122
- headline: "Simple menu, honest prices.",
121
+ headline: "Services and prices, up front.",
123
122
  items: [
124
123
  {
125
124
  slug: "haircut",
@@ -157,7 +156,7 @@ export const siteConfig: LocalServiceConfig = {
157
156
  eyebrow: "Book",
158
157
  headline: "Find a time that's open.",
159
158
  subcopy:
160
- "Pick a service and a day open slots are live, so what you see is what's actually free. No account, no phone tag.",
159
+ "Pick a service and a day from the live availability calendar. Book without creating an account or making a phone call.",
161
160
  slotMinutes: 30,
162
161
  leadTimeHours: 2,
163
162
  daysAhead: 14,
@@ -171,7 +170,7 @@ export const siteConfig: LocalServiceConfig = {
171
170
  6: { open: "09:00", close: "16:00" }, // Sat — shorter
172
171
  },
173
172
  confirmationMessage:
174
- "You're booked. We'll see you then — a reminder will go out the day before.",
173
+ "You're booked. We'll send a reminder the day before.",
175
174
  },
176
175
 
177
176
  reviews: {
@@ -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
@@ -1,12 +1,11 @@
1
1
  # __APP_NAME__
2
2
 
3
- A live, two-sided **marketplace** built with [Pylon](https://pylonsync.com)
4
- server-rendered listings (great for SEO) plus realtime offers, from one binary
5
- on one port. No Next.js, no separate realtime service.
3
+ A live, two-sided marketplace built with [Pylon](https://pylonsync.com), with
4
+ server-rendered listings and realtime offers.
6
5
 
7
6
  Anyone can list an item; anyone else can make an offer or buy it now; sellers
8
- watch offers arrive live and accept or decline every write fanned out to every
9
- open tab instantly.
7
+ watch offers arrive and accept or decline them. Every write reaches each open
8
+ tab.
10
9
 
11
10
  ## Develop
12
11
 
@@ -14,8 +13,8 @@ open tab instantly.
14
13
  __RUN_DEV__
15
14
  ```
16
15
 
17
- Open http://localhost:4321 the grid seeds itself on first load. Then **open a
18
- second tab**, post something from `/sell` in one, and watch it hit the "just
16
+ Open http://localhost:4321. The grid seeds itself on first load. Open a
17
+ second tab, post something from `/sell` in one, and watch it hit the "just
19
18
  listed" ticker in the other with no refresh. Sign in (the `/sell` form prefills
20
19
  a demo account) to list, make offers, and watch your `/me` inbox light up live.
21
20
 
@@ -34,7 +33,7 @@ a demo account) to list, make offers, and watch your `/me` inbox light up live.
34
33
  be spoofed. The heavier logic (accept = mark sold + auto-decline the rest)
35
34
  runs in `functions/respondToOffer.ts` where it enforces "only the seller".
36
35
 
37
- ## Privacy & policies
36
+ ## Privacy and policies
38
37
 
39
38
  - `Listing` + `Offer` are **public-read** (buyers and sellers both see live
40
39
  state); writes are owner-scoped.
@@ -113,11 +113,11 @@ export default function BrowsePage({ searchParams, serverData }: PageProps) {
113
113
  <div className="space-y-6">
114
114
  <section className="space-y-1">
115
115
  <h1 className="text-3xl font-semibold tracking-tight">
116
- The local marketplace that's actually live
116
+ A local marketplace with live listings and offers
117
117
  </h1>
118
118
  <p className="text-muted-foreground">
119
119
  Listings are server-rendered for search engines; offers are realtime.
120
- Open this in two tabs list in one, watch it appear in the other.
120
+ Open two tabs, list an item in one, and watch it appear in the other.
121
121
  </p>
122
122
  </section>
123
123
 
@@ -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
@@ -1,11 +1,10 @@
1
1
  # __APP_NAME__
2
2
 
3
- A restaurant site built with [Pylon](https://pylonsync.com) a server-rendered
4
- menu + landing page with **live table availability** and a private owner
5
- dashboard, all from one binary on one port. No Next.js, no separate API server.
3
+ A restaurant site built with [Pylon](https://pylonsync.com). One server handles
4
+ the menu, landing page, live table availability, and private owner dashboard.
6
5
 
7
- The realtime point: every seating shows how many tables are left, and a time
8
- greys out to "Full" for everyone the instant the last table goes.
6
+ Each seating shows how many tables remain. When someone takes the last table,
7
+ that time changes to "Full" on every open page.
9
8
 
10
9
  ## Develop
11
10
 
@@ -13,9 +12,8 @@ greys out to "Full" for everyone the instant the last table goes.
13
12
  __RUN_DEV__
14
13
  ```
15
14
 
16
- Open http://localhost:4321, scroll to **Reserve**. Then **open a second tab**,
17
- book the last table at a time in one, and watch that time flip to "Full" in the
18
- other — with no refresh.
15
+ Open http://localhost:4321 and scroll to **Reserve**. Book the last table for a
16
+ time in one tab and watch it change to "Full" in another without refreshing.
19
17
 
20
18
  ## How the realtime works
21
19
 
@@ -29,7 +27,7 @@ other — with no refresh.
29
27
  - `functions/cancelReservation.ts` deletes the marker, which frees a table on
30
28
  every open picker instantly.
31
29
 
32
- ## Privacy — read this
30
+ ## Privacy
33
31
 
34
32
  The `Reservation` entity holds the guest's name, email, phone, and notes (PII),
35
33
  so its policy in `app.ts` **denies every client read and write**. The public
@@ -47,10 +45,9 @@ in with, then create that account at `/login`.
47
45
 
48
46
  ## Rebrand + reconfigure it
49
47
 
50
- Everything lives in **`lib/site.config.ts`** brand, colors, the full menu,
51
- seating hours, tables-per-seating, lead time, reviews, location, FAQ. Edit that
52
- one file (or have a generator produce it) and the whole site and the
53
- reservation engine — reconfigures.
48
+ Brand, colors, the menu, seating hours, tables per seating, lead time, reviews,
49
+ location, and FAQ content live in **`lib/site.config.ts`**. Editing or
50
+ generating that file updates both the site and reservation engine.
54
51
 
55
52
  ## Layout
56
53
 
@@ -1,10 +1,9 @@
1
- // THE single source of truth for everything business-specific. Rebrand the
2
- // whole site and reconfigure the reservation engine by editing this ONE
3
- // file. The landing page, layout, AND the createReservation server function all
4
- // read from here, so the menu, seating hours, and table count stay in lockstep.
1
+ // Business-specific copy and settings live here. The landing page, layout, and
2
+ // createReservation function all read the same menu, seating hours, and table
3
+ // count.
5
4
  //
6
5
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
7
- // Fictional demo copy replace the values, keep the shape.
6
+ // Fictional demo copy. Replace the values and keep the shape.
8
7
 
9
8
  /* ----------------------------- types ----------------------------- */
10
9
 
@@ -90,14 +89,14 @@ export const siteConfig: RestaurantConfig = {
90
89
  seo: {
91
90
  title: "Cedar & Vine — a seasonal bistro in Dallas. Reserve a table.",
92
91
  description:
93
- "Wood-fired seasonal plates and natural wine in Dallas. See live table availability and reserve in seconds — no phone tag.",
92
+ "Wood-fired seasonal plates and natural wine in Dallas. Check live table availability and reserve online.",
94
93
  },
95
94
 
96
95
  hero: {
97
96
  tagline: "Dallas · Bishop Arts",
98
97
  headline: "A table by the fire, whenever you're ready.",
99
98
  subcopy:
100
- "Seasonal small plates, wood-fired mains, and a short natural-wine list. Reserve a table in seconds availability is live, so the time you see is a time you can actually get.",
99
+ "Seasonal small plates, wood-fired mains, and a short natural-wine list. The reservation calendar shows current table availability.",
101
100
  ctaLabel: "Reserve a table",
102
101
  quickFacts: {
103
102
  hours: "Wed–Sun, 5–10",
@@ -143,7 +142,7 @@ export const siteConfig: RestaurantConfig = {
143
142
  eyebrow: "Reserve",
144
143
  headline: "Find a table that's open.",
145
144
  subcopy:
146
- "Pick a date, a time, and your party size open tables are live, so what you see is what's actually free. We hold it; you just show up.",
145
+ "Pick a date, time, and party size from the live table calendar. We'll hold the reservation for you.",
147
146
  slotMinutes: 30,
148
147
  leadTimeHours: 2,
149
148
  daysAhead: 21,