acme/feedback-widget, a widget a customer embeds on their own site, where visitors ask questions and get AI-generated answers.
It’s the most security-sensitive kind of plugin you can build, so read Public pages and endpoints first.
Architecture
1. Publish the loader
composer.json
/acme-feedback.js, and the app bundle under /e/acme/feedback-widget/. The customer embeds one tag:
postMessage:
widget/src/loader.js
2. Serve the embed page
src/RequestHandlers/EmbedView.php
frame-ancestors is the control that actually stops the widget being framed elsewhere. Default to 'none' when no domains are configured, rather than allowing everything.
The template is a standalone page, not the app layout: <!DOCTYPE html>, your bundle, and a JSON config block.
3. Authenticate visitors
A visitor isn’t an Aikeedo user. Issue a short-lived token, signed with the widget’s own secret:src/RequestHandlers/Api/SessionRequestHandler.php
src/Middlewares/VisitorMiddleware.php
4. Answer with AI, billed to the owner
The visitor pays nothing; the workspace that owns the widget does.5. Verify ownership everywhere
Every route below/feedback/{wid}/ takes an ID from an untrusted caller. Check the whole chain:
6. Development
Point an environment variable at your bundler while you work:embed.twig
Launch checklist
frame-ancestors is set, and defaults to 'none' when nothing is configured.Tokens are signed per widget, short-lived, and never minted from caller-supplied IDs.
Every nested ID is checked against both the visitor and the widget.
Credits are billed to the widget’s workspace, and checked before generation starts.
Requests are rate-limited per visitor and per widget, and payload size is capped.
Responses contain nothing internal: no workspace details, no configuration, no other visitors.
Disabling the feature or deactivating the widget returns
404 for every route.The loader is small, framework-free, and degrades quietly if the iframe fails to load.