RFC: Plugin Routes & Pages (Phase B)
Status: draft, open for comment. This is the design discussion for the next phase of the plugin system — plugins contributing routes and, eventually, whole pages. Nothing here is shipped; signatures are sketches. Comment by opening an issue on pyxle-dev/pyxle titled RFC(plugin-pages): …. Authors in the Founding Plugin Program co-design this — their build experience against today's API is the primary input.
Where the line is today
Phase A (shipped, stable) lets a plugin run lifecycle hooks, register named services, and contribute ASGI middleware — see the plugins guide. What it cannot do is own a URL: no API endpoints, no pages. Every route in a Pyxle app comes from the app's own pages/ tree.
That line is why pyxle-auth's quickstart still asks you to write pages/api/sign_in.py yourself, and why an admin-panel plugin — the Django-admin moment this framework should eventually have — can't exist yet.
Phase B1 — plugin API routes (proposed, concrete)
The smaller, earlier slice: plugins contribute API endpoints (Starlette-style handlers — no .pyxl compilation, no client bundling).
class PyxleAuthPlugin(PyxlePlugin):
def routes(self) -> Sequence[RouteSpec]:
return [
RouteSpec("POST", "/sign-in", self.sign_in_endpoint,
csrf="same-origin"),
RouteSpec("POST", "/sign-out", self.sign_out_endpoint,
csrf="same-origin"),
]Proposed rules, each open to challenge:
- Namespacing by default. Plugin routes mount under
/api/<plugin-shortname>/…(/api/auth/sign-in). A host app can remount via config; collisions between two plugins are a startup error, not a silent override. - Host wins. If the app's
pages/api/tree defines the same path, the app's endpoint is used and a startup warning names the shadowed plugin route. - CSRF is declared, not assumed. Each route declares its posture:
"token"(default — framework CSRF applies),"same-origin"(Origin/Referer check for pre-auth endpoints that can't have a token yet), or"exempt"(signature-verified webhooks). The config'scsrf.exemptPathsmechanism stays for app routes; plugin routes carry their posture with them so installing a plugin can't silently widen the app's CSRF holes. - Introspectable.
pyxle routes(CLI) lists every route with its origin — app file or plugin name — because debuggability is where mounted-route systems usually rot.
What B1 unlocks immediately: pyxle-auth ships its own sign-in/sign-up/sign-out/reset endpoints and its quickstart shrinks to config plus a <Form>; webhook-receiving plugins (Stripe) own their endpoint with the right CSRF posture out of the box.
Phase B2 — plugin pages (design space, deliberately unresolved)
Plugins shipping real .pyxl pages — sign-in screens, admin panels. The hard questions, with current leanings:
- Compilation. Host-build compiles plugin-shipped
.pyxlsources (leaning: yes — plugins ship sources, the host'spyxle builddiscovers and compiles them; precompiled artifacts would freeze plugins to compiler versions). - Bundling. Plugin client code joins the host's Vite graph — one dedup'd React, plugin chunks hashed like app chunks. This is the riskiest area (version skew, CSS collisions) and needs a prototype before promises.
- Mounting & precedence. Same model as B1: prefix-mounted by default (
/auth/sign-in), host remounts via config, host pages shadow plugin pages with a warning. - Layouts & theming. Plugin pages render inside the host's root layout by default (they should look like your app, not the plugin author's). Styling contract — tokens/CSS variables vs. utility classes — is an open question; this is where founding-author input matters most.
- Security. A plugin page runs the plugin's loaders/actions in your process; B2 doesn't change the trust model (installing a plugin always meant running its code at startup), but route-owning plugins make the audit surface more visible. The directory's review bar (see standards) tightens for page-shipping plugins.
Capability contracts (the bigger principle)
Routes and pages are the how; this is the why. `pyxle-db` didn't just ship a database — it shipped DatabaseLike, a protocol pyxle-auth and any other plugin build against, so the concrete database is swappable. That pattern is the ecosystem's real differentiator, and the intent is to generalise it: framework-defined (or thin-contract-package) interfaces for the capabilities the ideas list keeps circling —
mail.send(...) # MailLike — Postmark, SES, Mailgun, SMTP
storage.put(...) # StorageLike — S3, R2, GCS, local
cache.get(...) # CacheLike — in-memory, Redis— so an app writes mail.send(...) and swaps the provider by config, not code. Pyxle ships the interface; the community ships the adapters. The directory then organises by capability with one recommended provider and alternatives behind it, instead of by package name. This is design intent, not a shipped API; the contracts land alongside the plugins that need them, and DatabaseLike is the template for all of them.
Sequencing
- B1 is targeted for an upcoming minor; it's mostly registry plumbing plus the CSRF-posture mechanism.
- B2 lands after the founding cohort's feedback and a bundling prototype — a wrong API here would be very expensive to walk back, and we'd rather be slow than stuck.
- Capability contracts are incremental — each interface ships with its first real consumer, the way
DatabaseLikealready did.
Open questions (comment on these)
- Should B1 routes be able to declare middleware-style guards (auth required) declaratively, or is "call the guard in the handler" enough?
- Route remounting config: per-plugin prefix, per-route overrides, or both?
- For B2, do plugin pages get access to host services by name only (
plugin("db.database")), or a declared-dependency mechanism with startup validation? - Is "host page shadows plugin page" ever the wrong default? (e.g. a security-critical plugin page an app shouldn't be able to accidentally replace.)