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. If you want an AI agent to do this integration for you,
hand it llms.md.
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
ComponentInitand 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 by dispatching on the
X-PP-Functionheader. - Optional: SSE streaming, WebSocket named sockets, redirects and SPA navigation support.
Steps 1–2 alone give you a fully reactive read-only page. Steps 3–4 connect components to your functions.
Step 1–2: the markup your server outputs
Your base layout should emit a complete document like this. The server owns the document shell and component boundaries; PulsePoint only evaluates the browser-side expressions inside them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PulsePoint V2</title>
<script type="module">
import { ComponentInit as PP } from "/js/pp-reactive-v2.min.js";
PP.bootstrap();
</script>
</head>
<body>
<template pp-component="main_layout_06cde1cb">
<slot />
</template>
</body>
</html>
- Escape user data as normal HTML and encode literal braces as
{and}so stored input cannot become a template expression. - If your template engine also uses braces, configure non-overlapping delimiters or emit PulsePoint braces literally.
- Replace the server-side
<slot />with the rendered page component when your layout engine composes a page into the shell.
The component or page inside the layout
The next layer is an ordinary page component. It owns its markup, browser state and event handlers, while the server owns the component boundary and the initial document.
<!-- A page component rendered inside the layout above -->
<template pp-component="todos_page_06cde1cb">
<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>
Step 3–4: the RPC contract
pp.rpc("addTodo", data) sends POST to the current route URL, or to options.url, with these headers:
| Header | Meaning |
|---|---|
X-PP-RPC: true | Identifies a PulsePoint RPC request. |
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 JSON, or multipart/form-data when a value is a File. Look up the function in an explicit per-route registry, filter payload keys against declared parameters, and return JSON. A function with no result returns null.
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": "Please fix the highlighted fields.",
"errors": { "email": ["Already registered"] },
"requestId": "req_7f3a9c"
}
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"));
const rpc = {
"/todos": {
listTodos: async () => db.todos.all(),
addTodo: async ({ title }) => db.todos.create({ title }),
},
};
app.use((req, res, next) => {
if (!req.cookies.pp_csrf) {
res.cookie("pp_csrf", crypto.randomUUID(), { sameSite: "lax" });
}
next();
});
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 }));
});
Python / FastAPI
# FastAPI — the same contract in Python
import inspect
import secrets
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, HTMLResponse
app = FastAPI()
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()
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
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)));
}
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
pp.rpc("generate", { prompt }, {
onStream: (chunk) => setText((t) => t + chunk),
onStreamComplete: () => setDone(true),
});
# FastAPI example
@app.post("/ai")
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 a same-origin Location, makes the client navigate with the SPA behavior enabled.
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; every later frame is one JSON value. A server error frame precedes the close. Production servers must check the Origin header and bound connections, message size and rate. The client API and wire walkthrough are in WebSockets.
- Answer the heartbeat. Reply to
{"__pp": "ping"}with{"__pp": "pong"}and do not pass heartbeat frames to the handler. - Close with the right code. Use
1000for a finished handler,4000for idle timeout, and1008/1009for policy and size violations. - Accept a fresh call per connection. Reconnects send the argument frame again, so the handler starts from the top.
SPA navigation (optional)
When the page starts the runtime with pp.mount(), same-origin link clicks become a GET carrying X-PP-Navigation: true. Answer it with the same full HTML document you always render.
- Managed head tags: mark per-page description, canonical, robots and social tags with
data-pp-meta. - Root layouts: send
X-PP-Root-Layout: <id>and render the same id as<meta name="pp-root-layout">. - Errors: non-2xx navigations and slow responses fall back to a full page load.
<head>
<title>Orders — Acme</title>
<meta name="description" content="Your recent orders" data-pp-meta />
<meta property="og:title" content="Orders — Acme" data-pp-meta />
<link rel="canonical" href="https://acme.test/orders" data-pp-meta />
<meta name="pp-root-layout" content="shop" />
</head>
Scroll restoration, loading UI and navigation events are covered in SPA Navigation.
Layouts that wrap pages
If your template layer renders a layout as its own component, its deferred <template pp-component> may hold the page boundary as its root. The runtime keeps both identities so page-owned slot content still resolves in the page scope.
<!-- Server output: the layout's deferred template contains the page boundary -->
<template pp-component="shop_layout">
<main pp-component="orders_page">
…page markup, including <template pp-owner="orders_page"> slot content…
<script>/* page script */</script>
</main>
</template>
Deferred roots
Every outermost reactive region should start in an inert template. The browser does not execute its script, fetch its bound URLs or validate its bound form values until PulsePoint materializes it.
<!-- Every outermost reactive region starts inside an inert template. -->
<template pp-component="todos_page">
<section>
…page markup and its script…
</section>
</template>
Integration checklist
- Serve
pp-reactive-v2.min.jsand callComponentInit.bootstrap()once in the base layout. - Generate unique
pp-componentids on deferred boundaries. - Set the
pp_csrfcookie and verifyX-CSRF-Tokenon RPC posts. - Dispatch RPC calls through an explicit function registry and filter payload keys.
- Escape user data and encode literal braces.
- Return JSON, including
nullfor empty results, and use the documented error shape. - For SPA navigation, mark per-page head tags with
data-pp-metaand keep the root layout id consistent. - Optional: add SSE streaming,
/__pulsepoint/wsand server redirects.
Upgrading an existing integration? The delta for the current runtime is listed in Runtime Changelog.
That is the entire surface. Everything else — routing, auth, sessions, databases and deployment — stays exactly how your backend already does it.