@syncropel/renderer
v0.5.0
Published
React renderer for Syncropel workspaces. Mount Syncropel workspace manifests in any React app — Next.js, Vite, Astro, Remix, Electron, Tauri, or your own.
Maintainers
Readme
@syncropel/renderer
React renderer for Syncropel workspaces. Mount
core.workspace.v1manifests in any React app — Next.js, Vite, Astro, Remix, Electron, Tauri, or your own.
Workspaces are portable: your app, your records, your renderer.
npm install @syncropel/renderer @tanstack/react-queryQuick start
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
RendererProvider,
WorkspaceRenderer,
WorkspaceBootstrap,
} from "@syncropel/renderer";
const queryClient = new QueryClient;
export function App {
return (
<QueryClientProvider client={queryClient}>
<RendererProvider
fetcher={authedFetch}
apiBase="https://api.example.com"
catalogUrl="https://catalog.syncropel.com"
onAction={(action, record) => console.log("action:", action, record)}
>
<WorkspaceBootstrap />
<WorkspaceRenderer
did="did:sync:syncropel-team/dashboard"
fallback={<p>Workspace unavailable</p>}
/>
</RendererProvider>
</QueryClientProvider>
);
}<RendererProvider> is optional — every prop has a sensible default for browser hosts. Drop it entirely if you don't need authenticated requests, custom catalogs, or typed action handlers; the renderer falls back to plain fetch, window.location.origin, and a window.CustomEvent action dispatch.
That's it. The renderer:
- Resolves
did:sync:syncropel-team/dashboardagainst the public catalog (https://catalog.syncropel.com). - Falls back to your local Syncropel instance if the catalog misses.
- Validates the manifest.
- Mounts each declared component, fetching records via the registered projection (
table,feed,metric, orsrp). - Renders your
fallbackif the manifest can't be resolved.
What this package is
A Syncropel workspace is a core.workspace.v1 record that declares an interactive UI — pages, views, projection names, action labels — over your Syncropel records. This package mounts that record as React.
Every workspace — published by Syncropel or by anyone else — is the same kind of artifact, rendered the same way. One renderer; mountable anywhere a React tree exists.
What it isn't
- Not the data client. Use
@syncropel/sdkto emit and query records. - Not the design system. Use
@syncropel/reactfor tokens, atoms, and molecules. This package consumes CSS variables but ships no styling decisions of its own. - Not the schema authority.
@syncropel/configships the canonical JSON Schemas. This package re-uses the renderer-relevant types. - Not the iframe extension SDK. Use
@syncropel/extensionsfor sandbox-isolated extensions. This package mounts in-process React components.
Boundary
This package is the portable React layer in a deliberate 3-layer cake:
Host (Next.js / Electron / Tauri / future browser-instance)
↓ wraps + configures via <RendererProvider>
Workspace Orchestrator (the future @syncropel/workspace)
↓ imports + composes
@syncropel/renderer ← you are hereThe renderer is identity-blind and orchestrator-blind (ADR-071 D8): it MUST NOT own connection state, consent enforcement, capability gating, extension lifecycle, federation policy, or thread subscription. Those are orchestrator/host concerns. The renderer's job is (manifest, providerConfig) → React.ReactNode.
The host injects 5 fields via <RendererProvider>: fetcher (authenticated), apiBase, catalogUrl, errorReporter, onAction. Not in the contract: instance, did, actor, auth_token — the renderer never sees user identity.
For the full normative boundary table, the rationale (portability thesis + per-instance origins + browser-instance horizon), and the accidental-coupling inventory in the canonical syncropel-web host, read syncropel-research/docs/thread-projection-convergence/02-renderer-host-boundary.md (ratified 2026-05-15) + ADR-071 (renderer capability contract) + ADR-072 (workspace orchestrator contract).
Concepts in 60 seconds
- Workspace — a
core.workspace.v1record withname,publisher_did,lifecycle, andcomponents[]. Identified by a DID of the formdid:sync:<publisher>/<slug>. - Component — one entry in
components[]. Has anid, akind(viewis fully supported in this release), and projection wiring. - Projection — a registered React component that takes records, options, and className, and renders UI. Four ship out of the box:
table,feed,metric, andsrp(renders a Syncropel Rendering Protocol document via@syncropel/react's<SRPRenderer>). Hosts can register more viaProjectionRegistry.register(...). - Manifest fetcher — resolves a DID by querying the catalog (
POST /v1/records/query) and falling back to the local instance. Cached in an in-memory LRU (32 entries, 5-minute TTL). - Action — a row-level button declared in a manifest's
projection_options. Clicks dispatch a typedWorkspaceActionEventDetailviawindow.dispatchEvent. (A future release adds a typed React context as the canonical mechanism.)
API tour
<WorkspaceRenderer>
<WorkspaceRenderer
did="did:sync:syncropel-team/dashboard"
fallback={<DashboardFallback />}
catalogUrl="https://catalog.example.com" // optional override
hideHeader // hide the workspace metadata header
manifest={preResolvedManifest} // skip the catalog hop entirely
recordsByComponent={{ "live-feed": [...] }} // skip the server hop too (test mode)
/>useWorkspaceManifest(did, options)
import { useWorkspaceManifest } from "@syncropel/renderer/hooks";
function Custom {
const { manifest, isLoading, isError, error } = useWorkspaceManifest(
"did:sync:alice/timetracker-pro",
);
// ...render manually
}Registering a custom projection
import { ProjectionRegistry, type Projection } from "@syncropel/renderer";
const VibeGraph: Projection = {
name: "cmv-vibe-graph",
label: "Vibe graph",
Component: ({ records, options, className }) => {
/* your D3 / xyflow / canvas surface */
},
};
ProjectionRegistry.register(VibeGraph);A workspace manifest can now reference cmv-vibe-graph in projection_options.default and the renderer will mount it instead of one of the built-in projections.
Listening for workspace actions
The recommended path: pass an onAction callback to <RendererProvider>.
<RendererProvider
onAction={(action, record) => {
if (action === "edit") openEditModal(record);
if (action === "delete") confirmDelete(record);
}}
>
{/* ... */}
</RendererProvider>The legacy window.CustomEvent surface still works for back-compat:
import { WORKSPACE_ACTION_EVENT, type WorkspaceActionEventDetail } from "@syncropel/renderer";
useEffect( => {
function onAction(ev: Event) {
const { action, record } = (ev as CustomEvent<WorkspaceActionEventDetail>).detail;
if (action === "edit") openEditModal(record);
if (action === "delete") confirmDelete(record);
}
window.addEventListener(WORKSPACE_ACTION_EVENT, onAction);
return => window.removeEventListener(WORKSPACE_ACTION_EVENT, onAction);
}, []);Both surfaces fire on every action — pick the one that fits your host. Inside a custom projection you've registered, use useWorkspaceActions to emit:
import { useWorkspaceActions } from "@syncropel/renderer";
function MyProjection({ records }) {
const { emit } = useWorkspaceActions;
return <button onClick={ => emit("custom-action", records[0])}>Do thing</button>;
}Sub-path exports
// Main entry — everything
import { WorkspaceRenderer, RendererProvider, ProjectionRegistry } from "@syncropel/renderer";
// Just the projections
import { TableProjection, FeedProjection } from "@syncropel/renderer/projections";
// Just the manifest types and utilities (no React)
import { parseWorkspaceDid, validateManifest, fetchManifest } from "@syncropel/renderer/manifest";
// Just the React hooks
import { useWorkspaceManifest } from "@syncropel/renderer/hooks";
// Just the context layer (provider + hooks)
import { RendererProvider, useRendererContext, useWorkspaceActions } from "@syncropel/renderer/context";Tree-shakers (esbuild, Rollup, Webpack 5) drop unused exports automatically.
Theming
The renderer is theme-agnostic. It uses CSS custom properties (var(--color-text-primary), var(--color-border-subtle), etc.) that the host defines. Two sane choices:
Option A — bring your own tokens. Define the variables on :root (or any ancestor of the renderer):
:root {
--color-text-primary: #111;
--color-text-secondary: #444;
--color-text-muted: #888;
--color-border-subtle: #ddd;
--color-bg-secondary: #fafafa;
--color-bg-hover: #f0f0f0;
--color-act-intend: #f97316;
--color-act-do: #38bdf8;
--color-act-know: #34d399;
--color-act-learn: #c084fc;
}Option B — use the canonical Syncropel design system. Install @syncropel/react and import its tokens:
import "@syncropel/react/tokens.css"; // or styles.css for tokens + atom CSSEither way, the renderer uses CSS variables and never bakes specific colors into its source. Customize at the variable level; no rebuild required.
SSR + prefetch (Next.js, Astro, Remix)
The renderer's useWorkspaceManifest hook is built on @tanstack/react-query, so server-side prefetching uses the standard Tanstack Query SSR pattern.
Next.js (App Router) — prefetch on the server, hydrate on the client:
// app/[...workspace]/page.tsx
import { fetchManifest } from "@syncropel/renderer/manifest";
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
export default async function Page({ params }: { params: { workspace: string[] } }) {
const manifestRef = params.workspace.join("/");
const queryClient = new QueryClient;
await queryClient.prefetchQuery({
queryKey: ["workspace-manifest", manifestRef],
queryFn: => fetchManifest({ ref: manifestRef, apiBaseGetter: => process.env.SPL_API_BASE! }),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<WorkspaceRenderer manifestRef={manifestRef} />
</HydrationBoundary>
);
}The renderer's hook reuses the prefetched query — no double fetch, no waterfalls. The cache TTL (5 min default) governs revalidation.
Edge case to know about: fetchManifest is async and uses the host's global fetch. In Edge runtimes (Cloudflare Workers, Vercel Edge), process.env.SPL_API_BASE may be undefined at build time but defined at request time — pass apiBaseGetter: => env.SPL_API_BASE from the request context, not from a module-level read.
For complete Next.js + Vite examples, see the examples/ directory.
Status: 0.2.1
Patch release on top of 0.2.0 — adopter-readiness polish surfaced by parallel-agent verification (sibling-conformance audit). Zero breaking changes.
CoreWorkspaceManifestV1type alias — spec-canonical name forWorkspaceManifest. Both resolve to the same@syncropel/configCoreWorkspaceV1type. Adopters reading code prefer this name; existingWorkspaceManifestretained for back-compat."sideEffects": falsedeclared in package.json — enables tree-shaking in adopter bundlers.examples/directory with two minimal host integrations (Next.js + Vite).- SSR + prefetch section added to this README (above).
Carried over from 0.2.0:
<RendererProvider>typed React context — the canonical mechanism for app-wide configuration. Inject your authenticatedfetch, server base URL, default catalog URL, error reporter, and action callback in one place. Optional — defaults work for standard browser hosts.useWorkspaceActionshook — callemit(action, record)from inside a custom projection. Fires both the provider'sonActioncallback (when set) and the legacywindow.CustomEvent(always, for back-compat)./contextsub-path —import { RendererProvider, useRendererContext } from "@syncropel/renderer/context".
Carried over from 0.1.0:
- The manifest schema defines five component kinds; this release renders one fully.
viewis fully supported.page,thread_view,workspace, andexternalrender a placeholder pending the runtime extension layer (Phase 2 in the unified Renderer+Workspace arc). - Module-level host setters (
_setHostFetch,_setApiBaseGetter,_setErrorReporter) still work as a fallback configuration mechanism for hosts that haven't adopted the provider.
Migrating from 0.2.0 requires no code changes. See CHANGELOG.md for the full roadmap.
Why this package
Workspaces should be portable in practice — mountable in your own app, not only on syncropel.com. That requires the renderer itself to be a public package. This is it.
Your workspace runs against your records, in your instance, in your app. End-to-end ownership.
Related packages
| Package | Role |
|---|---|
| @syncropel/sdk | Record client (emit, query, search, infer) |
| @syncropel/projections | SRP v0.2 schema for declarative UI documents |
| @syncropel/react | Design tokens + 21 React atoms/molecules |
| @syncropel/extensions | Iframe extension SDK (SAP v0.2) |
| @syncropel/config | Body-kind JSON Schemas |
| @syncropel/workspace-templates | Starter workspace manifests |
License
Apache-2.0. Copyright Syncropic, Inc. See LICENSE.
