How Pled builds my Bubble plugins
Bubble plugins normally live in a browser editor with copy-paste code and no version history. Pled is the Elixir CLI that pulls my plugin out into real files, so I can build the Tiptap rich-text editor and my other Bubble plugins like normal software.
How a Bubble plugin usually gets built
Bubble plugins are built in a browser editor. You get elements and actions, and each one has small code boxes: this one runs on init, this one on update, this one on click. There is no file tree, no diff, no history, and no way to run the code except inside Bubble itself.
So the workflow is: scroll to the right box, paste JavaScript, save, then click around a test app until something looks broken. Undo is your browser’s undo. Version control is copy-pasting the box into a Notion page and hoping. I did that for years, and the tooling is why my plugins stayed small.
Pled is what I built to stop doing that: an open-source Elixir CLI (ricotrevisan/pled) that treats a Bubble plugin as a source tree instead of a web form.
What it actually does
The mental model in the README is three steps: pull, edit, push.
pled pullfetches the plugin through the Bubble API and decodes it into a localsrc/tree with readable file names. Bubble embeds JavaScript inside JSON, so decoding means splitting those blobs back into separate files.- You edit
src/in whatever you like. That is the whole point. pled pushencodes the tree back into Bubble’s format and uploads it.
The layout it produces:
.plugin_id # plugin ID, safe to commit
src/plugin.json # plugin metadata
src/shared.html # shared HTML header
src/elements/<name>-<KEY>/ # initialize.js, update.js, reset.js, preview.js, actions/*.js
src/actions/<name>-<KEY>/ # client.js, server.js
dist/plugin.json # generated by `pled encode`, never edited by hand
One detail saves me constantly: I never write Bubble’s function wrapper. Bubble calls update.js with instance, properties and context; Pled adds the wrapper when it encodes. I write the body.
There are also pled encode (build dist/plugin.json without uploading — CI uses it), pled upload (push a file to Bubble’s CDN), and pled watch, which keeps src/ and Bubble in sync while I work: saves get debounced and pushed, remote edits get pulled, default poll 15 seconds.
The part that matters most: refusing to lose work
Since v0.1.0, every sync compares three things: the baseline snapshot of the last sync, my local src/, and the current plugin in Bubble. It classifies the workspace as in sync, local ahead, remote ahead, diverged, or missing a baseline. pled status and pled check-remote print the state and the next command.
That sounds like plumbing until you have worked without it: earlier versions force-pushed on every save, so one stray edit in the Bubble editor could be silently overwritten. Now pled push refuses when the remote moved, pled pull refuses to stomp unpushed local changes, and pled watch pauses with a conflict message and resumes on its own once I resolve it. --force still exists; it just has to be typed on purpose.
Pled also fingerprints JavaScript through an AST parse (a small Node script, with a fallback to text comparison if Node is missing), so whitespace-only changes don’t count as changes.
Libraries, tests, and CI
Bubble’s plugin editor has nowhere good to put a real npm tree. My Tiptap plugin bundles its dependencies in lib/index.js with esbuild into a single dist.js, uploads it with pled upload, and points headers.html at the returned CDN URL. pled init scaffolds that lib/ setup, plus .plugin_id, a .gitignore, and an AGENTS.md that documents the CLI workflow for AI agents.
Because the source is real files, the tests are real tests: Node lifecycle tests, Playwright specs against a real browser, a validator that parses every src/ JSON file and checks action declarations against their files, and a GitHub Actions workflow that runs it all on every pull request and push to main. None of that is possible when the code only lives inside Bubble.
What this looks like on the flagship plugin
My main Bubble plugin is a Tiptap rich-text editor — 55+ workflow actions, tables, images, and real-time collaboration, with around 2.5k installs. Its repo shows the structure Pled produces: one main element with dozens of action files under src/elements/tiptap-AAC/actions/, plus two server-side actions (JWT auth token generation, and a Hocuspocus webhook-to-HTML converter). Release notes track bugs like undo/redo breaking when the History extension is disabled — the kind a test file catches and a copy-pasted code box does not.
The same setup runs my smaller plugins. number-formatter-plugin-for-bubble and modern-dropdown both use the same .plugin_id plus src/ layout. The number formatter’s AGENTS.md is literally the Pled template, and the dropdown notes describe the shared workflow as “Pled, code piles, lib releases”.
What Pled does not do
It does not remove Bubble. Bubble stays the source of truth; I just stopped living in the editor.
It needs BUBBLE_COOKIE, your authenticated session cookie. That is the only secret, and it expires whenever Bubble feels like it — then you get 401s until you paste a fresh one. Treat it like a password.
It is one plugin per directory, so reuse means the same toolchain and conventions across repos, not one repo holding every plugin.
And there is no true local runtime. I can test the JavaScript outside Bubble, but the element only exists inside a Bubble page, so final checks always happen in a real test app.
That is it. Not a platform, not a framework. A CLI that makes plugin development feel like writing software again.
Enjoyed this?
I write short things mostly: TILs, expansions of tweets that needed more than 300 characters, and a log of what my AI agents ship and break.