Builder & Plugins
How the section-builder invitation works under the hood, how to author your own plugin components without writing app code, and how developers add new native components.
How the canvas works
A builder invitation is one JSON document stored in a single jsonb column (events.builder). That document has a global theme and an ordered list of typed sections - the sections are the page.
The same component, BuilderRenderer, draws that document in two modes: with editing=true it becomes the inline editor (click-to-edit text, drag-to-reorder, per-section toolbar, layers panel, autosave); with editing=false it renders the read-only invitation guests see, with the scroll reveal, falling petals, and end confetti. Because both come from the same JSON, what you build is exactly what ships.
One JSON document → one renderer → both the editor and the live page
Crucially, most design work needs no database changes. New fonts, colors, sizes, section types, and plugin output all live inside that flexible jsonb document - adding them never alters the schema or risks existing data.
Anatomy of the document
The shape, trimmed to essentials:
BuilderDoc = {
version: 1,
theme: {
bg, text, accent, headingColor, // colors
font, // curated display font
nameScale, nameLineHeight, // couple-name type
headingScale, headingLineHeight, // section-heading type
},
sections: Section[] // ordered - this is the page
}
Section = {
id: string, // stable unique id
type: "hero" | "story" | "timeline" | "gallery"
| "image" | "embed" | "rsvp" | ...,
props: { ... }, // data for THIS section's type
bg?: { image?, color? } // optional per-section backdrop
}type narrows props (a discriminated union), so each section is strongly typed. The renderer dispatches on type to the matching React component.
Create a plugin (no code deploy)
Plugins are WordPress-style custom components you author once and reuse across invitations - a countdown widget, a map, a video, a guestbook embed, any HTML. You create them in Settings → Plugins (open the account menu from the navbar avatar). No developer or deploy needed.
Add a plugin
In Settings → Plugins, give it a name, paste your HTML / embed code, set a height, and (optionally) declare editable fields so it can be customised per invitation. It's saved instantly.
Drop it into an invitation
Your plugins appear in the builder's component palette under Plugins. Click one and it's added as a section, pre-filled with your code.
It ships with the invite
The plugin's HTML is saved into that event's JSON, so every guest sees it - rendered in a sandboxed iframethat's isolated from the app (scripts run, but it can't touch cookies, storage, or the page around it).
Example plugin HTML:
<div style="font-family:sans-serif;text-align:center;padding:24px">
<h3 style="margin:0 0 8px">Live from our hashtag</h3>
<a href="https://instagram.com/explore/tags/RaelAndFaisha"
style="color:#c93766">#RaelAndFaisha</a>
</div>Where plugins live: your plugin library is saved to your account, so it follows you across browsers and devices. Keep the HTML reasonably small - for anything large, point a plugin at a hosted iframe embed rather than pasting it wholesale.
Build a plugin
A plugin is an HTML template plus a list of editable fields. You write the markup once and mark the parts the admin should be able to change with {{key}} placeholders. In the builder, each field becomes a control (text box, color picker, number, image upload), and the chosen values are substituted into your template before it renders in a sandboxed iframe.
Field types
text- single-line text box(headings, names, labels)textarea- multi-line text box(paragraphs, messages)color- color picker(accent / background colors)size- number input(font sizes, widths, durations)image- image uploader(becomes a hosted image URL)
Write the template with placeholders
Plain HTML. Put {{key}} wherever a field's value should appear - in text, in a style, or in an src. An imagefield resolves to the uploaded photo's URL.
<div style="text-align:center;padding:24px;font-family:sans-serif">
<h2 id="title"
style="margin:0;color:{{accent}};font-size:{{size}}px">
{{title}}
</h2>
<img src="{{photo}}" alt=""
style="max-width:180px;border-radius:14px;margin-top:14px" />
</div>Declare the fields
In Settings → Plugins → New plugin, add one field row per placeholder: a key (matches {{key}}), a label, and a type. For the template above:
title → Text (label: "Heading")
accent → Color (label: "Accent color")
size → Size (label: "Heading size")
photo → Image (label: "Photo")Use it in any invitation
Your plugin shows up in the builder palette under Plugins. Adding it drops in a section with a control for every field; editing a value live-updates the preview. The whole thing (template + fields + values) is snapshotted into the event JSON, so it ships to guests and stays editable on any device.
Building a native section type instead (in-repo, with React and inline editing)? That path is documented in the repo at docs/builder-components.md.
Scripts, animation & safety
Yes - plugins can run JavaScript: animations, interactions, even third-party libraries loaded from a CDN. Your code runs inside a sandboxed iframe, which is the key to why it's safe.
No naming conflicts - ever. The iframe is its own document with its own windowand global scope. Functions, variables and library globals you define cannot collide with the host app or with other plugins, and an infinite loop or crash in your script is contained to the iframe - it can't take down the invitation. So there is no list of "reserved" or forbidden function names to avoid; name things whatever you like.
<button id="btn">Tap me</button>
<script>
// Runs in the plugin's OWN isolated window - any names are fine.
const btn = document.getElementById("btn");
let count = 0;
btn.addEventListener("click", () => {
btn.textContent = "Tapped " + (++count);
});
</script>What the sandbox blocks
The iframe runs with allow-scripts allow-forms allow-popups allow-presentation but not allow-same-origin. That gives it an opaque origin, which is what prevents it from reaching into the app. Concretely, your plugin cannot:
- read or write the app's cookies, localStorage, or session
- touch the surrounding page's DOM (window.parent / top are cross-origin and blocked)
- navigate or redirect the whole browser tab
- call same-origin-only browser APIs (it has no real origin)
Everything self-contained works: your own JS/CSS, timers, animations, <canvas>, and libraries pulled in via <script src>. Just keep it light - guests open invitations on phones.
Notes & roadmap
- Values are escaped.Field values are HTML-escaped before they're substituted, so a stray
<in a text field can't break your markup. - Images are optimised. Uploads are downscaled and converted to WebP in the browser, then cached for a year.
- Plugin library syncs to your account, so it follows you across devices - no separate table required. Keep templates reasonably small.
- Roadmap: shareable plugin import/export, range/select field types, and a curated plugin marketplace.