Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Pyxle is a Python-first full-stack web framework that brings the Next.js developer experience to the Python ecosystem. Write server logic in Python, UI in React, and ship them together in `.pyxl` files.

**Current version:** 0.5.0 (beta)
**Status:** beta (`0.x`) — see the [changelog](changelog.md) for the current release.

## What's new in 0.5.0

Expand Down
8 changes: 8 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ Release notes for Pyxle. While we're in beta (`0.x`), minor versions may include
- **Fix: the CSRF cookie no longer collides between Pyxle apps on the same host.** The cookie was literally `pyxle-csrf` with `Path=/`, and browsers scope cookies to a host — ignoring the port — so two Pyxle dev servers on `127.0.0.1` kept overwriting each other's token and every action in the other app failed with `403 CSRF token mismatch`. The default cookie name is now namespaced by the app's bind port (`pyxle-csrf-8000`), a stable per-app discriminator in dev *and* behind a production reverse proxy, and the document shell injects the effective name into every page (`window.__PYXLE_CSRF_COOKIE__`) so `<Form>`/`useAction` always read the right cookie. Pin an explicit name with the existing `csrf.cookieName` key. **Upgrade note:** a leftover `pyxle-csrf` cookie from an earlier version is simply ignored — the token re-issues under the namespaced name on the next page load; no action needed. See [Security → CSRF protection](guides/security.md#csrf-protection) and [Configuration → CSRF](reference/configuration.md#csrf).
- **Fix: multipart forms (file uploads) now pass CSRF with a `_csrf_token` field.** Posting an action as `multipart/form-data` with a correct `_csrf_token` field was rejected with `403 CSRF token missing` — the middleware only read urlencoded bodies and the `X-CSRF-Token` header, so a progressive-enhancement `<Form>` containing a file input was broken before hydration. The middleware now stream-parses a multipart body just far enough to obtain `_csrf_token` (which `<Form>` renders before all other fields) and then replays the consumed bytes downstream untouched — file parts are never buffered by the CSRF layer, so uploads of any size work. The pre-token scan is capped at 1 MiB; past the cap (or when the field is absent) the request is rejected with a `403` that says exactly what to do: send the token in the `X-CSRF-Token` header, or place `_csrf_token` before the file fields. `missing` vs `mismatch` errors are now accurate on every path, including a new `CSRF cookie missing` message when a token is submitted without its cookie half. See [Security → Form bodies and uploads](guides/security.md#form-bodies-and-uploads).

### Documentation
- **The flagship WebSockets example no longer crashes SSR when copy-pasted.** The first example in the [WebSockets guide](guides/websockets.md) passed `window.location.pathname` to `useWebSocket()` — evaluated during the server render, where `window` doesn't exist, so following the guide verbatim 500'd the page. The example now derives the socket path from the route's loader data (the loader returns the `room` segment; the component templates it into the same path the page was served from), and a callout explains the rule: the hook never *connects* during SSR, but its arguments are evaluated there — build paths from props/params, not browser globals.
- **Fixed a broken `@action` example in the [pyxle-db docs](plugins/pyxle-db.md).** The request-scoped-access example inserted an undefined `title` instead of reading the request body; it now uses `body = await request.json()` (`request.json` is an async method — subscripting it without awaiting fails at runtime), matching the [Server Actions](core-concepts/server-actions.md) page.
- **Documented CSRF on the pyxle-auth credential endpoints.** `POST /auth/login` / `/auth/signup` / `/auth/logout` are CSRF-protected like every state-changing POST, so a raw curl per the old docs got `403` with no explanation. A new [CSRF subsection](plugins/pyxle-auth.md#csrf) states this, explains the double-submit dance (any GET issues the token cookie; echo it via the `X-CSRF-Token` header), notes that `useAuth()` does it automatically, and shows a working curl sequence.
- **Documented where pyxle-auth accounts live.** A new [Where accounts live](plugins/pyxle-auth.md#where-accounts-live) section covers the SQLite database auto-created at `data/app.db` on first run (no manual migration step) and how to point at a production database via pyxle-db's `url` setting; the [pyxle-db settings docs](plugins/pyxle-db.md#plugin-settings) now also state that the file and its directory are created automatically.
- **Honest scope note on `pyxle check`.** The [CLI reference](reference/cli.md#pyxle-check) now spells out that a green check validates syntax and semantics but does not prove a page renders — runtime-only mistakes (e.g. a component reading a loader key that doesn't exist) surface when the page loads, not in `check`. It also notes that as of 0.7.0 the JSX level works out of the box, since pyxle-langkit ships with the framework (previously the `[langkit]` extra).
- **Prerequisites up front.** The [Introduction](getting-started/introduction.md) now says on page one that Pyxle needs Python 3.10+ (3.12 recommended) and Node.js 18+ with npm, matching [Installation](getting-started/installation.md).

## 0.6.1 — 2026-07-01

A sharper `pyxle check` — the edit → check → fix loop now catches classes of mistake it used to wave through.
Expand Down
4 changes: 3 additions & 1 deletion docs/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Pyxle is a Python-first full-stack web framework that brings the Next.js developer experience to the Python ecosystem. You write your server logic in Python, your UI in React, and ship them together in a single `.pyxl` file — no separate API layer, no second service, no glue code between them.

**Current version:** 0.5.0 (beta)
**Status:** beta (`0.x`) — see the [changelog](../changelog.md) for the current release.

> **Prerequisites:** Python **3.10+** (3.12 recommended) and **Node.js 18+** with npm — Pyxle drives Vite and React under the hood. See [Installation](installation.md).

## One file, two languages

Expand Down
24 changes: 20 additions & 4 deletions docs/guides/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ over HTTP and a live connection over WS.

from pyxle.realtime import channel


@server
async def load(request):
return {"room": request.path_params["room"]}


async def websocket(ws):
await ws.accept()
room = ws.path_params["room"]
Expand All @@ -23,8 +29,8 @@ async def websocket(ws):
import React from 'react';
import { useWebSocket } from 'pyxle/client';

export default function Chat() {
const { status, send, lastMessage } = useWebSocket(window.location.pathname);
export default function Chat({ data }) {
const { status, send, lastMessage } = useWebSocket(`/chat/${data.room}`);
// …render the chat UI…
}
```
Expand All @@ -33,6 +39,14 @@ Open `/chat/lobby` and you get the page; the client hook upgrades the **same
path** to a WebSocket. Two browsers in `/chat/lobby` exchange messages in real
time; `/chat/general` is a separate room.

> **Derive the path from props, not `window`.** The component renders on the
> server first, and the hook's *arguments* are evaluated during that render —
> only the connection waits for the browser. Reaching for a browser global at
> render scope (`useWebSocket(window.location.pathname)`) therefore crashes SSR
> with `window is not defined`. Build the path from route data instead, as
> above: the loader returns the dynamic segment and the component templates it
> into the same path the page was served from.

## The `websocket` handler

Detection is by **convention**, not a decorator: a module-scope coroutine named
Expand Down Expand Up @@ -212,8 +226,10 @@ async def websocket(ws):
## The `useWebSocket()` client hook

`useWebSocket(path, options?)` connects from the browser with auto-reconnect,
JSON parsing, and connection state. It **never connects during SSR** and
reconnects with exponential backoff (capped, with jitter).
JSON parsing, and connection state. It **never connects during SSR** — though
its arguments are still evaluated there, so derive `path` from props or loader
data, never from `window` — and reconnects with exponential backoff (capped,
with jitter).

```jsx
import { useWebSocket } from 'pyxle/client';
Expand Down
39 changes: 39 additions & 0 deletions docs/plugins/pyxle-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ async def endpoint(request: Request) -> JSONResponse:

`sign_up` has the same shape and also returns `(user, cookie)`. `sign_out(cookie_value=...)` returns a cookie that clears the browser's copy — set it the same way.

## Where accounts live

pyxle-auth stores its records — users, sessions, rate-limit buckets, tokens, roles — in whatever database [pyxle-db](pyxle-db.md) opens. With no pyxle-db settings, that's a SQLite file **auto-created at `data/app.db` on first run**: the plugin creates the directory and the file, and pyxle-auth's bundled migrations build its tables at startup. There is no manual migration or `create-db` step — the Quickstart config above boots against an empty project and just works.

For production, point pyxle-db at your real database and pyxle-auth follows — its migrations apply there at startup exactly as they do to SQLite:

```json
{
"plugins": [
{ "name": "pyxle-db", "settings": { "url": "env:DATABASE_URL" } },
"pyxle-auth"
]
}
```

The `env:` indirection keeps credentials out of the committed config — see [pyxle-db → Plugin settings](pyxle-db.md#plugin-settings). Remember the SQLite file is your user table: back it up like one, and never commit or deploy a local `data/app.db` over a production database. (To back pyxle-auth with something other than pyxle-db entirely, see [Bring your own database](#bring-your-own-database).)

## Identity model: email or username

By default accounts are identified by **email** — exactly as the examples above. To build a username-based app (any available handle, no email or phone required), set `identifier` to `"username"`:
Expand Down Expand Up @@ -176,6 +193,28 @@ function Account() {

The signed-in user is **seeded into the server render** (`window.__PYXLE_AUTH__`), so `useAuth` shows the right state on the first frame — no flash of "logged out", no extra round-trip.

### CSRF

`POST /auth/login`, `/auth/signup`, and `/auth/logout` are state-changing endpoints, so the framework's CSRF protection guards them like any other POST — a request that doesn't carry the token is rejected with `403` ("CSRF token missing"). `useAuth()` handles this for you; you only deal with it when calling the endpoints directly (curl, an HTTP client, an integration test).

The dance is the standard double-submit pattern: any ordinary `GET` response issues the Pyxle CSRF cookie (responses marked publicly cacheable skip it), and the POST must echo that cookie's value back in the `X-CSRF-Token` header (alongside the cookie itself):

```bash
# 1. Any GET issues the CSRF cookie — capture it in a cookie jar
curl -s -c jar.txt http://localhost:8000/auth/me > /dev/null

# 2. The token is the value of the Pyxle CSRF cookie
TOKEN=$(awk '$6 ~ /csrf/ { print $7 }' jar.txt)

# 3. POST with the cookie jar, echoing the token in the header
curl -s -b jar.txt -H "X-CSRF-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "ada@example.com", "password": "correct horse battery"}' \
http://localhost:8000/auth/login
```

The cookie and header names (and how to customise or exempt paths from the check) are documented in [Security → CSRF protection](../guides/security.md#csrf-protection). For token-authenticated API clients that can't run this dance, see the exempt-paths note under [JWT](#jwt-for-api--mobile-clients).

## Guards

Drop-in checks for loaders, actions, and API routes:
Expand Down
7 changes: 5 additions & 2 deletions docs/plugins/pyxle-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def load(request):

## Plugin settings

All settings are optional — with none, you get SQLite at `./data/app.db`.
All settings are optional — with none, you get SQLite at `./data/app.db`. There is no init step: on first startup the plugin creates the `data/` directory and the database file itself, then applies any pending migrations. (Plugins built on pyxle-db inherit this — [pyxle-auth](pyxle-auth.md#where-accounts-live)'s user records land in this same file until you point `url` at a real server.)

| Key | Default | What it does |
|---|---|---|
Expand Down Expand Up @@ -156,7 +156,10 @@ async def load(request):

@action
async def create_post(request):
await request.state.db.execute("INSERT INTO posts (title) VALUES (?)", (title,))
body = await request.json()
await request.state.db.execute(
"INSERT INTO posts (title) VALUES (?)", (body["title"],)
)
return {"ok": True} # committed automatically on success
```

Expand Down
10 changes: 10 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ Validate `.pyxl` files, configuration, and dependencies without starting the ser
- **Python semantics** (via pyflakes): undefined names — e.g. a symbol you `raise` but never imported — unused imports, redefinitions. Compiler-injected runtime names (`server`, `action`, `LoaderError`, `ActionError`, …) are recognized, so the idiomatic patterns never read as undefined.
- **JSX syntax** (via Babel): unclosed tags/expressions, mismatched braces, TypeScript in a client block, and **duplicate `export default`** (which Babel accepts but the build rejects).

As of 0.7.0 the JSX level works out of the box — `pyxle-langkit`, which provides the Babel-based checker, ships with the framework. On earlier versions it required the `[langkit]` extra (`pip install 'pyxle-framework[langkit]'`); without it, the JSX check reported itself unavailable.

> **What a green check proves — and what it doesn't.** `pyxle check` is a
> static gate: it validates `.pyxl` syntax, Python semantics, and JSX syntax.
> It does **not** render your pages, so a mistake that only exists at runtime —
> a component reading `data.posts` when the loader returns `{"items": ...}`, a
> loader whose query fails against the real database — surfaces when the page
> renders, not here. Treat a clean check as "this compiles"; loading the page
> under `pyxle dev` (or a test that requests it) remains the runtime proof.

```bash
pyxle check [directory] [options]
```
Expand Down
Loading