Build Solana flows by dragging nodes — keypairs, PDAs, instructions, transactions — and run them against a live cluster.
PLGRND is a visual playground for Solana. Instead of writing a script to derive an ATA, build an instruction and send a transaction, you wire together nodes on a canvas: each one is a ready, tested block of code with typed inputs and outputs. Values flow along the edges and evaluate live, so you see the actual public key, the actual lamport amount, the actual transaction at every step. Import a program's IDL and its instructions become nodes you can call. Every flow encodes into its own URL, so a working example is a link you can send to someone.
| Area | Technology |
|---|---|
| App | React 19 + TypeScript, Vite |
| Canvas | React Flow (@xyflow/react) |
| Solana | @solana/web3.js, @solana/spl-token, Anchor for IDL decoding |
| Wallets | Solana Wallet Adapter |
| Crypto | tweetnacl, bip39, ed25519-hd-key, bs58 |
| State & data | Zustand, TanStack Query |
| UI | Tailwind CSS v4, Radix UI, lucide-react, sonner |
| Tooling | Yarn 4 workspaces, ESLint, Prettier |
| Deploy | Cloudflare Workers (wrangler) |
flowchart LR
A[Drag nodes onto canvas] --> B[Wire outputs to inputs]
B --> C{Flow graph}
C -->|evaluated live| D[Values: keys, PDAs, amounts]
C -->|IDL import| E[Program instructions]
D --> F[Transaction builder]
E --> F
F -->|wallet signature| G[Solana RPC]
C -->|encoded to URL| H[Shareable link]
- Pick nodes from the menu and drop them on the canvas — each category maps to a piece of the Solana model.
- Connect handles; the graph re-evaluates on every change, so downstream nodes update as you type.
- Feed the result into a transaction builder, connect a wallet, and send it to the cluster of your choice.
- Hit share — the whole graph is compressed into the URL hash. Hand someone the link, or use Share → Embed to generate a themed interactive widget for any page (see Embedding).
| Category | Nodes |
|---|---|
| Input | Text, number, display, comment |
| Math / String / Logic | Arithmetic, combine, split, substring, search, replace, length, compare, equality, if, booleans |
| Crypto | Keypair, mnemonic, private key, hash, sign, verify signature |
| Wallet / Network | Connected wallet, cluster selection, balance |
| Utils | SOL ↔ lamports, token amounts, ATA derivation, rent exemption, public key validation, string encode/decode |
| Programs | IDL import, program instructions, program accounts, PDA derivation |
| Transactions | Transaction builder, transaction, transaction view |
Built-in example flows cover hashing, signature verification, balance checking, a SOL transfer and a program call — each one loads straight onto the canvas.
Any PLGRND flow can be embedded on another site as an interactive widget — an <iframe> pointed at /embed, no JavaScript required on the host page:
<div style="position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:12px">
<iframe
src="https://plgrnd.io/embed?theme=light&bg=e0deda&accent=ed4937#flow=…"
title="PLGRND interactive flow"
loading="lazy"
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
allow="clipboard-write"
style="position:absolute;inset:0;width:100%;height:100%;border:0"
></iframe>
</div>The easiest way to get a snippet is the built-in builder: open a flow, hit Share → Embed, tune the options against the live preview and copy the result.
Options travel in the query string; the flow itself stays in the #flow= hash. Options are also accepted inside the hash after ?, which keeps old links working. Invalid values never break rendering — they fall back to their defaults.
| Parameter | Values | Default | Effect |
|---|---|---|---|
mode |
readonly · interactive · editor |
interactive |
Interaction level (see below) |
theme |
dark · light · auto |
dark |
Palette preset |
bg |
hex without # (3, 4, 6 or 8 digits) |
from theme | Canvas background |
surface |
hex | from theme | Node bodies, panels, popovers |
fg |
hex | from theme | Text |
border |
hex | from theme | Borders and input outlines |
accent |
hex | from theme | Highlights and focus ring |
radius |
integer 0–24 |
8 |
Corner radius in px |
font |
font-family string (letters, digits, spaces, ,, quotes, -; ≤ 100 chars) |
PLGRND default | UI font |
fontMono |
same rules | PLGRND default | Monospace font |
controls |
1/0 |
1 |
Zoom/fit controls |
minimap |
1/0 |
0 |
MiniMap |
toolbar |
1/0 |
1 |
"Open in PLGRND" + "Reset" buttons |
Unknown parameters are ignored. id= is reserved for future server-stored flows and is read from the query string only. The "Powered by PLGRND" badge is always shown.
| Behaviour | readonly |
interactive |
editor |
|---|---|---|---|
| Pan and zoom (Controls / pinch) | ✔ | ✔ | ✔ |
Edit values inside nodes (inputs, Generate, Refresh) |
✖ | ✔ | ✔ |
| Move, connect, add or delete nodes | ✖ | ✖ | ✔ |
| Header with the node menu | ✖ | ✖ | ✔ |
Wallet connection is never available inside the widget: the Wallet node deep-links to the full editor with the current canvas state instead. Send works inside the widget when the transaction is signed by a private key supplied in the flow (for example a throwaway devnet key pasted into a Private key or Keypair node); when signing requires a connected wallet, Send deep-links to the full editor as well. Read-only RPC (balances, account info, transaction views) always works.
theme=dark|light picks a preset and the colour tokens above apply on top. theme=auto follows the visitor's OS setting (prefers-color-scheme), not your site's own theme toggle — if your site switches dark mode with a class or a button, pin theme= to the value you render, or drive the widget at runtime via postMessage.
Each widget posts { type: 'plgrnd:ready' } to its direct parent once the flow has rendered (a nested shell must relay it upward if needed), and accepts exactly one inbound message shape — anything else is ignored:
frame.contentWindow.postMessage({ type: 'plgrnd:theme', theme: 'dark' }, '*')With several widgets on one page, resolve the sender via event.source — never guess iframes by src:
<script>
// Multi-widget-safe host-side theme sync.
const hostTheme = () => (document.documentElement.classList.contains('dark') ? 'dark' : 'light')
const widgets = new Set()
window.addEventListener('message', (e) => {
if (e.data?.type !== 'plgrnd:ready') return
widgets.add(e.source)
e.source.postMessage({ type: 'plgrnd:theme', theme: hostTheme() }, '*')
})
// Call this from your own theme toggle:
const syncWidgets = () => widgets.forEach((w) => w.postMessage({ type: 'plgrnd:theme', theme: hostTheme() }, '*'))
</script>Widget appearance is not trusted state: any script able to reach the widget's contentWindow — including third-party scripts on the host page — can change its theme. The message affects visuals only.
sandboxneedsallow-scripts allow-same-origin, plusallow-popups allow-popups-to-escape-sandboxfor "Open in PLGRND" and the badge link;allow="clipboard-write"enables click-to-copy inside nodes. Without popup permissions the widget falls back to a copyable link in a toast.- The mouse wheel over the widget scrolls the host page (same convention as embedded maps); zooming works through the Controls, pinch, or Ctrl/Cmd+wheel.
- Embeds are stateless: visitor edits live only inside the iframe, reloading it (or pressing Reset) restores the authored flow, and nothing is read from or written to the visitor's PLGRND storage.
Old-style https://plgrnd.io/#flow=…?view=true links keep working — they are rewritten client-side to /embed with default options.
<plgrnd-flow> renders the same widget directly in your page, without an iframe. You serve the file yourself, from your own site or bundle. At runtime the widget loads no Google Fonts, no analytics, no remote CSS and nothing from plgrnd.io. Its only requests go to the Solana RPC endpoints. It writes no cookies and no storage. Shadow DOM keeps your site's CSS out of the widget, and the widget's CSS out of your site.
The widget ships as the npm package @plgrnd/embed and as versioned downloads under https://plgrnd.io/sdk/<version>/. To get a snippet, open Share → Embed → Self-hosted and pick HTML, MDX, Mintlify or Markdoc. The snippet carries the same flow payload and options as the iframe snippet.
| File | Raw | gzip | Use |
|---|---|---|---|
plgrnd-flow-1.0.1.js |
1,255 kB | 395 kB | Classic script, everything in one file |
plgrnd-flow-1.0.1/plgrnd-flow.js |
7 kB | 3 kB | ESM entry: defines the element |
plgrnd-flow-1.0.1/plgrnd-flow-runtime.js |
1,322 kB | 394 kB | ESM runtime, loaded the first time an element connects |
Download the files once and serve them yourself. Never hot-link plgrnd.io/sdk. The version is in the file name, so a page keeps exactly the build it was tested with.
<!-- classic script, once per page -->
<script src="/plgrnd-flow-1.0.1.js"></script>
<!-- or the ESM folder, served as-is -->
<script type="module" src="/plgrnd-flow-1.0.1/plgrnd-flow.js"></script>
<plgrnd-flow flow="N4Ig…" theme="auto" style="display:block;aspect-ratio:16/9;min-height:320px"></plgrnd-flow>With a bundler, use npm install @plgrnd/embed and then import '@plgrnd/embed'. On the server that import resolves to an empty module, so SSR frameworks can import it anywhere. The package also ships TypeScript types for <plgrnd-flow> in React 18 and 19 JSX.
| Attribute | Values | Default |
|---|---|---|
flow |
The payload from the snippet | — |
theme |
dark · light · auto |
dark |
bg, surface, fg, border, accent |
Hex, with or without # |
From the theme |
radius |
0–24 (px) |
8 |
font, font-mono |
A font-family string | Your page's font · the system monospace stack |
controls, minimap, toolbar |
1 · 0 · true · false |
1 · 0 · 1 |
mode |
readonly · interactive |
interactive (editor falls back to interactive) |
rpc-mainnet, rpc-devnet |
http: or https: JSON-RPC URL |
The public cluster endpoints |
link-ref (also ref in plain HTML) |
[A-Za-z0-9._-]{1,64} |
— (added as ?ref= to the plgrnd.io links) |
How attributes behave:
- Invalid values fall back to the defaults, silently.
- Every attribute also exists as a camelCase property, such as
fontMono,rpcDevnetorlinkRef. Properties do not reflect to attributes, and the last write wins. - The element fires
plgrnd:readyafter each successful (re)initialization. The event bubbles, is composed, and carriesdetail.version. - A widget mounts when it comes within one viewport height of the visible area, so an off-screen widget fires
plgrnd:readyonly after the reader scrolls near it.
theme="auto" follows your site's own toggle, live. It checks these markers in order:
body[data-md-color-scheme](MkDocs Material).slateordarkmeans dark, anddefaultorlightmeans light. For any other value, the luminance of the body background decides.html[data-theme](Docusaurus).- A
darkorlightclass on<html>(Mintlify, next-themes, Fumadocs). prefers-color-scheme.
The defaults are https://api.mainnet-beta.solana.com and https://api.devnet.solana.com. Both are rate-limited public endpoints, so production docs should pass their own, for example rpc-mainnet="https://…".
Transactions are confirmed by HTTPS polling, so the widget never opens a WebSocket. No private RPC key is compiled into the files. A key you put in an attribute is visible in your page source, so restrict it by referrer.
These rules come from measurements on packages/embed/fixtures/strict-csp.html (CSP: script-src 'self', https: RPC only, no wss:; 0 violations).
script-src. Allow the origin that serves the file. No'unsafe-eval', noblob:and nodata:scripts are needed, and the widget starts no workers.'strict-dynamic': works, with 0 violations.- Trusted Types: with a static
<script src>, the widget raises no Trusted Types violations. A host script that injects the<script>itself needs its own Trusted Types policy.
connect-src. Allow your RPC endpoints, overhttps:only.wss:is not needed.style-src. No'unsafe-inline'is needed, because styles use constructable stylesheets and the CSSOM. Browsers without constructable stylesheets (Safari before 16.4) fall back to a<style>element inside the shadow root, which does need'unsafe-inline'.
The snippets reserve space with an inline style. To reserve space for every widget before the script runs, add:
plgrnd-flow:not(:defined) {
display: block;
aspect-ratio: 16 / 9;
min-height: 320px;
}Mintlify.
-
Put
plgrnd-flow-1.0.1.jsat the root of the docs repo. Mintlify injects every.jsfile in the repo into every page, so keep exactly one version file. Never add the ESM folder: its files would be injected as classic scripts too. -
Add
snippets/plgrnd-flow.jsx:export const PlgrndFlow = ({ style, ...props }) => ( <plgrnd-flow style={{ display: 'block', aspectRatio: '16/9', minHeight: 320, ...style }} {...props} /> )
-
Use it on a page. The Mintlify format of the Share dialog has this shape:
import { PlgrndFlow } from '/snippets/plgrnd-flow.jsx' <PlgrndFlow flow="N4Ig…" theme="auto" />
Docusaurus. Copy the ESM folder to static/plgrnd-flow-1.0.1/, then load it from docusaurus.config.js (prefix baseUrl if it is not /):
scripts: [{ src: '/plgrnd-flow-1.0.1/plgrnd-flow.js', type: 'module' }],Alternatively, run npm install @plgrnd/embed and add a client module (clientModules: [require.resolve('./src/plgrnd.js')]) whose only line is import '@plgrnd/embed'. In MDX, use the MDX format (<plgrnd-flow … style={{ … }} />).
MkDocs Material. Copy the ESM folder to docs/javascripts/, then load it from mkdocs.yml:
extra_javascript:
- path: javascripts/plgrnd-flow-1.0.1/plgrnd-flow.js
type: modulePaste the HTML format into the Markdown. theme="auto" follows the palette toggle, and instant navigation mounts and unmounts widgets with the page.
Next.js / Fumadocs. Register the element once, in a client component rendered by the root layout:
'use client'
import '@plgrnd/embed'
export default function PlgrndFlowRegister() {
return null
}Then use the MDX format in your pages. On the server the import is an empty module, and the element renders nothing into light DOM, so hydration never mismatches.
Markdoc. Add a tag and a component. The Markdoc format produces {% plgrnd-flow flow="…" aspect="16/9" /%}. Add rpcMainnet, rpcDevnet or linkRef to a tag yourself when you need them.
// markdoc/tags.js, inside the tags object
'plgrnd-flow': {
selfClosing: true,
render: PlgrndFlow,
attributes: Object.fromEntries(
['flow', 'theme', 'bg', 'surface', 'fg', 'border', 'accent', 'radius', 'font', 'fontMono',
'controls', 'minimap', 'toolbar', 'mode', 'rpcMainnet', 'rpcDevnet', 'linkRef',
'aspect', 'height'].map((name) => [name, { type: String }])
),
},// components/PlgrndFlow.jsx
import '@plgrnd/embed'
export function PlgrndFlow({ aspect = '16/9', height, fontMono, rpcMainnet, rpcDevnet, linkRef, ...attributes }) {
const style = height
? { display: 'block', height: `${height}px`, minHeight: 0 }
: { display: 'block', aspectRatio: aspect, minHeight: 320 }
return (
<plgrnd-flow
{...attributes}
font-mono={fontMono}
rpc-mainnet={rpcMainnet}
rpc-devnet={rpcDevnet}
link-ref={linkRef}
style={style}
/>
)
}- Download the new version folder, or run
npm install @plgrnd/embed@<version>. - Check it against
SHA256SUMS.txt. - Replace the old file. On Mintlify, delete the old version file.
Existing snippets keep working. A flow saved by a newer PLGRND than your file supports shows "This flow needs a newer PLGRND widget". Update the file to fix it.
- Wallets. No wallet connection.
Sendworks only with a key signer wired into the flow, as in the iframe. - Modes. No editor mode.
- Dropdowns and the info dialog. Dropdowns use the browser's native select popup. The node-info dialog is non-modal and has no focus trap. Escape, the close button and the backdrop close it and return focus. A click outside the widget also closes it, without returning focus.
- Fonts. No web fonts are loaded. The sans font follows your page and the monospace font uses the system stack.
fontandfont-monooverride them. - JSX attributes. Pass booleans and numbers as strings, such as
controls="0": until the element is defined, React 19 drops a JSX{false}and writes{true}as an empty attribute. - Errors. Errors raised through React or React Flow callbacks stay inside the widget and are logged with a
[plgrnd]prefix. Exceptions from other libraries' timers or promises can still reachwindow.onerror.
Each version folder also contains:
SHA256SUMS.txt: runsha256sum -c SHA256SUMS.txtto check your downloads.SRI.txt: thesha384-…values for<script integrity>.THIRD_PARTY_LICENSES.txt: the licences of the bundled packages.
Anyone can add nodes — upstream into this repository (they ship on plgrnd.io) or in your own fork and deployment. The full guide, from the node contract to the PR checklist, is in CONTRIBUTING.md. Node PRs must also extend the Node categories table above.
Prerequisites: Node.js v22 or higher, Yarn 4+.
yarn install # install workspace dependencies
yarn web:dev # run the web app in development mode
yarn web:build # production buildOptionally set VITE_PUBLIC_SOLANA_RPC to point the app at your own RPC endpoint, and VITE_HELIUS_API_KEY to route the nodes' RPC calls through Helius (see apps/web/.env.example).