@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.
- package/package.json +1 -1
- package/templates/ARCHETYPES.md +72 -74
- package/templates/_root/AGENTS.md +16 -16
- package/templates/_root/README.md +3 -3
- package/templates/agency/AGENTS.md +30 -30
- package/templates/agency/CLAUDE.md +3 -3
- package/templates/agency/README.md +13 -14
- package/templates/agency/lib/site.config.ts +9 -10
- package/templates/ai-chat/AGENTS.md +30 -30
- package/templates/ai-chat/CLAUDE.md +3 -3
- package/templates/ai-chat/README.md +10 -11
- package/templates/ai-chat/lib/site.config.ts +3 -4
- package/templates/ai-studio/AGENTS.md +30 -30
- package/templates/ai-studio/CLAUDE.md +3 -3
- package/templates/ai-studio/README.md +11 -12
- package/templates/ai-studio/lib/site.config.ts +5 -6
- package/templates/barebones/AGENTS.md +30 -30
- package/templates/barebones/CLAUDE.md +3 -3
- package/templates/barebones/README.md +5 -6
- package/templates/chat/AGENTS.md +30 -30
- package/templates/chat/CLAUDE.md +3 -3
- package/templates/chat/README.md +4 -6
- package/templates/consumer/AGENTS.md +30 -30
- package/templates/consumer/CLAUDE.md +3 -3
- package/templates/consumer/README.md +5 -6
- package/templates/creator/AGENTS.md +30 -30
- package/templates/creator/CLAUDE.md +3 -3
- package/templates/creator/README.md +11 -13
- package/templates/creator/lib/site.config.ts +6 -8
- package/templates/default/AGENTS.md +30 -30
- package/templates/default/CLAUDE.md +3 -3
- package/templates/default/README.md +8 -10
- package/templates/default/app/auth-shell.tsx +2 -2
- package/templates/default/lib/site.config.ts +26 -29
- package/templates/directory/AGENTS.md +30 -30
- package/templates/directory/CLAUDE.md +3 -3
- package/templates/directory/README.md +10 -14
- package/templates/directory/lib/site.config.ts +7 -9
- package/templates/local-service/AGENTS.md +30 -30
- package/templates/local-service/CLAUDE.md +3 -3
- package/templates/local-service/README.md +12 -15
- package/templates/local-service/lib/site.config.ts +8 -9
- package/templates/marketplace/AGENTS.md +30 -30
- package/templates/marketplace/CLAUDE.md +3 -3
- package/templates/marketplace/README.md +7 -8
- package/templates/marketplace/app/page.tsx +2 -2
- package/templates/restaurant/AGENTS.md +30 -30
- package/templates/restaurant/CLAUDE.md +3 -3
- package/templates/restaurant/README.md +10 -13
- package/templates/restaurant/lib/site.config.ts +7 -8
- package/templates/shop/AGENTS.md +30 -30
- package/templates/shop/CLAUDE.md +3 -3
- package/templates/shop/README.md +11 -13
- package/templates/shop/lib/site.config.ts +4 -5
- package/templates/todo/AGENTS.md +30 -30
- package/templates/todo/CLAUDE.md +3 -3
- package/templates/todo/README.md +4 -6
- package/templates/waitlist/AGENTS.md +30 -30
- package/templates/waitlist/CLAUDE.md +3 -3
- package/templates/waitlist/README.md +12 -16
- package/templates/waitlist/lib/site.config.ts +11 -12
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
# AGENTS.md
|
|
1
|
+
# AGENTS.md: working in a Pylon project
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from one Rust process on one port. Treat this app as production infrastructure: it supports real auth, SQLite or Postgres, row-level policies, jobs, search, and one-command deploys. Declare entities, policies, and server functions in TypeScript; the binary handles the runtime. Read the full API reference at https://docs.pylonsync.com/llms-full.txt before guessing an API name.
|
|
4
4
|
|
|
5
5
|
## Directory conventions
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
|
-
- `app.ts
|
|
9
|
-
- `app
|
|
10
|
-
- `app/globals.css
|
|
11
|
-
- `functions
|
|
12
|
-
- `.pylon
|
|
8
|
+
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
|
+
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
|
+
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
|
+
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
13
13
|
|
|
14
|
-
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated
|
|
14
|
+
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated; do not hand-edit.
|
|
15
15
|
|
|
16
16
|
## The core authoring loop
|
|
17
17
|
|
|
18
|
-
1. **Define an entity
|
|
19
|
-
2. **Write a policy
|
|
20
|
-
3. **Author a function** in `functions/<name>.ts
|
|
21
|
-
4. **Read it on the client
|
|
18
|
+
1. **Define an entity:** `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update; use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
|
|
19
|
+
2. **Write a policy:** `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). Omitted actions deny by default. `pylon lint` flags wide-open development policies such as `allow*: "true"`; tighten them before shipping.
|
|
20
|
+
3. **Author a function** in `functions/<name>.ts`: `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
|
|
21
|
+
4. **Read it on the client:** `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
|
|
22
22
|
|
|
23
23
|
## Key gotchas
|
|
24
24
|
|
|
25
|
-
- **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access
|
|
26
|
-
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps
|
|
27
|
-
- **Anonymous output caching is opt-in
|
|
28
|
-
- **No-JS forms use `route.ts`
|
|
29
|
-
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends
|
|
30
|
-
- **`export const streaming = true` streams a page's
|
|
31
|
-
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`)
|
|
32
|
-
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive
|
|
33
|
-
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"
|
|
25
|
+
- **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access; enforce trust with `ctx.auth` checks inside the handler, not policies.
|
|
26
|
+
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps`; they're server-only and stripped from hydration, so reading them in the render would mismatch.
|
|
27
|
+
- **Anonymous output caching is opt-in and conditional.** `export const revalidate = 60` makes a page CDN-cacheable (`public, s-maxage=60`) only when the render is auth-independent: it must not read `props.auth`, set a cookie, or run with strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. When any condition fails, the page is `no-cache`. Eligible renders also use the origin disk cache at `.pylon/.cache/ssr`: a cookie-less GET without a query string is served from disk for the TTL and rerendered when stale. The cache is namespaced per deploy, cleared by each build, disabled in `pylon dev`, and invalidated by the `revalidate` TTL or the next deploy.
|
|
28
|
+
- **No-JS forms use `route.ts` and `<Form>`.** Add `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` from `@pylonsync/react` with plain `<input name=...>`. It uses native POST → handler → redirect without JavaScript and no-reload enhancement with JavaScript. The handler's `db` is read+write under the mutation trust model, so gate it on `auth`. CSRF protection is automatic through the Origin gate and SameSite=Lax. Multipart uploads are not supported yet; use URL-encoded forms and `/api/files`.
|
|
29
|
+
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends; a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
|
|
30
|
+
- **`export const streaming = true` streams a page's inner `<Suspense>` boundaries.** Without it or a `loading.tsx`, the page is buffered until all suspended children resolve. With it, the shell and fallbacks flush immediately, then each boundary streams its content. Streaming commits the HTTP head before suspended subtrees finish, so the page is never CDN- or disk-cacheable; do not combine it with `export const revalidate`. Calls to `response.setStatus`, `setCookie`, `redirect`, or `notFound` only take effect during the synchronous shell render. A call from a suspended subtree is dropped and logged. An error from a deep `<Suspense>` child resolves through the nearest `error.tsx` at HTTP 200 rather than 5xx. Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
|
|
31
|
+
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
32
|
+
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
|
|
33
|
+
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
|
-
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db
|
|
35
|
+
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
+
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
37
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
|
-
- **
|
|
38
|
+
- **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
|
|
39
39
|
|
|
40
40
|
## Testing
|
|
41
41
|
|
|
42
|
-
`pylon test` discovers every `*.test.ts` / `*.test.tsx` file under `tests/` (or `functions/`) and runs it with **Bun's test runner** (`import { test, expect } from "bun:test"`). Run the suite with `pylon test` (or `npm test`); filter with `pylon test <substring>`. This template ships `bunfig.toml` + `tests/setup.ts` (registers happy-dom) so component tests render out of the box, plus starter tests under `tests
|
|
42
|
+
`pylon test` discovers every `*.test.ts` / `*.test.tsx` file under `tests/` (or `functions/`) and runs it with **Bun's test runner** (`import { test, expect } from "bun:test"`). Run the suite with `pylon test` (or `npm test`); filter with `pylon test <substring>`. This template ships `bunfig.toml` + `tests/setup.ts` (registers happy-dom) so component tests render out of the box, plus starter tests under `tests/`; replace them with your own.
|
|
43
43
|
|
|
44
|
-
**Tier 1
|
|
44
|
+
**Tier 1: pure logic (start here).** Keep access and plan gating, pricing, credit math, validation, and formatting in pure functions under `lib/`, and test them exhaustively. These tests need no server and run instantly. Keep `query`, `mutation`, and `action` handlers as thin wrappers so their decision logic remains testable without a running app.
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
import { expect, test } from "bun:test";
|
|
@@ -52,7 +52,7 @@ test("unknown slug → undefined", () => {
|
|
|
52
52
|
});
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
**Tier 2
|
|
55
|
+
**Tier 2: React components.** `@testing-library/react` and happy-dom are already wired through `tests/setup.ts`. Render and assert. The template uses the classic JSX transform, so add `import React from "react"` in `.tsx` tests. For a component that reads Pylon data hooks, mock the boundary, then dynamically import the component so the mock is in place first:
|
|
56
56
|
|
|
57
57
|
```tsx
|
|
58
58
|
import { test, expect, mock } from "bun:test";
|
|
@@ -70,7 +70,7 @@ test("renders orgs from the query", () => {
|
|
|
70
70
|
});
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
**Tier 3
|
|
73
|
+
**Tier 3: functions over HTTP.** Use this tier when pure logic tests cannot cover the behavior. A handler's policies, `ctx.db` calls, and auth run in the app. Start `pylon dev` in another terminal and call the API. `resetDb()` from `@pylonsync/functions` clears the in-memory database between cases; it does nothing when the server is down and refuses to run in production.
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
import { afterEach, expect, test } from "bun:test";
|
|
@@ -90,9 +90,9 @@ test("createThing then read it back", async () => {
|
|
|
90
90
|
});
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
`pylon test:security` is a separate adversarial probe
|
|
93
|
+
`pylon test:security` is a separate adversarial probe; it hits a running app and reports auth/policy holes (run `pylon dev`, then `pylon test:security`).
|
|
94
94
|
|
|
95
|
-
## Use the CLI
|
|
95
|
+
## Use the CLI
|
|
96
96
|
|
|
97
97
|
| Need | Command |
|
|
98
98
|
|---|---|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CLAUDE.md
|
|
2
2
|
|
|
3
|
-
Agent guidance for this Pylon project lives in **AGENTS.md
|
|
4
|
-
standard
|
|
5
|
-
|
|
3
|
+
Agent guidance for this Pylon project lives in **AGENTS.md**, the cross-editor
|
|
4
|
+
standard read by Claude Code, Cursor, Codex, and other agents. The import below
|
|
5
|
+
makes it available to Claude Code:
|
|
6
6
|
|
|
7
7
|
@AGENTS.md
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
|
-
A personal
|
|
4
|
-
server-rendered landing page
|
|
5
|
-
private owner dashboard
|
|
6
|
-
separate API server.
|
|
3
|
+
A personal brand or creator site built with [Pylon](https://pylonsync.com). It
|
|
4
|
+
combines a server-rendered landing page, live newsletter subscriber count, and
|
|
5
|
+
private owner dashboard in one server.
|
|
7
6
|
|
|
8
|
-
The
|
|
9
|
-
open the instant someone subscribes.
|
|
7
|
+
The subscriber count updates on every open page when someone subscribes.
|
|
10
8
|
|
|
11
9
|
## Develop
|
|
12
10
|
|
|
@@ -14,18 +12,18 @@ open the instant someone subscribes.
|
|
|
14
12
|
__RUN_DEV__
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
Open http://localhost:4321
|
|
18
|
-
|
|
15
|
+
Open http://localhost:4321, then subscribe in one tab and watch the counter
|
|
16
|
+
increment in another without refreshing.
|
|
19
17
|
|
|
20
18
|
## How the realtime works
|
|
21
19
|
|
|
22
|
-
- `functions/subscribe.ts
|
|
20
|
+
- `functions/subscribe.ts`: a public **mutation** that validates, lowercases,
|
|
23
21
|
and dedupes the email, inserts one `Subscriber` row, and bumps a public,
|
|
24
22
|
PII-free `SubscriberCount` row.
|
|
25
23
|
- `app/newsletter-signup.tsx` subscribes to `SubscriberCount` with
|
|
26
24
|
`db.useQuery`, so the live count syncs to every open tab. No polling.
|
|
27
25
|
|
|
28
|
-
## Privacy
|
|
26
|
+
## Privacy
|
|
29
27
|
|
|
30
28
|
The `Subscriber` entity holds reader emails (PII), so its policy in `app.ts`
|
|
31
29
|
**denies every client read and write**. The public page only ever reads the
|
|
@@ -42,9 +40,9 @@ in with, then create that account at `/login`.
|
|
|
42
40
|
|
|
43
41
|
## Rebrand it
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
Your name, colors, bio, offerings, testimonials, newsletter copy, and links
|
|
44
|
+
live in **`lib/site.config.ts`**. Edit that file, or generate it, to re-theme
|
|
45
|
+
the page.
|
|
48
46
|
|
|
49
47
|
## Layout
|
|
50
48
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// page and layout read from here and stay generic. The create-pylon scaffolder
|
|
4
|
-
// and Mast target this file too.
|
|
1
|
+
// Personal and business-specific copy lives here. The landing page, layout,
|
|
2
|
+
// create-pylon scaffolder, and Mast all read this file.
|
|
5
3
|
//
|
|
6
4
|
// Colors live here (applied as CSS variables on <html> in app/layout.tsx).
|
|
7
|
-
// Fictional demo copy
|
|
5
|
+
// Fictional demo copy. Replace the values and keep the shape.
|
|
8
6
|
|
|
9
7
|
/* ----------------------------- types ----------------------------- */
|
|
10
8
|
|
|
@@ -98,7 +96,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
98
96
|
headline: "Hi, I'm Maya.",
|
|
99
97
|
paragraphs: [
|
|
100
98
|
"I've led design at two startups and a public company, shipped products to millions, and mentored designers who now lead teams of their own.",
|
|
101
|
-
"These days I coach one-on-one, review portfolios, and write a weekly newsletter about the craft and the career.
|
|
99
|
+
"These days I coach one-on-one, review portfolios, and write a weekly newsletter about the craft and the career. Every issue focuses on practical ways to move the work forward.",
|
|
102
100
|
],
|
|
103
101
|
},
|
|
104
102
|
|
|
@@ -113,7 +111,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
113
111
|
},
|
|
114
112
|
{
|
|
115
113
|
title: "Portfolio review",
|
|
116
|
-
body: "A
|
|
114
|
+
body: "A detailed portfolio critique and a recorded walkthrough of the changes I'd make.",
|
|
117
115
|
price: "$250",
|
|
118
116
|
},
|
|
119
117
|
{
|
|
@@ -142,7 +140,7 @@ export const siteConfig: CreatorConfig = {
|
|
|
142
140
|
},
|
|
143
141
|
{
|
|
144
142
|
quote:
|
|
145
|
-
"
|
|
143
|
+
"I read the newsletter every week. It feels like a coaching session in my inbox.",
|
|
146
144
|
name: "Marcus Bell",
|
|
147
145
|
role: "Product Designer",
|
|
148
146
|
},
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
# AGENTS.md
|
|
1
|
+
# AGENTS.md: working in a Pylon project
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from one Rust process on one port. Treat this app as production infrastructure: it supports real auth, SQLite or Postgres, row-level policies, jobs, search, and one-command deploys. Declare entities, policies, and server functions in TypeScript; the binary handles the runtime. Read the full API reference at https://docs.pylonsync.com/llms-full.txt before guessing an API name.
|
|
4
4
|
|
|
5
5
|
## Directory conventions
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
|
-
- `app.ts
|
|
9
|
-
- `app
|
|
10
|
-
- `app/globals.css
|
|
11
|
-
- `functions
|
|
12
|
-
- `.pylon
|
|
8
|
+
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
|
+
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
|
+
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
|
+
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
13
13
|
|
|
14
|
-
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated
|
|
14
|
+
**Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated; do not hand-edit.
|
|
15
15
|
|
|
16
16
|
## The core authoring loop
|
|
17
17
|
|
|
18
|
-
1. **Define an entity
|
|
19
|
-
2. **Write a policy
|
|
20
|
-
3. **Author a function** in `functions/<name>.ts
|
|
21
|
-
4. **Read it on the client
|
|
18
|
+
1. **Define an entity:** `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update; use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
|
|
19
|
+
2. **Write a policy:** `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). Omitted actions deny by default. `pylon lint` flags wide-open development policies such as `allow*: "true"`; tighten them before shipping.
|
|
20
|
+
3. **Author a function** in `functions/<name>.ts`: `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
|
|
21
|
+
4. **Read it on the client:** `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
|
|
22
22
|
|
|
23
23
|
## Key gotchas
|
|
24
24
|
|
|
25
|
-
- **Policies deny by default; server functions
|
|
26
|
-
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps
|
|
27
|
-
- **Anonymous output caching is opt-in
|
|
28
|
-
- **No-JS forms use `route.ts`
|
|
29
|
-
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends
|
|
30
|
-
- **`export const streaming = true` streams a page's
|
|
31
|
-
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`)
|
|
32
|
-
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive
|
|
33
|
-
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"
|
|
25
|
+
- **Policies deny by default; server functions bypass them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full database access. Enforce trust inside the handler with `ctx.auth` checks, or use `ctx.requireMember(orgId, { role: ["owner", "admin"] })` for organization membership and role gates. It throws `FORBIDDEN` for non-members and works in queries, mutations, and actions. Do not hand-roll a member lookup; this primitive fails closed.
|
|
26
|
+
- **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps`; they're server-only and stripped from hydration, so reading them in the render would mismatch.
|
|
27
|
+
- **Anonymous output caching is opt-in and conditional.** `export const revalidate = 60` makes a page CDN-cacheable (`public, s-maxage=60`) only when the render is auth-independent: it must not read `props.auth`, set a cookie, or run with strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. When any condition fails, the page is `no-cache`. Eligible renders also use the origin disk cache at `.pylon/.cache/ssr`: a cookie-less GET without a query string is served from disk for the TTL and rerendered when stale. The cache is namespaced per deploy, cleared by each build, disabled in `pylon dev`, and invalidated by the `revalidate` TTL or the next deploy.
|
|
28
|
+
- **No-JS forms use `route.ts` and `<Form>`.** Add `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` from `@pylonsync/react` with plain `<input name=...>`. It uses native POST → handler → redirect without JavaScript and no-reload enhancement with JavaScript. The handler's `db` is read+write under the mutation trust model, so gate it on `auth`. CSRF protection is automatic through the Origin gate and SameSite=Lax. Multipart uploads are not supported yet; use URL-encoded forms and `/api/files`.
|
|
29
|
+
- **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends; a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
|
|
30
|
+
- **`export const streaming = true` streams a page's inner `<Suspense>` boundaries.** Without it or a `loading.tsx`, the page is buffered until all suspended children resolve. With it, the shell and fallbacks flush immediately, then each boundary streams its content. Streaming commits the HTTP head before suspended subtrees finish, so the page is never CDN- or disk-cacheable; do not combine it with `export const revalidate`. Calls to `response.setStatus`, `setCookie`, `redirect`, or `notFound` only take effect during the synchronous shell render. A call from a suspended subtree is dropped and logged. An error from a deep `<Suspense>` child resolves through the nearest `error.tsx` at HTTP 200 rather than 5xx. Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
|
|
31
|
+
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
32
|
+
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
|
|
33
|
+
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
|
-
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db
|
|
35
|
+
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
+
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
37
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
|
-
- **
|
|
38
|
+
- **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
|
|
39
39
|
|
|
40
40
|
## Testing
|
|
41
41
|
|
|
42
|
-
`pylon test` discovers every `*.test.ts` / `*.test.tsx` file under `tests/` (or `functions/`) and runs it with **Bun's test runner** (`import { test, expect } from "bun:test"`). Run the suite with `pylon test` (or `npm test`); filter with `pylon test <substring>`. This template ships `bunfig.toml` + `tests/setup.ts` (registers happy-dom) so component tests render out of the box, plus starter tests under `tests
|
|
42
|
+
`pylon test` discovers every `*.test.ts` / `*.test.tsx` file under `tests/` (or `functions/`) and runs it with **Bun's test runner** (`import { test, expect } from "bun:test"`). Run the suite with `pylon test` (or `npm test`); filter with `pylon test <substring>`. This template ships `bunfig.toml` + `tests/setup.ts` (registers happy-dom) so component tests render out of the box, plus starter tests under `tests/`; replace them with your own.
|
|
43
43
|
|
|
44
|
-
**Tier 1
|
|
44
|
+
**Tier 1: pure logic (start here).** Keep access and plan gating, pricing, credit math, validation, and formatting in pure functions under `lib/`, and test them exhaustively. These tests need no server and run instantly. Keep `query`, `mutation`, and `action` handlers as thin wrappers so their decision logic remains testable without a running app.
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
import { expect, test } from "bun:test";
|
|
@@ -52,7 +52,7 @@ test("unknown slug → undefined", () => {
|
|
|
52
52
|
});
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
**Tier 2
|
|
55
|
+
**Tier 2: React components.** `@testing-library/react` and happy-dom are already wired through `tests/setup.ts`. Render and assert. The template uses the classic JSX transform, so add `import React from "react"` in `.tsx` tests. For a component that reads Pylon data hooks, mock the boundary, then dynamically import the component so the mock is in place first:
|
|
56
56
|
|
|
57
57
|
```tsx
|
|
58
58
|
import { test, expect, mock } from "bun:test";
|
|
@@ -70,7 +70,7 @@ test("renders orgs from the query", () => {
|
|
|
70
70
|
});
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
**Tier 3
|
|
73
|
+
**Tier 3: functions over HTTP.** Use this tier when pure logic tests cannot cover the behavior. A handler's policies, `ctx.db` calls, and auth run in the app. Start `pylon dev` in another terminal and call the API. `resetDb()` from `@pylonsync/functions` clears the in-memory database between cases; it does nothing when the server is down and refuses to run in production.
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
import { afterEach, expect, test } from "bun:test";
|
|
@@ -90,9 +90,9 @@ test("createThing then read it back", async () => {
|
|
|
90
90
|
});
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
`pylon test:security` is a separate adversarial probe
|
|
93
|
+
`pylon test:security` is a separate adversarial probe; it hits a running app and reports auth/policy holes (run `pylon dev`, then `pylon test:security`).
|
|
94
94
|
|
|
95
|
-
## Use the CLI
|
|
95
|
+
## Use the CLI
|
|
96
96
|
|
|
97
97
|
| Need | Command |
|
|
98
98
|
|---|---|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CLAUDE.md
|
|
2
2
|
|
|
3
|
-
Agent guidance for this Pylon project lives in **AGENTS.md
|
|
4
|
-
standard
|
|
5
|
-
|
|
3
|
+
Agent guidance for this Pylon project lives in **AGENTS.md**, the cross-editor
|
|
4
|
+
standard read by Claude Code, Cursor, Codex, and other agents. The import below
|
|
5
|
+
makes it available to Claude Code:
|
|
6
6
|
|
|
7
7
|
@AGENTS.md
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
3
|
A full-stack, multi-tenant SaaS starter on [Pylon](https://pylonsync.com),
|
|
4
|
-
branded as
|
|
5
|
-
site
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from one binary on one port. No Next.js, no separate API server, no realtime
|
|
9
|
-
sidecar.
|
|
4
|
+
branded as the fictional product **Acme**. It includes a server-rendered
|
|
5
|
+
marketing site, first-run onboarding, email/password and Google auth,
|
|
6
|
+
organizations with members and roles, tenant-scoped projects, and per-workspace
|
|
7
|
+
Stripe billing. Pylon serves it from one process.
|
|
10
8
|
|
|
11
9
|
## Develop
|
|
12
10
|
|
|
@@ -14,10 +12,10 @@ sidecar.
|
|
|
14
12
|
__RUN_DEV__
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
Open http://localhost:4321
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
Open http://localhost:4321 to load the Acme landing page. Sign up and create an
|
|
16
|
+
organization to enter a workspace with tenant-scoped projects and a members
|
|
17
|
+
panel. Create a second organization and switch between them; each one's data
|
|
18
|
+
remains private. Editing a file under `app/` reloads the page.
|
|
21
19
|
|
|
22
20
|
## Layout
|
|
23
21
|
|
|
@@ -50,8 +50,8 @@ export function AuthShell({
|
|
|
50
50
|
“
|
|
51
51
|
</div>
|
|
52
52
|
<blockquote className="mt-2 text-[1.6rem] font-medium leading-snug tracking-tight text-zinc-900">
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
Acme gives our team one view of the work, from planning through
|
|
54
|
+
launch.
|
|
55
55
|
</blockquote>
|
|
56
56
|
<div className="mt-8 flex items-center gap-3">
|
|
57
57
|
<span className="flex size-10 items-center justify-center rounded-full bg-zinc-200 text-[13px] font-semibold text-zinc-500">
|