Clients

@pramen/client

A typed client: call() is RPC over HTTP, subscribe() is a live query over an auto-reconnecting WebSocket. It is generic over your server's handler map, so calls are fully typed with no runtime dependency on the server — import the type only.

import { createClient } from "@pramen/client";
import type { app } from "../server/app"; // type-only, erased at build

const pramen = createClient<typeof app.handlers>({ url, token, tenant: "acme" });

const note = await pramen.call("createNote", { title: "hi", body: "..." }); // typed
const stop = pramen.subscribe("listNotes", undefined, { onData: (notes) => render(notes) });

Fronting pramen with a meta-framework? Handlers are matched at the exact path POST /rpc/<handler>. A framework that enforces trailing slashes (e.g. Astro's trailingSlash: 'always') 308-redirects /rpc/x/rpc/x/, and browsers drop the POST body on the redirect — so the call silently arrives empty. Set trailingSlash: 'ignore' (or 'never') for the API routes.

@pramen/react

Hooks that re-render on every server push:

import { useLiveQuery, useMutation } from "@pramen/react";

function Notes({ pramen }) {
  const { data, loading } = useLiveQuery(pramen, "listNotes");
  const createNote = useMutation(pramen, "createNote");

  if (loading) return <p>Loading…</p>;
  return (
    <>
      <ul>{data.map((n) => <li key={n.id}>{n.title}</li>)}</ul>
      <button onClick={() => createNote({ title: "New", body: "" })}>Add</button>
    </>
  );
}

Because the client is generic over typeof app.handlers, handler names, inputs, and result shapes are all checked at compile time — a typo or a wrong input type is a build error, not a runtime surprise.