@pylonsync/create-pylon 0.3.333 → 0.3.334

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/templates/ARCHETYPES.md +72 -74
  3. package/templates/_root/AGENTS.md +16 -16
  4. package/templates/_root/README.md +3 -3
  5. package/templates/agency/AGENTS.md +30 -30
  6. package/templates/agency/CLAUDE.md +3 -3
  7. package/templates/agency/README.md +13 -14
  8. package/templates/agency/lib/site.config.ts +9 -10
  9. package/templates/ai-chat/AGENTS.md +30 -30
  10. package/templates/ai-chat/CLAUDE.md +3 -3
  11. package/templates/ai-chat/README.md +10 -11
  12. package/templates/ai-chat/lib/site.config.ts +3 -4
  13. package/templates/ai-studio/AGENTS.md +30 -30
  14. package/templates/ai-studio/CLAUDE.md +3 -3
  15. package/templates/ai-studio/README.md +11 -12
  16. package/templates/ai-studio/lib/site.config.ts +5 -6
  17. package/templates/barebones/AGENTS.md +30 -30
  18. package/templates/barebones/CLAUDE.md +3 -3
  19. package/templates/barebones/README.md +5 -6
  20. package/templates/chat/AGENTS.md +30 -30
  21. package/templates/chat/CLAUDE.md +3 -3
  22. package/templates/chat/README.md +4 -6
  23. package/templates/consumer/AGENTS.md +30 -30
  24. package/templates/consumer/CLAUDE.md +3 -3
  25. package/templates/consumer/README.md +5 -6
  26. package/templates/creator/AGENTS.md +30 -30
  27. package/templates/creator/CLAUDE.md +3 -3
  28. package/templates/creator/README.md +11 -13
  29. package/templates/creator/lib/site.config.ts +6 -8
  30. package/templates/default/AGENTS.md +30 -30
  31. package/templates/default/CLAUDE.md +3 -3
  32. package/templates/default/README.md +8 -10
  33. package/templates/default/app/auth-shell.tsx +2 -2
  34. package/templates/default/lib/site.config.ts +26 -29
  35. package/templates/directory/AGENTS.md +30 -30
  36. package/templates/directory/CLAUDE.md +3 -3
  37. package/templates/directory/README.md +10 -14
  38. package/templates/directory/lib/site.config.ts +7 -9
  39. package/templates/local-service/AGENTS.md +30 -30
  40. package/templates/local-service/CLAUDE.md +3 -3
  41. package/templates/local-service/README.md +12 -15
  42. package/templates/local-service/lib/site.config.ts +8 -9
  43. package/templates/marketplace/AGENTS.md +30 -30
  44. package/templates/marketplace/CLAUDE.md +3 -3
  45. package/templates/marketplace/README.md +7 -8
  46. package/templates/marketplace/app/page.tsx +2 -2
  47. package/templates/restaurant/AGENTS.md +30 -30
  48. package/templates/restaurant/CLAUDE.md +3 -3
  49. package/templates/restaurant/README.md +10 -13
  50. package/templates/restaurant/lib/site.config.ts +7 -8
  51. package/templates/shop/AGENTS.md +30 -30
  52. package/templates/shop/CLAUDE.md +3 -3
  53. package/templates/shop/README.md +11 -13
  54. package/templates/shop/lib/site.config.ts +4 -5
  55. package/templates/todo/AGENTS.md +30 -30
  56. package/templates/todo/CLAUDE.md +3 -3
  57. package/templates/todo/README.md +4 -6
  58. package/templates/waitlist/AGENTS.md +30 -30
  59. package/templates/waitlist/CLAUDE.md +3 -3
  60. package/templates/waitlist/README.md +12 -16
  61. package/templates/waitlist/lib/site.config.ts +11 -12
@@ -1,13 +1,11 @@
1
1
  # __APP_NAME__
2
2
 
3
- A studio / agency site built with [Pylon](https://pylonsync.com) a
4
- server-rendered marketing page with **live availability**, a private project
5
- inquiry form, and an owner dashboard, all from one binary on one port. No
6
- Next.js, no separate API server.
3
+ A studio or agency site built with [Pylon](https://pylonsync.com). It combines
4
+ a server-rendered marketing page, live availability, a private project inquiry
5
+ form, and an owner dashboard in one server.
7
6
 
8
- The realtime point: boutique studios take on a few projects at a time. The hero
9
- shows how many slots are open this quarter, and the moment the owner books a new
10
- client from the dashboard, that number drops for everyone with the page open.
7
+ The hero shows how many project slots are open this quarter. When the owner
8
+ books a client from the dashboard, the count drops for every open page.
11
9
 
12
10
  ## Develop
13
11
 
@@ -15,7 +13,7 @@ client from the dashboard, that number drops for everyone with the page open.
15
13
  __RUN_DEV__
16
14
  ```
17
15
 
18
- Open http://localhost:4321. Then **open a second tab**, sign in as the owner
16
+ Open http://localhost:4321. In a second tab, sign in as the owner
19
17
  (see below), book a lead from the dashboard, and watch the "N slots open" pill on
20
18
  the public page tick down — with no refresh.
21
19
 
@@ -25,14 +23,14 @@ the public page tick down — with no refresh.
25
23
  slot count. `app/contact-form.tsx` reads it with `db.useQuery("Capacity")`, so
26
24
  the hero's "N project slots open" pill is live everywhere.
27
25
  - `functions/submitInquiry.ts` is a public **mutation** — anyone can send a
28
- project lead. It does NOT consume a slot (a lead isn't a booking).
26
+ project lead. A lead does not consume a slot because it is not a booking.
29
27
  - `functions/bookInquiry.ts` / `declineInquiry.ts` are owner-only mutations that
30
- mark a lead booked/declined AND adjust `Capacity.openSlots` (under an advisory
28
+ mark a lead booked or declined and adjust `Capacity.openSlots` under an advisory
31
29
  lock), so the public counter moves live.
32
30
  - `functions/seedCapacity.ts` creates the Capacity row from config on first
33
31
  visit (idempotent).
34
32
 
35
- ## Privacy — read this
33
+ ## Privacy
36
34
 
37
35
  The `Inquiry` entity holds the prospect's name, email, company, and budget (PII),
38
36
  so its policy in `app.ts` **denies every client read and write**. The public page
@@ -62,10 +60,11 @@ The logo cloud uses sample names — replace with your real client logos.
62
60
 
63
61
  ## Rebrand it
64
62
 
65
- Everything lives in **`lib/site.config.ts`** — brand, colors, hero, services,
63
+ Brand, colors, hero copy, services,
66
64
  case studies, process, team, testimonials, and the contact form's project-type
67
- and budget options. Edit that one file (or have Mast generate it) and the whole
68
- studio re-themes; the capacity re-seeds on a fresh database.
65
+ and budget options live in **`lib/site.config.ts`**. Edit that file, or have
66
+ Mast generate it, to update the studio. A fresh database seeds capacity from
67
+ the same config.
69
68
 
70
69
  ## Layout
71
70
 
@@ -1,11 +1,10 @@
1
- // The single source of truth for everything business-specific on this agency
2
- // site. Rebrand the whole studio by editing this one file — the landing page,
3
- // layout, and the seedCapacity function all read from here.
1
+ // Business-specific copy and settings live here. The landing page, layout, and
2
+ // seedCapacity function all read this file.
4
3
  //
5
4
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
6
- // Fictional demo copy replace the values, keep the shape. Anywhere a real
5
+ // Fictional demo copy. Replace the values and keep the shape. Anywhere a real
7
6
  // photo belongs (case-study shots, team headshots) the page renders a clearly
8
- // marked placeholder swap those for <img>s when you have the assets.
7
+ // marked placeholder; swap those for <img>s when you have the assets.
9
8
 
10
9
  /* ----------------------------- types ----------------------------- */
11
10
 
@@ -122,7 +121,7 @@ export const siteConfig: AgencyConfig = {
122
121
  domain: "halyard.studio",
123
122
  email: "hello@halyard.example",
124
123
  footerBlurb:
125
- "A product studio in Dallas. We design and build the software ambitious teams bet on — and we only take on a few projects at a time, so the work stays sharp.",
124
+ "A Dallas product studio that designs and builds software for ambitious teams. We take on only a few projects at a time.",
126
125
  copyrightName: "Halyard Studio",
127
126
  socials: [
128
127
  {
@@ -143,14 +142,14 @@ export const siteConfig: AgencyConfig = {
143
142
  seo: {
144
143
  title: "Halyard — a product studio for ambitious teams.",
145
144
  description:
146
- "Halyard is a Dallas product studio. We design and build web and mobile software end-to-end. We take on a few projects at a time — see how many slots are open this quarter.",
145
+ "Halyard is a Dallas product studio for web and mobile software. See how many project slots are open this quarter.",
147
146
  },
148
147
 
149
148
  hero: {
150
149
  tagline: "Product studio · Dallas",
151
150
  headline: "We build the products teams bet on.",
152
151
  subcopy:
153
- "Halyard is a small, senior team that designs and ships web and mobile software end-to-end. We take on a handful of projects at a time, so the work and your launch — stays sharp.",
152
+ "Halyard is a small, senior team that designs and ships web and mobile software. We take on a handful of projects at a time so each launch gets the team's attention.",
154
153
  ctaLabel: "Start a project",
155
154
  secondaryCtaLabel: "See our work",
156
155
  },
@@ -202,7 +201,7 @@ export const siteConfig: AgencyConfig = {
202
201
  tags: ["Product design", "iOS", "Brand"],
203
202
  selected: true,
204
203
  challenge:
205
- "A two-founder fintech had funding and a thesis, but no product, no brand, and a hard 12-week runway to a launchable app the App Store would approve.",
204
+ "A two-founder fintech had funding, a thesis, and 12 weeks to create its product, brand, and App Store-ready launch.",
206
205
  approach:
207
206
  "We ran a one-week scope sprint, then designed and built in parallel — a clickable prototype on real data by week three, weekly TestFlight builds after that. Brand and UI moved together so nothing felt bolted on.",
208
207
  outcome:
@@ -355,7 +354,7 @@ export const siteConfig: AgencyConfig = {
355
354
  },
356
355
  {
357
356
  title: "Design",
358
- body: "Clickable, real-data prototypes not slide decks. You react to something you can use within two weeks.",
357
+ body: "Within two weeks, you can test a clickable prototype running on real data.",
359
358
  },
360
359
  {
361
360
  title: "Build",
@@ -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 streaming **AI chat** app built with [Pylon](https://pylonsync.com) token
4
- streaming, multi-conversation history, and realtime cross-tab sync, all from one
5
- binary on one port. No Next.js, no separate API server.
3
+ A streaming AI chat app built with [Pylon](https://pylonsync.com). One server
4
+ handles token streaming, conversation history, and cross-tab sync.
6
5
 
7
6
  Tokens stream from the built-in `POST /api/ai/stream` endpoint, so your provider
8
7
  API key never reaches the browser. Conversations are sync-backed and
9
- owner-scoped — open two tabs and a chat you send in one shows up in the other.
8
+ owner-scoped; a chat sent in one tab appears in the other.
10
9
 
11
10
  ## Develop
12
11
 
@@ -14,14 +13,14 @@ owner-scoped — open two tabs and a chat you send in one shows up in the other.
14
13
  __RUN_DEV__
15
14
  ```
16
15
 
17
- Open http://localhost:4321. You can create chats right away; to get **replies**,
18
- point it at an LLM provider (see below). Then **open a second tab** your
19
- conversations and messages stay in lockstep.
16
+ Open http://localhost:4321. You can create chats immediately. Configure an LLM
17
+ provider to receive replies, then open a second tab to see conversations and
18
+ messages sync.
20
19
 
21
20
  ## Configure the model
22
21
 
23
- The assistant replies only once you set a provider (the app boots fine without
24
- it and shows a friendly notice):
22
+ The assistant replies after you configure a provider. Without one, the app
23
+ still boots and displays a configuration notice:
25
24
 
26
25
  ```bash
27
26
  # .env
@@ -77,8 +76,8 @@ data.userId`) — you can only ever read or write your own. `User.passwordHash`
77
76
 
78
77
  ## Rebrand it
79
78
 
80
- Everything brand-specific — name, colors, the assistant's system prompt, the
81
- empty-state copy, and starter prompts — lives in **`lib/site.config.ts`**.
79
+ The name, colors, system prompt, empty-state copy, and starter prompts live in
80
+ **`lib/site.config.ts`**.
82
81
 
83
82
  ## Layout
84
83
 
@@ -1,9 +1,8 @@
1
- // Single source of truth for everything brand-specific on this AI chat app —
2
- // edit this one file to rebrand; the layout + chat UI read from here. The
3
- // create-pylon scaffolder and Mast target this file.
1
+ // Brand-specific copy and settings live here. The layout, chat UI,
2
+ // create-pylon scaffolder, and Mast all read this file.
4
3
  //
5
4
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
6
- // Fictional demo copy replace the values, keep the shape.
5
+ // Fictional demo copy. Replace the values and keep the shape.
7
6
 
8
7
  /* ----------------------------- types ----------------------------- */
9
8
 
@@ -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,13 +1,12 @@
1
1
  # __APP_NAME__
2
2
 
3
- A generative **AI media studio** (image / audio / video) built with
4
- [Pylon](https://pylonsync.com) a live gallery that fills in as each generation
5
- finishes, from one binary on one port. No Next.js, no separate job service.
3
+ A generative image, audio, and video studio built with
4
+ [Pylon](https://pylonsync.com). One server runs background jobs and updates the
5
+ gallery as each generation finishes.
6
6
 
7
- Kick off a generation and a card appears instantly, then flips to the finished
8
- result the moment the generation completes live, across every open tab. The
9
- generation runs in a **background job** (so even a minutes-long video never
10
- blocks your request), and the provider call + API token stay on the server.
7
+ Start a generation and a pending card appears immediately in every open tab.
8
+ The card updates when the background job finishes. Provider calls and API
9
+ tokens stay on the server, and long video jobs do not block the request.
11
10
 
12
11
  ## Develop
13
12
 
@@ -15,9 +14,9 @@ blocks your request), and the provider call + API token stay on the server.
15
14
  __RUN_DEV__
16
15
  ```
17
16
 
18
- Open http://localhost:4321 and generate something it works with **no config**
19
- (a clearly-labeled placeholder). Add a Replicate token for real media (below).
20
- Then **open a second tab** your gallery stays in sync.
17
+ Open http://localhost:4321 and generate something. Without configuration, the
18
+ app returns a clearly labeled placeholder. Add a Replicate token for provider
19
+ output, then open a second tab to see the gallery sync.
21
20
 
22
21
  ## Enable real generation (Replicate)
23
22
 
@@ -53,8 +52,8 @@ hosted URLs.
53
52
  runs, so you can see the realtime gallery with zero config.
54
53
  - Results are Replicate's hosted URLs (fine for a live studio). For permanent
55
54
  storage, download the asset in the job and persist via `/api/files`.
56
- - Video is genuinely wired (Replicate has text-to-video models) it just takes
57
- longer, which is exactly why the work runs in a background job.
55
+ - Video uses Replicate's text-to-video models. These slower requests run as
56
+ background jobs.
58
57
 
59
58
  ## Rebrand it
60
59
 
@@ -1,9 +1,8 @@
1
- // THE single source of truth for everything brand-specific on this AI media
2
- // studio. Rebrand by editing this ONE file — the layout + studio UI read from
3
- // here. The create-pylon scaffolder and Mast target this file.
1
+ // Brand-specific copy and settings live here. The layout, studio UI,
2
+ // create-pylon scaffolder, and Mast all read this file.
4
3
  //
5
4
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
6
- // Fictional demo copy replace the values, keep the shape.
5
+ // Fictional demo copy. Replace the values and keep the shape.
7
6
 
8
7
  import type { GenerationKind } from "./studio";
9
8
 
@@ -42,7 +41,7 @@ export const siteConfig: StudioConfig = {
42
41
  letter: "P",
43
42
  domain: "prism.studio",
44
43
  email: "hello@prism.example",
45
- footerBlurb: "A generative media studio built on Pylon — image, audio, and video from a prompt.",
44
+ footerBlurb: "Generate images, audio, and video from a prompt with Pylon.",
46
45
  copyrightName: "Prism",
47
46
  socials: [
48
47
  {
@@ -63,7 +62,7 @@ export const siteConfig: StudioConfig = {
63
62
 
64
63
  studio: {
65
64
  headline: "Make something.",
66
- subcopy: "Describe it, pick a medium, and watch it land in your gallery live, the moment it's done.",
65
+ subcopy: "Describe it and pick a medium. The result appears in your gallery when the job finishes.",
67
66
  inputPlaceholder: "A neon koi swimming through a rainy Tokyo alley at night…",
68
67
  kinds: [
69
68
  { id: "image", label: "Image", wired: true },