Widgets
Drop-in feedback, waitlist, changelog, inbox and chat widgets — a script tag on your page, endpoints already behind it.
Five small features that every app wants and nobody wants to build: a feedback box, a waitlist, a changelog, a per-user notification inbox, and a chat box. Each is a hosted script plus a public endpoint on the platform — the console's Widgets page hands you a ready-made snippet per widget.
Adding one
<script src="https://camplax.dev/cdn/feedback.js"
data-project="<slug>" data-api="<api origin>"
data-key="wx_…" data-turnstile-sitekey="…" data-screenshot></script>
Paste the snippet where the widget should live. Some widgets also need a mount point — <div id="camplax-waitlist">, <div id="camplax-changelog">, <div id="camplax-inbox"> — and the snippet says so. The data-key is your project's publication key: deliberately public, it only binds the embed to your project. Anonymous writes are protected the real way — the Origin must be a hostname that is live for your project, every action is rate-limited per IP, and each mutation needs an invisible Turnstile token the script obtains itself. That works on your custom domains too: on a page outside camplax.app the script asks GET /v1/w/<slug>/challenge for your project's own bot-check key, which Camplax issues and keeps in step with your domains (up to nine custom domains per project).
Server-side calls skip the publication key and send your app's own CAMPLAX_PROJECT_TOKEN as Authorization: Bearer … — Camplax injects it into every environment, so there is no key to create. (A project API key with widgets:read or widgets:write does the same from outside Camplax.)
The five
Feedback. A bug / idea / praise box with an optional 1–5 rating, email and screenshot. Entries land on the Widgets page and emit a $feedback event into analytics.
Waitlist. Email capture with a queue position. Optional double opt-in (the widget mails a confirm link that lands on a hosted confirm page), disposable-domain blocking, referral ids, and a showPosition switch. A confirmed join emits $waitlist_join.
Changelog. You write entries in Markdown on the Widgets page — publish and unpublish per entry — and the widget renders sanitized HTML from GET /v1/w/<slug>/changelog. Fifty entries per fetch.
Inbox. Per-user notifications. The sensitive part is done properly: your server mints a 15-minute per-user token with POST /v1/w/<slug>/inbox/access-token and { "user": "<id>" }, authenticated with its CAMPLAX_PROJECT_TOKEN (which never reaches the browser), and the script reads only that user's items with GET /v1/w/<slug>/inbox. Create notifications server-side with POST /v1/w/<slug>/inbox (user, title, optional body and href). Each preview has its own inbox: a preview's token only reads and writes the preview's notifications, never production's.
const res = await fetch(`${process.env.CAMPLAX_API_URL}/v1/w/${process.env.CAMPLAX_PROJECT_SLUG}/inbox/access-token`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.CAMPLAX_PROJECT_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ user: session.user.id }),
});
const { token } = await res.json(); // render as data-user-token on the inbox script
Chat. A visitor message box that stores threads — what a visitor sends lands on the Widgets page for you to read. It records the conversation; it sends no reply on its own — it is not yet a support agent.
There is also a dropzone script — file upload that uses the same signed upload-URL machinery as storage, bound to the size and type you allow, with active formats (SVG, HTML, JS) refused. Each upload is saved as a new file — its name gets a random prefix — so a visitor can never replace a file that is already there.
Settings and limits
Per project, in the console: chat options (collect email), waitlist options (double opt-in, show position, referrals, disposable block) and dropzone limits (max bytes up to 95 MB; bigger files go through a resumable upload, max files, accept list).
Public endpoints are rate-limited per action — feedback and waitlist joins at 5/hour per IP, chat at 8/minute, inbox reads at 60/minute. The full surface:
| Method | Path | Caller |
|---|---|---|
POST | /v1/w/<slug>/feedback | Publication key or production's app token |
POST | /v1/w/<slug>/waitlist | Same — plus the hosted /waitlist/confirm link |
GET | /v1/w/<slug>/changelog | Publication key or the app token |
POST | /v1/w/<slug>/chat | Publication key or production's app token |
GET/POST | /v1/w/<slug>/inbox | Per-user token, or the app token (each environment its own inbox) |
POST | /v1/w/<slug>/inbox/access-token | The app token only |
POST | /v1/w/<slug>/dropzone/upload-url | Publication key or production's app token |
GET | /v1/w/<slug>/challenge | The page itself — returns the bot-check key for its address |
Feedback, waitlist, chat and dropzone keep production's data only, so a preview's token gets 403 preview_not_supported there; the scripts on a preview page still work with the publication key.
Under the hood: each script is a single-file bundle served from camplax.dev/cdn, and every write lands in platform tables owned by your project.