Implement PulsePoint In Your Backend
PulsePoint is open source and the browser contract is small and fully specified. Any server that can print HTML and read JSON can host it — this page is the complete contract, with reference implementations in Node, Python, PHP and Go.
The payoff: your backend and your frontend live in one project. Logic,
data, auth and templates stay server-side in the language your team already
knows; the browser gets fine-grained reactivity without a second codebase, an
API layer or a bundler. And if you want an AI agent to do this integration for
you, hand it llms.md — it contains
everything on this page in machine-consumable form.
Ready to use
Frameworks with PulsePoint already implemented
Start with a framework where PulsePoint is already part of the stack, then focus on your application instead of the integration.
PHP
Prisma PHP
A native PHP full-stack framework that combines Prisma PHP architecture, PulsePoint reactivity, and the Prisma ORM data layer.
Visit framework →Python
Caspian
A reactive Python web framework with PulsePoint already integrated into its server-rendered component model.
Visit framework →What the server must do
- Serve and start the runtime — import
ComponentInitfrompp-reactive-v2.min.jsand callPP.bootstrap()once. - Render deferred component regions — each unique
<template pp-component>boundary holds one root element and its plain<script>. - Set a CSRF cookie named
pp_csrfon page responses. - Answer RPC posts — one middleware that dispatches on the
X-PP-Functionheader. - Optional: SSE streaming, WebSocket named sockets, and
X-PP-Redirect.
Steps 1–2 alone give you a fully reactive read-only page. Steps 3–4 connect components to your functions.
<!-- Put this in the base layout after serving /js/pp-reactive-v2.min.js -->
<script type="module">
import { ComponentInit as PP } from "/js/pp-reactive-v2.min.js";
PP.bootstrap();
</script>
Step 1–2: the markup your server outputs
Produce this with any template engine — Jinja, Blade, Razor, html/template, ERB, string concatenation. PulsePoint does not care how the HTML was made:
<!-- What your server must OUTPUT (any template engine can produce this) -->
<template pp-component="todos_page">
<section>
<form onsubmit="add(event)">
<input name="title" />
<button>Add</button>
</form>
<ul>
<template pp-for="todo in todos">
<li key="{todo.id}">{todo.title}</li>
</template>
</ul>
<script>
const [todos, setTodos] = pp.state([]);
pp.effect(() => { pp.rpc("listTodos").then(setTodos); }, []);
const add = async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
const created = await pp.rpc("addTodo", data);
setTodos([...todos, created]);
event.currentTarget.reset();
};
</script>
</section>
</template>
Two rules matter when generating it server-side:
- Escape user data twice-over: HTML-escape it as usual, and encode literal braces (
{/}) so stored input like{fetch(...)}can never execute as a template expression. This is PulsePoint's one security-critical rule. - Brace collision: if your template engine also uses
{}, configure delimiters or emit PulsePoint braces literally — the two must not overlap.
Three richer output shapes are part of the same contract when your template layer
supports component composition: children passed into a component travel as
<template pp-owner="parent_id"> inside the child, a component whose
root is another component renders as a display: contents host with its
script projected as slot content, and a multi-root component is framed with
<!--pp:id--> … <!--/pp--> comment markers. All three are
specified in Components.
Step 3–4: the RPC contract
pp.rpc("addTodo", data) sends POST to the current route URL (or options.url) with these headers:
| Header | Meaning |
|---|---|
X-PP-RPC: true |
Identifies a PulsePoint RPC request. Route on this. |
X-PP-Function: <name> |
The server-side function to invoke. |
X-PulsePoint-Wire: true |
Wire-format marker. |
X-CSRF-Token: <token> |
Must equal the pp_csrf cookie value. |
X-Requested-With: XMLHttpRequest |
Standard AJAX marker. |
Accept: application/json, text/event-stream |
The client accepts JSON or an SSE stream. |
The body is application/json, or multipart/form-data when any
value is a File/FileList. The server looks the function up in an
explicit per-route registry (never eval a client-supplied name),
filters payload keys against the function's declared parameters, and returns
JSON. Non-2xx responses reject the promise; include
{"error": "message"} in the body.
Node.js / Express
// Express — one middleware implements the whole RPC contract
import express from "express";
import crypto from "node:crypto";
import cookieParser from "cookie-parser";
const app = express();
app.use(cookieParser(), express.json(), express.static("public"));
// Explicit per-route function registry — never eval arbitrary names.
const rpc = {
"/todos": {
listTodos: async () => db.todos.all(),
addTodo: async ({ title }) => db.todos.create({ title }),
},
};
// CSRF cookie on page loads (client JS must be able to read it).
app.use((req, res, next) => {
if (!req.cookies.pp_csrf) {
res.cookie("pp_csrf", crypto.randomUUID(), { sameSite: "lax" });
}
next();
});
// The RPC contract.
app.post("*", (req, res, next) => {
if (req.get("X-PP-RPC") !== "true") return next();
if (req.get("X-CSRF-Token") !== req.cookies.pp_csrf) {
return res.status(403).json({ error: "CSRF token mismatch" });
}
const fn = rpc[req.path]?.[req.get("X-PP-Function")];
if (!fn) return res.status(404).json({ error: "Unknown function" });
Promise.resolve(fn(req.body ?? {}))
.then((result) => res.json(result ?? null))
.catch((err) => res.status(500).json({ error: err.message }));
});
app.get("/todos", (req, res) => res.send(renderTodosPage()));
Python / FastAPI
# FastAPI — same contract, Python flavor
import secrets, inspect
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/js", StaticFiles(directory="public/js"), name="js")
RPC = {
"/todos": {
"listTodos": lambda: db.todos.all(),
"addTodo": lambda title: db.todos.create(title=title),
},
}
@app.middleware("http")
async def pulsepoint(request: Request, call_next):
if request.headers.get("X-PP-RPC") == "true" and request.method == "POST":
token = request.cookies.get("pp_csrf")
if not token or request.headers.get("X-CSRF-Token") != token:
return JSONResponse({"error": "CSRF token mismatch"}, status_code=403)
fn = RPC.get(request.url.path, {}).get(request.headers.get("X-PP-Function", ""))
if fn is None:
return JSONResponse({"error": "Unknown function"}, status_code=404)
payload = await request.json()
# Filter payload to declared parameters only.
allowed = set(inspect.signature(fn).parameters)
result = fn(**{k: v for k, v in payload.items() if k in allowed})
return JSONResponse(result)
response = await call_next(request)
if "pp_csrf" not in request.cookies:
response.set_cookie("pp_csrf", secrets.token_urlsafe(32), samesite="lax")
return response
@app.get("/todos")
def todos_page() -> HTMLResponse:
return HTMLResponse(render("todos.html"))
PHP
<?php // Plain PHP — front controller
if (empty($_COOKIE['pp_csrf'])) {
setcookie('pp_csrf', bin2hex(random_bytes(16)), ['samesite' => 'Lax', 'path' => '/']);
}
$isRpc = ($_SERVER['HTTP_X_PP_RPC'] ?? '') === 'true'
&& $_SERVER['REQUEST_METHOD'] === 'POST';
if ($isRpc) {
header('Content-Type: application/json');
if (($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '') !== ($_COOKIE['pp_csrf'] ?? null)) {
http_response_code(403);
exit(json_encode(['error' => 'CSRF token mismatch']));
}
$registry = [
'/todos' => [
'listTodos' => fn() => Todo::all(),
'addTodo' => fn(string $title) => Todo::create($title),
],
];
$fn = $registry[parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)]
[$_SERVER['HTTP_X_PP_FUNCTION'] ?? ''] ?? null;
if (!$fn) { http_response_code(404); exit(json_encode(['error' => 'Unknown function'])); }
$payload = json_decode(file_get_contents('php://input'), true) ?? [];
exit(json_encode($fn(...$payload)));
}
echo renderTodosPage(); // prints the pp-component markup
Go
// Go — net/http
func pulsePointRPC(next http.Handler) http.Handler {
registry := map[string]map[string]func(json.RawMessage) (any, error){
"/todos": {
"listTodos": func(_ json.RawMessage) (any, error) { return db.All(), nil },
"addTodo": func(raw json.RawMessage) (any, error) {
var in struct{ Title string `json:"title"` }
if err := json.Unmarshal(raw, &in); err != nil { return nil, err }
return db.Create(in.Title), nil
},
},
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-PP-RPC") != "true" || r.Method != http.MethodPost {
next.ServeHTTP(w, r)
return
}
cookie, _ := r.Cookie("pp_csrf")
if cookie == nil || r.Header.Get("X-CSRF-Token") != cookie.Value {
http.Error(w, `{"error":"CSRF token mismatch"}`, http.StatusForbidden)
return
}
fn := registry[r.URL.Path][r.Header.Get("X-PP-Function")]
if fn == nil {
http.Error(w, `{"error":"Unknown function"}`, http.StatusNotFound)
return
}
body, _ := io.ReadAll(r.Body)
result, err := fn(body)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
})
}
Streaming responses
Respond with Content-Type: text/event-stream and the same RPC call
becomes a stream — ideal for LLM output and progress feeds:
# Any backend: respond with text/event-stream to stream chunks
# The client consumes them via pp.rpc options:
#
# pp.rpc("generate", { prompt }, {
# onStream: (chunk) => setText((t) => t + chunk),
# onStreamComplete: () => setDone(true),
# });
#
# Server (FastAPI example):
@app.post("/ai") # reached via the X-PP-RPC middleware above
async def generate(prompt: str):
async def stream():
async for chunk in llm.stream(prompt):
yield f"data: {json.dumps(chunk)}\n\n"
return StreamingResponse(stream(), media_type="text/event-stream")
Server-driven redirects
An RPC response carrying an X-PP-Redirect: /target header (or
Location) makes the client navigate, SPA-aware. Cross-origin targets are
ignored by the client.
Named sockets (optional)
pp.socket("room", args, handlers) connects to the single endpoint
/__pulsepoint/ws?name=room. The first client frame is one JSON object
(the arguments); every later frame in either direction is one JSON value; a
server frame {"error": "..."} signals failure and precedes the close.
Production servers must check the Origin header, cap connections and
bound message size and rate. The client API, component pattern and a full
wire walkthrough live in WebSockets.
Deferred roots
<!-- Every outermost reactive region starts inside an inert template.
The runtime materializes it before scanning component roots. -->
<template pp-component="todos_page">
<section>
…
</section>
</template>
Integration checklist
- Copy
pp-reactive-v2.min.jsinto static assets; importComponentInitfrom it and callPP.bootstrap()once in the base layout. - Generate unique
pp-componentids on the<template>boundaries in your template layer. - Set the
pp_csrfcookie on page responses; verifyX-CSRF-Tokenon RPC posts. - Add the RPC middleware with an explicit function registry per route.
- Escape user data and encode its braces.
- Optional: SSE streaming,
/__pulsepoint/ws, andX-PP-Redirect.
That is the entire surface. Everything else — routing, auth, sessions, databases, deployment — stays exactly how your backend already does it.