ShareMyPage

Data and backends

Where a page can store data. The built-in blocks need no backend of your own; window.smp covers custom shapes; and a page can call your own API or a hosted database over HTTP.

A shared page is real HTML in a real browser, so a common question is where it keeps data. "Do I need a backend? Can I connect a database?"

Most of the time the answer is no, you do not need either. ShareMyPage stores responses for you. There are three levels, and you should reach for the first one that fits.

1. The built-in blocks

Drop a block into your page's HTML and storage is handled. No backend, no API key, no JavaScript of your own.

BlockWhat it storesRead it back
<smp-poll>one vote per person, tallied live on the pagelive counts on the page, plus CSV export
<smp-form>free-text submissions, private to youCSV export, one column per field
<smp-reactions>one row per person per emojithe counts on the page
<smp-checklist>shared team state, synced across everyone, and with allow-add the items readers type toothe page itself

This covers RSVPs, feedback, surveys, votes, guestbooks, launch checklists, shared todo and shopping lists, and counters. Responses live in the page's own storage, are tied to the block's id, and are read from the Responses panel in page settings.

Note the split in that last column, because it decides which block you want. A form's submissions are private to you: they never appear on the page, so a visitor cannot see what anyone else wrote. A checklist is the opposite, it is shared, and allow-add extends that to the items themselves. If you want readers to add something the next reader can see, that is the checklist, not the form.

2. window.smp for a custom shape

If your data does not fit a poll, a form, or a checklist, every page has a raw append-and-read stream available to any inline script:

// append a row to a named stream (one updatable row per visitor)
await window.smp.append("signups", { name: "Sam", plan: "team" });

// or spell out where it lands: a named bucket is what the built-in blocks
// count, so this is how your own script writes something they can read back
await window.smp.append("checklist-items", { text: "Ship it" }, {
  blockId: "todo:items",   // defaults to the stream name
  bucket: "Ship it",       // defaults to null, which only adds to the total
  dedupeKey: "i1a2b3c",    // one updatable row per key, per visitor
});

// the identity a checklist files an item under, so your own board UI can
// address the same item the block does
const id = window.smp.itemValue("Ship it"); // "i1a2b3c"

// read the running aggregate. `bucketOrder` holds the same keys as `buckets`,
// oldest first, for when the order matters.
const agg = await window.smp.read("signups");

Both calls resolve to the same aggregate, or to null when the stream has nothing yet or the write was rejected (closed, paused, over quota, rate-limited, or a network failure). Always null-check before reading fields:

{
  total: 12,                                 // rows counted
  buckets: { sushi: 7, pizza: 4 },           // per-key counts; {} for a bucketless stream
  bucketOrder: ["sushi", "pizza"],           // the same keys, oldest first
  mine: ["sushi"],                           // the keys YOU hold, when you identify yourself
  closed: false                              // true once the owner closes the block
}

buckets is filled only when appends carry a bucket. A plain window.smp.append with no bucket has buckets: {}, so use total.

You get the counts, not the individual rows. Individual responses stay private to you and come out of the CSV export. That makes this a good fit for counters, tallies, and intake with a bespoke UI, and a poor fit for anything where the page itself must display what other people wrote. For that last case, reach for <smp-checklist allow-add> before you reach for a backend: it is the one built-in block whose contents readers write and every other reader sees.

3. Your own API or a hosted database

When you need real reads and writes that the page renders back, call your own endpoint over HTTP. A page is allowed to make outbound requests, so a plain fetch() to your API, to Supabase, to a Neon Data API, to Firebase, Turso, or Airtable works.

Three things decide whether it actually works, and all three trip people up.

You cannot connect to a database directly

A browser speaks HTTP and WebSocket, nothing else. There is no way to open a Postgres or MySQL connection from a page, on ShareMyPage or anywhere else. You always need an HTTP API in front of the database, either one you write or the one your database host provides.

Anything in the page is public

Your page's HTML is served to every visitor, so any key inside it can be read by every visitor. Treat it as published the moment you save.

That means a publishable or anon key only, paired with row-level security in the database so the key alone cannot read or change anything it should not. Never a service key, an admin token, or a database URL with a password in it.

Do not paste secrets into a page

A private page is private to readers you have not invited, not to the browser. Anyone who can open the page can read its source. If a credential must stay secret, keep it on your own server and have the page call a small endpoint that holds it.

CORS sees Origin: null

Page content renders inside a sandboxed frame with no origin of its own, which is what keeps a page's scripts away from your account and everyone else's pages. A side effect is that outbound requests carry Origin: null.

So an API that answers Access-Control-Allow-Origin: * works, and one whose allowlist expects a specific domain will reject the request. If you control the endpoint, allow the null origin explicitly. If a hosted service only offers a domain allowlist, put your own small proxy in between.

There is no localStorage

For the same reason, the sandboxed page has no localStorage, no sessionStorage, and no cookies of its own. Keep per-visitor state in memory for the length of the visit, or push it to window.smp or your own API if it has to survive a reload.

Which one should I use

  • A poll, a form, a checklist, or a counter. Use the built-in block. It is the only option with no keys to leak and no service to run.
  • A list readers add to and everyone then sees, like a todo list, a shopping list, or an agenda. Use <smp-checklist allow-add>. This is the one case that used to need your own backend, because a form's answers are private to you and window.smp reads back totals rather than rows.
  • The same kind of data, your own look. Style the block, or drive it from your own script with window.smp.
  • Reading back individual records, joining data, or showing a visitor the full text of what other visitors submitted. Use your own API, with a publishable key and row-level security, or a proxy endpoint that holds the secret. window.smp.read returns per-answer totals, not the rows behind them, so anything that has to render one visitor's words to another needs either an appendable checklist or a backend of your own.

The built-in storage has fair-use caps: a per-page response cap plus per-visitor and per-IP rate limits. It is sized for pages that collect responses, not for high-frequency or high-volume data collection. Bring your own backend if you are past that.

On this page