create-caspian-app 0.8.0-rc.2 → 1.0.0

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/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Caspian — The Native Python Web Framework for the Reactive Web
2
2
 
3
- Caspian is a high-performance, FastAPI-powered full-stack framework that brings reactive UI to Python without forcing a JavaScript backend. It combines:
3
+ Caspian is a FastAPI-powered full-stack framework that brings reactive UI to Python without forcing a JavaScript backend. You write file-system routes, plain HTML templates, and `async def` Python — Caspian wires the browser to your server.
4
4
 
5
- - **FastAPI Engine** for async-native performance and the broader FastAPI ecosystem
6
- - **Hybrid Frontend Engine**: start with zero-build HTML, then upgrade to Vite + NPM + TypeScript when needed
7
- - **Direct async RPC** ("Zero-API"): call Python functions from the browser via `pp.rpc()`
8
- - **File-system routing** with nested layouts and dynamic routes (Next.js App Router mental model)
9
- - **Prisma ORM integration** with an auto-generated, type-safe Python client
10
- - **PulsePoint** a lightweight browser-side reactive runtime for interactive UI
11
- - **Session-based authentication** with RBAC support and built-in security defaults
5
+ - **FastAPI engine** async-native, with the Starlette/FastAPI middleware and ecosystem underneath
6
+ - **PulsePoint** a shipped browser-side reactive runtime with a React-style hook API and **plain HTML** templates (no JSX, no build step required)
7
+ - **"Zero-API" RPC** — call Python functions from the browser with `pp.rpc()`; no controllers, no route handlers, no fetch boilerplate
8
+ - **File-system routing** with nested layouts, dynamic segments, and route groups (Next.js App Router mental model)
9
+ - **Python components** reusable `@component` functions rendered as HTML-first `x-*` tags
10
+ - **Prisma ORM** with a generated, type-safe Python client
11
+ - **Session auth** with RBAC and OAuth providers, plus fail-closed security defaults
12
+ - **Optional**: Tailwind CSS, TypeScript tooling, MCP server, WebSockets — each gated by one config flag
12
13
 
13
14
  ---
14
15
 
@@ -16,183 +17,288 @@ Caspian is a high-performance, FastAPI-powered full-stack framework that brings
16
17
 
17
18
  ### Requirements
18
19
 
19
- - **Node.js**: v24.13.1+
20
- - **Python**: v3.14.0+
20
+ - **Python** `3.14` or newer
21
+ - **Node.js** with `npm` and `npx` available (used for the CLI, Prisma, Tailwind, and the dev stack)
21
22
 
22
- ### Create an app (interactive wizard)
23
+ ```bash
24
+ node -v
25
+ python -V
26
+ ```
27
+
28
+ ### Create an app
23
29
 
24
30
  ```bash
25
31
  npx create-caspian-app@latest
26
32
  ```
27
33
 
28
- The wizard walks through the main project options:
34
+ The interactive wizard walks through:
29
35
 
30
36
  - Project name
31
- - Feature toggles: backend-only mode, Tailwind CSS, Prisma, MCP, TypeScript
32
- - Starter kit selection (basic, fullstack, api, realtime)
37
+ - Feature toggles: backend-only, Tailwind CSS, Prisma, MCP, TypeScript
38
+ - Starter kit selection (`basic`, `fullstack`, `api`, `realtime`, or a custom Git source)
33
39
 
34
- ### Run dev server
40
+ ### Run the dev server
35
41
 
36
42
  ```bash
37
43
  cd my-app
38
44
  npm run dev
39
45
  ```
40
46
 
41
- > **Note:** Many Caspian projects use BrowserSync plus PostCSS watchers rather than a Vite dev server. Check `package.json` for the actual dev script.
47
+ > The generated `package.json` is the source of truth for what `npm run dev` does. Caspian projects typically run a **BrowserSync proxy plus PostCSS/Vite watchers**, not a Vite dev server that owns the page. When the dev stack is running, check `settings/bs-config.json` for the active local URL and port — the proxy does not always land on the default port.
42
48
 
43
49
  ---
44
50
 
45
51
  ## What "Reactive Python" looks like
46
52
 
47
- A Caspian page is plain HTML with reactive directives plus a small `<script>` block for state. The framework handles the rest.
53
+ A route is a folder. The markup lives in `index.html`, the server logic in `index.py`. Templates are **plain HTML** with `{expression}` interpolation and a small `<script>` for state.
48
54
 
49
- ### Route template (`src/app/todos/index.html`)
55
+ ### Route template `src/app/todos/index.html`
50
56
 
51
57
  ```html
52
- <!-- @import { Badge } from "../components/ui" -->
58
+ <!-- @import { Badge } from "../../components/ui/Badge.py" -->
53
59
 
54
60
  <section>
55
- <div class="flex gap-2 mb-4">
56
- <x-badge variant="default">Tasks: {todos.length}</x-badge>
57
- </div>
61
+ <x-badge variant="default">Tasks: {todos.length}</x-badge>
62
+
63
+ <form onsubmit="{addTodo(event)}">
64
+ <input name="title" required />
65
+ <button type="submit" disabled="{isSaving}">Add</button>
66
+ </form>
58
67
 
59
68
  <ul>
60
69
  <template pp-for="(todo, index) in todos">
61
70
  <li key="{todo.id}" class="p-2 border-b">
62
71
  {index + 1}. {todo.title}
63
- <button onclick="removeTodo(todo.id)">Remove</button>
72
+ <button onclick="{removeTodo(todo.id)}">Remove</button>
64
73
  </li>
65
74
  </template>
66
75
  </ul>
67
76
 
77
+ <p hidden="{todos.length > 0}">Nothing here yet.</p>
78
+
68
79
  <script>
69
80
  const [todos, setTodos] = pp.state([]);
70
- function removeTodo(id) {
81
+ const [isSaving, setIsSaving] = pp.state(false);
82
+
83
+ pp.effect(() => {
84
+ pp.rpc("list_todos").then(setTodos);
85
+ }, []);
86
+
87
+ async function addTodo(event) {
88
+ event.preventDefault();
89
+ setIsSaving(true);
90
+ try {
91
+ const data = Object.fromEntries(
92
+ new FormData(event.currentTarget).entries(),
93
+ );
94
+ const created = await pp.rpc("create_todo", data);
95
+ setTodos([created, ...todos]);
96
+ event.currentTarget.reset();
97
+ } finally {
98
+ setIsSaving(false);
99
+ }
100
+ }
101
+
102
+ async function removeTodo(id) {
103
+ await pp.rpc("delete_todo", { id });
71
104
  setTodos(todos.filter((todo) => todo.id !== id));
72
105
  }
73
106
  </script>
74
107
  </section>
75
108
  ```
76
109
 
77
- ### Backend RPC (`src/app/todos/index.py`)
110
+ ### Backend `src/app/todos/index.py`
78
111
 
79
112
  ```python
113
+ from casp.layout import Metadata, render_page
80
114
  from casp.rpc import rpc
81
- from casp.validate import Validate
82
- from src.lib.prisma.db import prisma
115
+ from casp.validate import Rule, Validate
116
+ from src.lib.prisma import prisma
117
+
118
+ metadata = Metadata(
119
+ title="Todos",
120
+ description="A tiny Caspian todo list.",
121
+ )
122
+
123
+
124
+ async def page():
125
+ return render_page(__file__)
126
+
83
127
 
84
128
  @rpc()
85
- async def create_todo(title):
86
- if Validate.with_rules(title, "required|min:3") is not True:
87
- raise ValueError("Title must be at least 3 chars")
129
+ async def list_todos():
130
+ todos = await prisma.todo.find_many(order_by={"id": "desc"})
131
+ return [todo.to_dict() for todo in todos]
88
132
 
89
- new_todo = await prisma.todo.create(data={
90
- "title": title,
91
- "completed": False
92
- })
93
- return new_todo.to_dict()
94
133
 
95
- @rpc(require_auth=True)
96
- async def delete_todo(id, _current_user_id=None):
97
- await prisma.todo.delete(where={"id": id})
98
- return {"success": True}
99
- ```
134
+ @rpc()
135
+ async def create_todo(title: str):
136
+ checked = Validate.with_rules(title, [Rule.REQUIRED, Rule.min(3)])
137
+ if checked is not True:
138
+ raise ValueError("Title must be at least 3 characters.")
100
139
 
101
- ### Frontend call
140
+ todo = await prisma.todo.create(data={"title": title.strip(), "completed": False})
141
+ return todo.to_dict()
102
142
 
103
- ```html
104
- <script>
105
- async function add(e) {
106
- e.preventDefault();
107
- const data = Object.fromEntries(new FormData(e.target));
108
- const newTodo = await pp.rpc("create_todo", data);
109
- setTodos([newTodo, ...todos]);
110
- e.target.reset();
111
- }
112
- </script>
143
+
144
+ @rpc(require_auth=True)
145
+ async def delete_todo(id: int):
146
+ await prisma.todo.delete(where={"id": int(id)})
147
+ return {"deleted": True}
113
148
  ```
114
149
 
150
+ That is the whole loop: no API routes, no client, no serializer. `pp.rpc("create_todo", data)` posts to the current route, Caspian resolves the decorated function, filters the payload against its signature, runs it, and returns JSON.
151
+
115
152
  ---
116
153
 
117
- ## Why developers choose Caspian
154
+ ## Core concepts
118
155
 
119
- ### FastAPI engine, async-native
156
+ ### 1. Routing
120
157
 
121
- Your logic runs in native async Python and can leverage FastAPI/Starlette features (dependency injection, middleware, validation, etc.) without a separate JS backend.
158
+ Your directory structure under `src/app` is your URL structure.
122
159
 
123
- ### Hybrid frontend engine (zero-build → Vite)
160
+ | File | URL |
161
+ | ------------------------------- | ------------- |
162
+ | `src/app/index.html` | `/` |
163
+ | `src/app/about/index.html` | `/about` |
164
+ | `src/app/blog/posts/index.html` | `/blog/posts` |
124
165
 
125
- Start with simple HTML-first development for speed and clarity, then adopt Vite + NPM + TypeScript when you need richer libraries or complex bundles.
166
+ ```
167
+ src/app/users/[id]/index.html -> /users/123 (dynamic segment)
168
+ src/app/docs/[...slug]/index.html -> /docs/a/b/c (catch-all)
169
+ src/app/(auth)/login/index.html -> /login (route group, no URL segment)
170
+ src/app/dashboard/layout.html -> wraps every /dashboard/* page
171
+ ```
126
172
 
127
- ### "Zero-API" server actions (RPC)
173
+ #### Route file conventions
128
174
 
129
- Define `async def` actions decorated with `@rpc()` and call them directly from the browser; Caspian handles serialization, security, and async execution via `pp.rpc()`.
175
+ | File | Owns |
176
+ | ---------------- | -------------------------------------------------------------------------------- |
177
+ | `index.html` | The page's authored markup (single root element) |
178
+ | `index.py` | `metadata`, `page()`, route-owned `@rpc()` actions, redirects, first-render data |
179
+ | `layout.html` | A section wrapper containing `<slot />` |
180
+ | `layout.py` | Layout-level props and server logic |
181
+ | `loading.html` | Navigation loading state for the route |
182
+ | `not-found.html` | 404 UI |
183
+ | `error.html` | Error UI |
130
184
 
131
- ### File-system routing with nested layouts
185
+ `page()` renders the sibling template with `render_page(__file__, context)`. It can also return a
186
+ `(page_html, layout_props)` tuple, whose dict keys become `{{ layout.* }}` in a parent layout.
132
187
 
133
- Routes are determined by files in `src/app`, supporting dynamic segments (`[id]`), catch-alls (`[...slug]`), route groups (`(auth)`), and layout nesting via `layout.html`.
188
+ > **Rule:** `index.py` never inlines page HTML. Markup belongs in `index.html`.
134
189
 
135
- ### Prisma ORM integration (type-safe Python client)
190
+ ---
136
191
 
137
- Define a single Prisma schema and generate a typed Python client autocomplete-first database access without boilerplate.
192
+ ### 2. Templates are plain HTMLnot JSX
138
193
 
139
- ### PulsePoint reactive runtime
194
+ PulsePoint borrows React's **hook API** and its **component decomposition model**. It does **not** borrow JSX. This is the single most common source of broken pages.
140
195
 
141
- A lightweight browser-side reactive runtime ships with Caspian. It follows a React-like mental model but is HTML-first rather than JSX-first, with `pp.state`, `pp.effect`, `pp.ref`, `pp-context`, `pp-for`, and `pp.portal`.
196
+ ```html
197
+ <!-- ❌ These silently corrupt the page -->
198
+ <div class="{cls}">…</div>
199
+ <!-- unquoted brace attr -->
200
+ {isOpen &&
201
+ <div>Panel</div>
202
+ } {items.map(item => (
203
+ <li>{item.name}</li>
204
+ ))}
205
+ <button className="btn" onClick="{save}">Save</button>
206
+
207
+ <!-- ✅ The PulsePoint equivalents -->
208
+ <div class="{cls}">…</div>
209
+ <div hidden="{!isOpen}">Panel</div>
210
+ <template pp-for="item in items"><li key="{item.id}">{item.name}</li></template>
211
+ <button class="btn" onclick="{save()}">Save</button>
212
+ ```
142
213
 
143
- ### Security defaults and authentication
214
+ An unquoted `class={...}` is **invalid HTML**: the parser splits it on spaces into junk attributes, the component root never compiles, and the route serves a blank page **with no console error**.
144
215
 
145
- Built-in CSRF protection, strict Origin validation, HttpOnly cookies, and a session-based auth model with OAuth provider support.
216
+ **Sanity check:** delete every `{}` from your template. What remains must still be valid HTML.
146
217
 
147
- ---
218
+ There is no `pp-if`, `pp-show`, `pp-else`, or `pp-key`. Conditionals are `hidden="{...}"` or a ternary inside `{...}`. Keyed lists use plain `key`.
148
219
 
149
- ## Core concepts
220
+ #### The complete author-facing template surface
150
221
 
151
- ### Routing
222
+ | Syntax | Where | Purpose |
223
+ | --------------------------------------------------------------- | ------------------------------------------ | ----------------------------- |
224
+ | `{expression}` | Text nodes and **quoted** attribute values | Interpolation |
225
+ | `onclick`, `oninput`, `onchange`, `onsubmit`, any `on*` | Any element | Event binding |
226
+ | `pp-for="item in items"` / `"(item, index) in items"` | **`<template>` only** | Keyed list rendering |
227
+ | `key="{expr}"` | The repeated element | Diffing identity |
228
+ | `pp-ref="name"` / `pp-ref="{expr}"` | Native elements and `x-*` tags | Imperative element access |
229
+ | `pp-style="{cssText}"` | Any element | Dynamic inline style (string) |
230
+ | `pp-spread="{...obj}"` | Any element | Spread object into attributes |
231
+ | `<token.provider value="{v}">` (lowercase) | Anywhere | Context provider |
232
+ | `pp-spa="true"` / `pp-spa="false"` | `<body>` / an `<a>` | SPA navigation opt-in/out |
233
+ | `pp-reset-scroll="true"`, `pp-scroll-key="name"` | A scroll container | Scroll restoration control |
234
+ | `pp-loading-content`, `pp-loading-url`, `pp-loading-transition` | Navigation regions | Loading UI |
152
235
 
153
- Caspian follows the same mental model as the Next.js App Router. Your directory structure becomes your URL structure.
236
+ Never handwrite runtime-managed attributes (`pp-component`, `type="text/pp"`, `pp-owner`, `pp-ref-owner`, `pp-ref-forward`, `data-pp-*`, …). The render pipeline and the browser runtime write those.
154
237
 
155
- | File | URL |
156
- | ------------------------------- | ------------- |
157
- | `src/app/index.html` | `/` |
158
- | `src/app/about/index.html` | `/about` |
159
- | `src/app/blog/posts/index.html` | `/blog/posts` |
238
+ #### The single-root rule
160
239
 
161
- #### Dynamic segments
240
+ Every route, layout, and component template must have **exactly one** top-level element (or one imported `x-*` root), with any owned `<script>` **inside** that root. Caspian injects `pp-component` on the final root and errors if it cannot find one. `<!-- @import ... -->` comments sit above the root and do not count as content.
162
241
 
163
- ```
164
- src/app/users/[id]/index.html -> /users/123
165
- ```
242
+ ---
166
243
 
167
- #### Catch-all segments
244
+ ### 3. PulsePoint hooks
168
245
 
169
- ```
170
- src/app/docs/[...slug]/index.html -> /docs/getting-started/setup
171
- ```
246
+ Component `<script>` blocks are plain JavaScript. The `pp` object mirrors React hooks:
172
247
 
173
- #### Route groups (organize without changing URLs)
248
+ | Hook | Returns / purpose |
249
+ | --------------------------------------------- | ---------------------------------------------------------------------- |
250
+ | `pp.state(initial)` | `[value, setValue]` — setter accepts a value or an updater |
251
+ | `pp.effect(fn, deps?)` | After render; may return a cleanup function |
252
+ | `pp.layoutEffect(fn, deps?)` | Synchronously after DOM mutation |
253
+ | `pp.ref(initial?)` | `{ current }` |
254
+ | `pp.memo(fn, deps)` / `pp.callback(fn, deps)` | Memoized value / stable function |
255
+ | `pp.reducer(reducer, initial)` | `[state, dispatch]` |
256
+ | `pp.context(token)` | Read a context value from an ancestor provider |
257
+ | `pp.portal(ref, target?)` | Render into another DOM target, preserving logical ancestry |
258
+ | `pp.id()` | Stable unique id for `id`/`for`/`aria-*` pairing |
259
+ | `pp.errorBoundary()` | `[error, reset]` — catches render and effect throws in descendants |
260
+ | `pp.syncExternalStore(subscribe, snapshot)` | Subscribe to a source the component doesn't own (`matchMedia`, stores) |
261
+ | `pp.imperativeHandle(ref, create, deps?)` | Publish an imperative API to a parent's ref |
262
+ | `pp.transition()` | `[isPending, startTransition]` |
263
+ | `pp.deferredValue(value, initial?)` | Lags one commit behind the source |
264
+ | `pp.optimistic(passthrough, reducer?)` | Optimistic UI that reconciles against a confirmed value |
265
+ | `pp.props` | Props bag derived from the rendered root's attributes |
174
266
 
175
- ```
176
- src/app/(auth)/login/index.html -> /login
177
- src/app/(auth)/register/index.html -> /register
178
- ```
267
+ Runtime utilities: `pp.createContext`, `pp.mount`, `pp.redirect`, `pp.rpc`, `pp.enablePerf`, `pp.disablePerf`, `pp.getPerfStats`, `pp.resetPerfStats`.
179
268
 
180
- #### Nested layouts
269
+ React APIs with **no** PulsePoint equivalent: `forwardRef`, `memo()` as a wrapper, `lazy`, `Suspense`, `useInsertionEffect`, `useActionState`, `useFormStatus`, free-function `startTransition`.
181
270
 
182
- Layouts wrap pages and preserve state during navigation:
271
+ Inside an `on*` attribute the runtime injects `event` plus the aliases `e`, `$event`, `target`, `currentTarget`, and `el`.
183
272
 
184
- - Root: `src/app/layout.html`
185
- - Section: e.g., `/dashboard/settings` inherits root + dashboard layout automatically
273
+ #### Context
186
274
 
187
- > **Rule:** For UI routes, keep markup in `index.html` and server logic in `index.py`. For section layouts, keep the visible wrapper in `layout.html` and layout-level props in `layout.py`.
275
+ Providers are authored as **lowercase** tags derived from the token variable name:
276
+
277
+ ```html
278
+ <section>
279
+ <script>
280
+ const ThemeContext = pp.createContext("light");
281
+ const [theme, setTheme] = pp.state("dark");
282
+ </script>
283
+
284
+ <themecontext.provider value="{theme}">
285
+ <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
286
+ Theme: {theme}
287
+ </button>
288
+ <x-child-panel />
289
+ </themecontext.provider>
290
+ </section>
291
+ ```
292
+
293
+ A descendant reads it with `const theme = pp.context(ThemeContext);`.
188
294
 
189
295
  ---
190
296
 
191
- ### Components (Python-first, HTML when you want it)
297
+ ### 4. Components
192
298
 
193
- Components are Python functions decorated with `@component`. Import them with `<!-- @import ... -->` comments and render them with `x-*` tags.
299
+ Components are Python functions decorated with `@component`, imported at the top of a template and rendered as kebab-cased `x-*` tags.
194
300
 
195
- #### Atomic component
301
+ #### Return a string (small, presentational)
196
302
 
197
303
  ```python
198
304
  from casp.component_decorator import component
@@ -200,31 +306,43 @@ from casp.html_attrs import get_attributes, merge_classes
200
306
 
201
307
  @component
202
308
  def Container(children: str = "", **props) -> str:
203
- incoming_class = props.pop("class", "")
204
- final_class = merge_classes("mx-auto max-w-7xl px-4", incoming_class)
309
+ final_class = merge_classes("mx-auto max-w-7xl px-4", props.pop("class", ""))
205
310
  attributes = get_attributes({"class": final_class}, props)
206
- return f'<div {attributes}>{children}</div>'
311
+ return f"<div {attributes}>{children}</div>"
207
312
  ```
208
313
 
209
- #### Type-safe props
314
+ #### Single-file with `html(...)` (small/medium, the common case)
315
+
316
+ Keep markup, server interpolation, and the PulsePoint script inline. Three brace dialects coexist:
317
+ `{{ value }}` is server-side Jinja, `{{ value | json }}` safely serializes into a `<script>`,
318
+ `{# … #}` is a Jinja comment, and `{ value }` is left untouched for PulsePoint.
210
319
 
211
320
  ```python
212
- from typing import Any, Literal
213
- from casp.component_decorator import component
321
+ from casp.component_decorator import component, html
214
322
  from casp.html_attrs import get_attributes, merge_classes
215
323
 
216
- ButtonVariant = Literal["default", "outline", "destructive"]
217
-
218
324
  @component
219
- def Button(children: Any = "", variant: ButtonVariant = "default", **props) -> str:
220
- incoming_class = props.pop("class", "")
221
- attrs = get_attributes({
222
- "class": merge_classes(f"btn btn-{variant}", incoming_class),
325
+ def UserCard(user=None, **props):
326
+ attributes = get_attributes({
327
+ "class": merge_classes("card", props.pop("class", "")),
328
+ "user-name": user["name"],
223
329
  }, props)
224
- return f'<button {attrs}>{children}</button>'
330
+
331
+ # html
332
+ return html("""
333
+ <div {{ attributes }}>
334
+ <h3>{{ user.name }}</h3>
335
+ <button onclick="setLikes(likes + 1)">Likes: {likes}</button>
336
+ <script>
337
+ const [likes, setLikes] = pp.state({{ user.likes | json }});
338
+ </script>
339
+ </div>
340
+ """, attributes=attributes, user=user)
225
341
  ```
226
342
 
227
- #### Template-backed components (when UI is richer)
343
+ > Use a raw string (`r"""…"""`) when the inline `<script>` contains backslashes (regex, `\n`).
344
+
345
+ #### Template-backed (`render_html`) — for large markup
228
346
 
229
347
  `Counter.py`:
230
348
 
@@ -240,7 +358,7 @@ def Counter(label: str = "Clicks") -> str:
240
358
 
241
359
  ```html
242
360
  <div>
243
- <h3>[[ label ]]</h3>
361
+ <h3>{{ label }}</h3>
244
362
  <button onclick="setCount(count + 1)">{count}</button>
245
363
 
246
364
  <script>
@@ -249,168 +367,371 @@ def Counter(label: str = "Clicks") -> str:
249
367
  </div>
250
368
  ```
251
369
 
370
+ #### Importing and rendering
371
+
372
+ ```html
373
+ <!-- @import Container from "../components" -->
374
+ <!-- @import { Button, Card as UserCard } from "../components/ui" -->
375
+ <!-- @import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "../components/Breadcrumb.py" -->
376
+
377
+ <x-container class="py-10">
378
+ <x-button variant="outline">Continue</x-button>
379
+ </x-container>
380
+ ```
381
+
382
+ - `Container` → `<x-container />`, `CommandDialog` → `<x-command-dialog />`.
383
+ - If one Python file exports several components, import them **from that file path**, not from the folder.
384
+ - `@import` is a file-level directive: it must sit **above** the authored root, never inside it.
385
+ - For component-to-component composition, prefer real Python imports inside the component module; a component's own `x-*` tags resolve from its module imports.
386
+
387
+ #### Props: every prop the template reads must be re-emitted on the root
388
+
389
+ This is the most common silent failure in Python components:
390
+
391
+ 1. Attributes on the `x-*` tag arrive in Python as **raw string kwargs** (`open="{permOpen}"` arrives as the literal `"{permOpen}"`), kebab-case converted to camelCase.
392
+ 2. The Python component must deliberately re-emit them onto its single rendered root via `get_attributes({...}, props)` + `{{ attributes }}`.
393
+ 3. PulsePoint derives `pp.props` from **the rendered root's attributes** and evaluates pure `{expr}` values in the parent's scope.
394
+
395
+ A prop accepted by Python but not re-emitted is silently `undefined` in the browser — no server error, no console warning. Forwarding also does not preserve types: a brace expression keeps its real type, a literal (`volume="0"`) arrives as a **string**, a valueless attribute arrives as `true`, `None`/`False`/`""` are omitted entirely, and JS reserved words such as `class` are dropped from `pp.props`.
396
+
397
+ #### Tailwind class merging
398
+
399
+ When `tailwindcss: true`, Python `merge_classes(...)` emits a frontend-ready `{twMerge(...)}` expression, and the browser's global `twMerge(...)` resolves conflicts. That pair is the only supported merge path.
400
+
252
401
  ---
253
402
 
254
- ### PulsePoint reactivity
403
+ ### 5. Data: `pp.rpc()` and `@rpc()`
404
+
405
+ ```js
406
+ await pp.rpc(name, data?, optionsOrAbort?)
407
+ ```
408
+
409
+ - Posts to the **current route**; resolves the `@rpc()` function of that name in the route's `index.py`.
410
+ - Smart serialization — switches to `FormData` automatically when a `File` is present.
411
+ - CSRF token injected as `X-CSRF-Token`.
412
+ - Server redirect headers are honored through `pp.redirect()`.
413
+ - Passing `true` as the third argument means `{ abortPrevious: true }`.
414
+
415
+ Options: `abortPrevious`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, `onUploadComplete`.
416
+
417
+ **Payload safety:** RPC keys are filtered against the function signature, so a parameter is client-settable only when it is declared. Declaring `**kwargs` opts into the whole payload — do that deliberately.
418
+
419
+ #### Upload progress
420
+
421
+ ```html
422
+ <input type="file" onchange="{upload(event.target.files?.[0])}" />
423
+ <progress max="100" value="{percent ?? 0}"></progress>
424
+
425
+ <script>
426
+ const [percent, setPercent] = pp.state(null);
427
+
428
+ async function upload(file) {
429
+ if (!file) return;
430
+ await pp.rpc(
431
+ "upload_asset",
432
+ { file },
433
+ {
434
+ onUploadProgress: ({ percent }) => setPercent(percent),
435
+ onUploadComplete: () => setPercent(100),
436
+ },
437
+ );
438
+ }
439
+ </script>
440
+ ```
441
+
442
+ `onUploadProgress` receives `{ loaded, total, percent }`; `total` and `percent` are `null` when the length is not computable.
255
443
 
256
- PulsePoint is the default reactive frontend layer for Caspian. Key APIs:
444
+ #### Streaming (the path for AI/LLM token output)
257
445
 
258
- | API | Description |
259
- | ---------------------------------- | ------------------------------------------- |
260
- | `pp.state(initial)` | Returns `[value, setValue]` |
261
- | `pp.effect(callback, deps?)` | Runs after render; returns cleanup function |
262
- | `pp.layoutEffect(callback, deps?)` | Runs synchronously after DOM mutation |
263
- | `pp.ref(initialValue?)` | Returns `{ current }` |
264
- | `pp.createContext(defaultValue)` | Creates a context token |
265
- | `<Context.Provider value="{...}">` | Provide context to descendants |
266
- | `pp.context(token)` | Read context in a descendant |
267
- | `pp.portal(ref, target?)` | Portal rendering to a ref target |
268
- | `pp.rpc(name, data?, options?)` | Call a backend RPC action |
269
- | `pp.redirect(url)` | SPA-aware navigation |
446
+ A generator `@rpc()` becomes a `text/event-stream` response:
270
447
 
271
- #### `pp.rpc(functionName, data?)`
448
+ ```python
449
+ @rpc()
450
+ async def ask_question(topic: str):
451
+ async for chunk in llm.stream(topic):
452
+ yield chunk
453
+ ```
272
454
 
273
- The bridge to your Python backend. Caspian handles:
455
+ ```js
456
+ pp.rpc(
457
+ "ask_question",
458
+ { topic },
459
+ {
460
+ onStream: (chunk) => setAnswer((current) => current + chunk),
461
+ onStreamComplete: () => setIsStreaming(false),
462
+ },
463
+ );
464
+ ```
274
465
 
275
- - Smart serialization (JSON FormData when `File` is present)
276
- - CSRF token injection via `X-CSRF-Token`
277
- - Auto-redirects when server returns redirect headers
278
- - Upload progress callbacks when `onUploadProgress` is provided
466
+ Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or a WebSocket.
279
467
 
280
468
  ---
281
469
 
282
- ### Authentication (session-based, secure defaults)
470
+ ### 6. Validation
471
+
472
+ ```python
473
+ from casp.validate import Rule, Validate
474
+
475
+ email = Validate.email(" User@Example.com ") # -> "User@Example.com"
476
+ count = Validate.int("42") # -> 42
477
+ bad = Validate.url("not-a-url") # -> None
478
+
479
+ checked = Validate.with_rules(password, [Rule.REQUIRED, Rule.min(8)])
480
+ if checked is not True:
481
+ return {"error": checked}
482
+ ```
483
+
484
+ `Validate` covers strings and identifiers (`string`, `email`, `url`, `ip`, `uuid`, `ulid`, `cuid`, `cuid2`, `nanoid`), numbers (`int`, `big_int`, `float`, `decimal`), dates (`date`, `date_time`), `boolean`, and structured values (`json`, `enum`, `enum_class`). `Validate.string()` trims and HTML-escapes by default.
283
485
 
284
- Configure auth in `main.py` and customize settings in `src/lib/auth/auth_config.py`.
486
+ Browser-side checks are UX only. Server-side validation at the RPC/route boundary is authoritative.
285
487
 
286
- | Method | Description |
287
- | ---------------------------------- | ----------------------------- |
288
- | `auth.sign_in(data, redirect_to?)` | Sign in a user |
289
- | `auth.sign_out(redirect_to?)` | Sign out (RPC-first) |
290
- | `auth.is_authenticated()` | Check current session |
291
- | `auth.get_payload()` | Get user payload from session |
488
+ ---
489
+
490
+ ### 7. Authentication
491
+
492
+ Session-based, configured centrally in `src/lib/auth/auth_config.py` and wired in `main.py`.
292
493
 
293
494
  ```python
294
- from casp.auth import Auth, GoogleProvider, GithubProvider, configure_auth
495
+ from casp.auth import Auth, GithubProvider, GoogleProvider, configure_auth
295
496
  from src.lib.auth.auth_config import build_auth_settings
296
497
 
297
498
  configure_auth(build_auth_settings())
298
499
  Auth.set_providers(GithubProvider(), GoogleProvider())
299
500
  ```
300
501
 
301
- ---
502
+ | Method | Purpose |
503
+ | ------------------------------------------------------------ | ----------------------------------------------------------------- |
504
+ | `auth.sign_in(data, token_validity=None, redirect_to=False)` | Store the payload, set a CSRF token; returns `"ok"` or a redirect |
505
+ | `auth.sign_out(redirect_to=None)` | Clear the session and redirect |
506
+ | `auth.is_authenticated()` | `False` when the payload is missing, malformed, or expired |
507
+ | `auth.get_payload()` | Read the signed-in payload |
508
+ | `auth.refresh_session()` | Extend expiry when `token_auto_refresh=True` |
509
+ | `auth.check_role(user, allowed_roles)` | RBAC check against the configured role field |
302
510
 
303
- ## CLI reference
511
+ Guards: `@rpc(require_auth=True)` for actions, `@guest_only()` / route-level guards for pages, and central public-vs-private route policy in `auth_config.py`.
304
512
 
305
- ### Create a new project
513
+ **OAuth is already wired.** `Auth.set_providers(...)` registers `/api/auth/signin/{google,github}` and `/api/auth/callback/{google,github}`. Link a button and set the credentials in `.env` — do not hand-roll the flow.
306
514
 
307
- ```bash
308
- npx create-caspian-app my-app
309
- npx create-caspian-app my-app --starter-kit=fullstack
310
- npx create-caspian-app my-app --tailwindcss --typescript --prisma
311
- ```
515
+ **Redirect ownership is centralized.** Do not re-implement `next=` handling or post-login routing in a sign-in page; `auth_config.py` owns protected-route redirects, auth-route redirects, and `default_signin_redirect` (defaults to `/dashboard`).
312
516
 
313
- ### Useful flags
517
+ ---
314
518
 
315
- | Flag | Description |
316
- | --------------------- | ---------------------------------------------- |
317
- | `--backend-only` | Skip frontend assets |
318
- | `--tailwindcss` | Enable Tailwind CSS |
319
- | `--prisma` | Enable Prisma ORM |
320
- | `--mcp` | Enable MCP server scaffolding |
321
- | `--typescript` | Enable TypeScript tooling |
322
- | `--starter-kit=<kit>` | Use a preset (basic, fullstack, api, realtime) |
323
- | `-y` | Non-interactive mode |
519
+ ### 8. Database (Prisma)
324
520
 
325
- ### Update existing project
521
+ Enabled by `"prisma": true`. Define one `prisma/schema.prisma` and generate a typed Python client into `src/lib/prisma/`.
326
522
 
327
- ```bash
328
- npx casp update project
329
- npx casp update project --tag beta
330
- npx casp update project --version 1.2.3 -y
523
+ ```python
524
+ from src.lib.prisma import prisma
525
+
526
+ users = await prisma.user.find_many(
527
+ where={"active": True},
528
+ include={"userRole": True},
529
+ order_by={"createdAt": "desc"},
530
+ )
331
531
  ```
332
532
 
333
- ### ORM regeneration (after schema changes)
533
+ After schema changes, in this order:
334
534
 
335
535
  ```bash
336
536
  npx prisma migrate dev
337
- npx prisma generate
338
- npx prisma db seed
537
+ ```
538
+
539
+ ```bash
339
540
  npx ppy generate
340
541
  ```
341
542
 
543
+ If the seed flow depends on the new schema, run `npx prisma generate` first, then `npx prisma db seed` — **`db seed` may clear or overwrite tables; confirm the datasource before running it.**
544
+
545
+ `src/lib/prisma/__init__.py`, `db.py`, `models.py`, and `settings/prisma-schema.json` are generated by `npx ppy generate`. Never hand-edit them, and never add a second data layer (raw drivers, hand-written SQL helpers, JSON stores, browser-side fetches) alongside the ORM.
546
+
547
+ ---
548
+
549
+ ### 9. Optional features
550
+
551
+ Every optional capability is gated by one flag in `caspian.config.json`. **That file is the single source of truth** — a doc or example mentioning a feature does not mean it is enabled in your project. To turn one on after scaffold, set the flag and run `npx casp update project`.
552
+
553
+ | Flag | Enables |
554
+ | ------------- | -------------------------------------------------------------------- |
555
+ | `backendOnly` | API/service mode with no frontend assets |
556
+ | `tailwindcss` | Tailwind v4 + PostCSS pipeline, `merge_classes` / `twMerge` contract |
557
+ | `typescript` | TypeScript frontend tooling and the Vite build path |
558
+ | `prisma` | Prisma schema, migrations, and the generated Python ORM |
559
+ | `mcp` | A FastMCP server mounted into the same app (`/mcp`) |
560
+ | `websocket` | App-owned FastAPI `@app.websocket(...)` endpoints and socket helpers |
561
+
562
+ #### WebSockets
563
+
564
+ Use RPC for ordinary reads, writes, uploads, and SSE streams. Reach for WebSockets only for **long-lived bidirectional** channels: chat, collaboration, presence, multiplayer state.
565
+
566
+ Endpoints are app-owned in `main.py`; reusable session/auth/broadcast helpers live in `src/lib/websocket/`. In the browser, use PulsePoint for state and lifecycle but the **native** `WebSocket` for the transport — keep the socket in `pp.ref(...)` and close it in a cleanup effect.
567
+
568
+ **Sockets authorize themselves.** The HTTP middleware stack early-returns on `scope["type"] == "websocket"`, so `AuthMiddleware` does not protect them. Every endpoint must run its own origin + auth guard, and authenticated and guest traffic belong in separate broadcast pools.
569
+
570
+ #### MCP
571
+
572
+ When `mcp: true`, a FastMCP server is mounted into the same app so one deploy serves both web and MCP. The endpoint is mounted **outside** the routing tree, so `AuthMiddleware` does not cover it — `MCP_AUTH_TOKEN` is its credential. With no token it stays open in development and returns **503 in production**.
573
+
574
+ ---
575
+
576
+ ## Security defaults
577
+
578
+ Caspian ships fail-closed defaults. Things worth knowing before you change them:
579
+
580
+ - **`APP_ENV` resolves fail-closed.** Only an explicit development value (`dev`, `development`, `local`, `staging`, `test`, `testing`) enables relaxations. Unset or misspelled counts as **production**.
581
+ - **Server-interpolated values never carry live PulsePoint syntax.** The Jinja environment encodes `{` / `}` as entities on every non-`Markup` value, so stored user data can never execute as a template expression. `Markup` is the trust boundary — `| safe`, `get_attributes(...)`, `merge_classes(...)`, and the `json` filter legitimately keep their braces.
582
+ - **Authenticated renders are never cached.** The page cache keys on the URI alone, so a request-eligibility check gates both the read and the write; a route's `Cache(...)` cannot override it.
583
+ - **RPC payload keys are filtered against the function signature.**
584
+ - **`/uploads` serves user content in attachment mode**; only real image types render inline. First-party `/css`, `/js`, and `/assets` stay inline.
585
+ - **CSRF protection, strict Origin validation, HttpOnly cookies, security headers, and page rate limiting** are on by default.
586
+
587
+ Security-relevant environment variables: `MCP_AUTH_TOKEN`, `RATE_LIMIT_PAGES` (default `200/minute`), `CONTENT_SECURITY_POLICY` (replaces the default policy wholesale), `MAX_WEBSOCKET_CONNECTIONS`, `MAX_WEBSOCKET_MESSAGES_PER_WINDOW`, `WEBSOCKET_RATE_WINDOW_SECONDS`, and `WEBSOCKET_ALLOWED_ORIGINS` (**required in production** — the same-origin fallback is derived from the client-supplied `Host` header and is development-only).
588
+
342
589
  ---
343
590
 
344
591
  ## Project structure
345
592
 
346
593
  ```
347
594
  my-app/
348
- ├── main.py # FastAPI entry point
349
- ├── caspian.config.json # Feature flags
595
+ ├── main.py # FastAPI entry point, middleware stack, WebSocket + MCP mounts
596
+ ├── caspian.config.json # Feature flags — the single source of truth
597
+ ├── pyproject.toml # Python deps and tooling config
598
+ ├── package.json # CLI/tooling scripts
350
599
  ├── prisma/
351
600
  │ ├── schema.prisma
352
601
  │ └── seed.ts
353
602
  ├── src/
354
- │ ├── app/ # File-system routes
355
- │ │ ├── layout.html # Root layout
356
- │ │ ├── index.html # Home page
357
- │ │ └── users/
358
- │ │ └── [id]/
359
- │ │ └── index.html # /users/:id
360
- │ ├── components/ # Reusable UI components
361
- │ │ └── ui/
362
- │ │ └── Button.py
363
- └── lib/ # Non-UI helpers
603
+ │ ├── app/ # File-system routes
604
+ │ │ ├── layout.html # Root layout
605
+ │ │ ├── layout.py
606
+ │ │ ├── index.html # Home page
607
+ │ │ ├── index.py
608
+ │ │ ├── globals.css
609
+ ├── error.html
610
+ │ │ ├── not-found.html
611
+ │ │ └── users/[id]/
612
+ │ ├── index.html # /users/:id
613
+ │ │ └── index.py
614
+ │ ├── components/ # Reusable UI (@component)
615
+ │ └── lib/ # Non-UI code
364
616
  │ ├── auth/auth_config.py
365
- └── prisma/
366
- └── db.py
367
- ├── public/ # Static assets
368
- └── settings/ # BrowserSync config
617
+ ├── prisma/ # Generated Python ORM — do not edit
618
+ ├── websocket/ # Socket helpers (when websocket: true)
619
+ │ └── mcp/ # FastMCP server (when mcp: true)
620
+ ├── public/ # Static assets, incl. the PulsePoint runtime and uploads
621
+ └── settings/ # Dev stack config and generated indexes
369
622
  ```
370
623
 
371
- ### Key conventions
624
+ ### Placement rules
372
625
 
373
- - **UI routes:** `index.html` for markup, optional `index.py` for backend logic
374
- - **Section layouts:** `layout.html` for the wrapper, optional `layout.py` for props
375
- - **Components:** `src/components/` for reusable UI; `src/lib/` for helpers and services
376
- - **`<!-- @import ... -->`:** Must appear above the single authored root element
377
- - **Single-root rule:** Every template must have exactly one top-level parent node with any owned `<script>` inside it
626
+ - **Route-owned** logic (first-render query, route `@rpc()` actions, redirects, route validation) stays in that route's `index.py`. Move it to `src/lib/**` only when it is genuinely shared.
627
+ - **Reusable UI** goes in `src/components/`; **helpers, services, adapters** go in `src/lib/`.
628
+ - **Compose pages from components.** A route's `index.html` should read as a short assembly of `x-*` chunks (topbar, sidebar, header, sections, forms, footer), not a wall of markup. Plan the breakdown before writing the route.
629
+ - **Generated, never hand-edited:** `src/lib/prisma/**`, `settings/prisma-schema.json`, `settings/files-list.json`, `settings/component-map.json`, `public/css/styles.css`, `__pycache__/`.
378
630
 
379
631
  ---
380
632
 
381
- ## Built-in icon workflow (ppicons)
633
+ ## CLI reference
382
634
 
383
- Caspian integrates **ppicons** (Lucide-based), offering 1,500+ icons:
635
+ ### Create
384
636
 
385
637
  ```bash
386
- npx ppicons add Rocket
638
+ npx create-caspian-app my-app
387
639
  ```
388
640
 
389
- Then use in HTML:
641
+ | Flag | Description |
642
+ | ---------------------------- | ------------------------------------------------- |
643
+ | `-y` | Non-interactive; skip all prompts |
644
+ | `--backend-only` | API/service project, no frontend assets |
645
+ | `--tailwindcss` | Enable Tailwind CSS |
646
+ | `--typescript` | Enable TypeScript frontend tooling |
647
+ | `--prisma` | Enable Prisma ORM |
648
+ | `--mcp` | Enable MCP server scaffolding |
649
+ | `--starter-kit=<kit>` | `basic`, `fullstack`, `api`, `realtime`, `custom` |
650
+ | `--starter-kit-source=<url>` | Git repository for `--starter-kit=custom` |
651
+ | `--list-starter-kits` | Print the built-in starter catalog |
652
+
653
+ In `-y` mode every feature defaults to `false`. Starter kit presets can still be overridden by explicit flags: `npx create-caspian-app my-app --starter-kit=fullstack --typescript`.
654
+
655
+ `websocket` has no create flag — enable it in `caspian.config.json` after scaffold, then run the update command.
656
+
657
+ ### Update an existing project
658
+
659
+ ```bash
660
+ npx casp update project
661
+ ```
662
+
663
+ Also accepts a positional tag or version, or the named forms `--tag beta`, `--tag=beta`, `--version 1.2.3`, `--version=1.2.3`, plus `-y`. Supplying more than one version source is an error. On Windows the updater resolves `npx.cmd`, and the creator reuses an existing `.venv` rather than recreating it.
664
+
665
+ Use `excludeFiles` in `caspian.config.json` to protect files you have customized (a common entry is `./src/lib/auth/auth_config.py`) from being overwritten on update. Excluded files are yours to merge going forward.
666
+
667
+ ### Project scripts
668
+
669
+ | Command | What it does |
670
+ | ---------------------- | ------------------------------------------------------------------ |
671
+ | `npm run dev` | Full local stack: BrowserSync proxy, Tailwind watch, asset watch |
672
+ | `npm run build` | Build Tailwind and regenerate the route/component index |
673
+ | `npm run static` | Export every static route to `static/` (SSG) |
674
+ | `npm run static:serve` | Preview the exported folder on an auto-selected free loopback port |
675
+
676
+ These are opt-in workflows. Don't run them as a validation step just because source files changed.
677
+
678
+ ---
679
+
680
+ ## Static export (SSG)
681
+
682
+ `npm run static` boots the app and writes `static/<route>/index.html` plus copied public assets — the equivalent of Next.js `output: export`. It always runs `npm run build` first so the export walks a fresh route index.
683
+
684
+ Policy is **warn & skip**: dynamic routes are pre-rendered only when their `index.py` exports `static_paths` (the `getStaticPaths` equivalent); auth-gated, non-200, and non-HTML routes are reported and skipped.
685
+
686
+ `npm run static:serve` serves only `static/`, binds loopback `127.0.0.1` (network exposure is opt-in via `HOST=0.0.0.0`), and walks upward from port 8000 until it finds a free one — **read the port it prints**.
687
+
688
+ `pp.rpc()`, auth, WebSockets, streaming, and per-request server data are all inert in a static export.
689
+
690
+ ---
691
+
692
+ ## Ecosystem
693
+
694
+ ### ppicons — 1,500+ Lucide-based icons as Python components
695
+
696
+ ```bash
697
+ npx ppicons add Rocket
698
+ ```
390
699
 
391
700
  ```html
392
- <!-- @import { Rocket } from "../lib/ppicons" -->
701
+ <!-- @import { Rocket, ChevronDown } from "../lib/ppicons" -->
393
702
  <x-rocket class="w-6 h-6 text-primary" />
394
703
  ```
395
704
 
396
- ---
705
+ ### maddex — shadcn-style UI component kit for Caspian
706
+
707
+ ```bash
708
+ npx maddex add button card dialog
709
+ ```
397
710
 
398
- ## Recommended VS Code extensions
711
+ ```html
712
+ <!-- @import { Button } from "../lib/maddex/Button.py" -->
713
+ <x-button variant="outline">Continue</x-button>
714
+ ```
399
715
 
400
- For the best development experience:
716
+ ---
401
717
 
402
- - **Caspian Official Framework Support** — component snippets and autocomplete
403
- - **Python** — Python language support
404
- - **Prisma** — Schema formatting and highlighting
405
- - **Tailwind CSS IntelliSense** — Class completion and sorting
718
+ ## Recommended VS Code setup
719
+
720
+ - **[Caspian Official Framework Support](https://marketplace.visualstudio.com/items?itemName=JeffersonAbrahamOmier.caspian)** — component snippets and autocomplete (the key piece)
721
+ - **[Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)** — ships Pylance, which is Pyright under the hood
722
+ - **[Prisma](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma)** — schema formatting and highlighting
723
+ - **[Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)** — class completion and sorting
406
724
 
407
725
  ---
408
726
 
409
727
  ## Learn more
410
728
 
729
+ The full documentation ships inside every Caspian project at `node_modules/caspian-utils/dist/docs/` — start with `index.md`, which routes you to the right feature guide.
730
+
411
731
  - Documentation: [caspian.tsnc.tech/docs](https://caspian.tsnc.tech/docs)
412
- - PulsePoint docs: [pulsepoint.tsnc.tech](https://pulsepoint.tsnc.tech)
413
- - ppicons library: [ppicons.tsnc.tech](https://ppicons.tsnc.tech)
732
+ - PulsePoint: [pulsepoint.tsnc.tech](https://pulsepoint.tsnc.tech)
733
+ - Components: [maddex.tsnc.tech/docs](https://maddex.tsnc.tech)
734
+ - Icons: [ppicons.tsnc.tech](https://ppicons.tsnc.tech)
414
735
 
415
736
  ---
416
737