TL;DR
The one-paragraph version before you dive in.
This prompt kit builds a Canny-style feedback tool: an embeddable widget, an OpenAI edge function that scores sentiment and extracts a topic label, an admin inbox sortable by sentiment, and a public roadmap. The entire build fits Lovable Free. The single biggest risk is the OpenAI API key — it must live in Cloud → Secrets, never in a VITE_ variable or any /src/ file, or it gets scraped from your JS bundle and billed to your card.
Setup checklist
Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.
Cloud tab settings
Database
Stores feedback, boards, votes, status_changes, and profiles. RLS enforces that anonymous users can INSERT feedback but cannot SELECT any rows — admins read all, submitters can read their own.
- 1Click the + icon next to the preview panel, then click 'Cloud tab' → 'Database'.
- 2Lovable Cloud provisions Supabase Postgres automatically.
- 3The starter prompt generates the migration. If it does not auto-run, paste the migration SQL into Cloud → Database → SQL Editor.
- 4After migration, verify anon INSERT-only RLS: in SQL Editor run `SELECT policyname, cmd, roles FROM pg_policies WHERE tablename = 'feedback';` — you should see INSERT for anon, SELECT only for authenticated.
Auth
Email/password sign-in for the admin. Feedback submitters are anonymous by default. Upvoting on the public roadmap requires a signed-in account to prevent duplicate votes.
- 1In Cloud tab → 'Users & Auth', confirm Email provider is enabled.
- 2After the starter runs, sign up with your email and set your role to admin: `UPDATE profiles SET role = 'admin' WHERE id = (SELECT id FROM auth.users WHERE email = 'your@email.com');`
Edge Functions
The submit-feedback edge function inserts the feedback row, then calls OpenAI for sentiment scoring and topic extraction. All OpenAI API traffic goes through this Deno function — never the browser.
- 1After the starter prompt runs, open Cloud tab → 'Edge Functions' to confirm submit-feedback is deployed.
- 2If it is missing, paste the OpenAI sentiment edge function follow-up prompt.
- 3The function reads OPENAI_API_KEY via Deno.env.get() — this key must be added to Cloud → Secrets before deploying.
Secrets (Cloud tab → Secrets)
OPENAI_API_KEYPurpose: Used exclusively inside the submit-feedback Deno edge function for sentiment scoring and topic label extraction. NEVER add this as VITE_OPENAI_KEY or reference it in any /src/ file — that bakes the key into your production JS bundle where bots can read it.
Where to get it: Go to platform.openai.com → API Keys → Create new secret key. Set a monthly usage cap (e.g. $10/mo) in the Usage Limits section as defense-in-depth.
SLACK_WEBHOOK_URLPurpose: When sentiment_label is 'negative' or 'urgent', the edge function sends a notification to your Slack channel so you catch angry feedback in real time.
Where to get it: In your Slack workspace → Apps → Incoming Webhooks → Add to Slack → select channel → copy Webhook URL. Optional.
RESEND_API_KEYPurpose: Alternative to Slack — sends admin notification emails on negative feedback, and weekly digest summaries.
Where to get it: Sign up at resend.com → API Keys → Create API Key. Free tier: 3K emails/mo. Optional.
Preflight checklist
- You are in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded).
- Add OPENAI_API_KEY to Cloud → Secrets BEFORE deploying the submit-feedback edge function. The function will fail with 401 if the key is missing.
- Lovable Free tier is enough for the full build — the prompt chain runs ~30-60 credits.
- Lovable docs use this exact app (AI-enriched feedback widget) as the canonical example for OpenAI + Supabase integration. The pattern is well-documented and works on first try if the key is in Secrets.
The starter prompt
Copy this. Paste it into Lovable Build mode (the default chat at the bottom-left of the editor). Hit send.
Build a Canny-style feedback collection tool using Vite + React + TypeScript + Tailwind CSS + shadcn/ui, backed by Supabase (Lovable Cloud). Here is the exact schema, RLS, routes, and components to scaffold:
**CRITICAL BEFORE STARTING:** The OpenAI API key MUST be in Cloud → Secrets as OPENAI_API_KEY (no VITE_ prefix). It must only be accessed via `Deno.env.get('OPENAI_API_KEY')` inside the edge function. Never reference OPENAI_API_KEY in any /src/ file and never add it as a VITE_ environment variable.
**Database migration (0001_feedback_schema.sql):**
Create five tables:
1. `profiles` — id uuid references auth.users pk, full_name text, role text default 'user'. RLS: per-user write own.
2. `boards` — id uuid pk, owner_id uuid references auth.users, slug text unique, name text, public_voting bool default true. RLS: public-read where public_voting=true, per-owner write.
3. `feedback` — id uuid pk default gen_random_uuid(), user_id uuid nullable references auth.users, submitter_email text nullable, submitter_name text nullable, page_url text, text text not null check (char_length(text) between 5 and 2000), sentiment_score float, sentiment_label text check (sentiment_label in ('positive','neutral','negative','urgent')), topic text, status text check (status in ('new','reviewing','planned','in_progress','shipped','wont_do')) default 'new', upvotes int default 0, board_id uuid nullable references boards, created_at timestamptz default now(). RLS: anon INSERT WITH CHECK (char_length(text) BETWEEN 5 AND 2000), NO SELECT for anon. Authenticated INSERT same check. Admin SELECT all via is_admin(). Submitter SELECT own where user_id=auth.uid().
4. `votes` — id uuid pk, feedback_id uuid references feedback, user_id uuid references auth.users, voted_at timestamptz default now(), unique (feedback_id, user_id). RLS: authenticated INSERT own (user_id=auth.uid()), public SELECT via count (not individual row access).
5. `status_changes` — id uuid pk, feedback_id uuid references feedback, actor_id uuid references auth.users, from_status text, to_status text, note text, created_at timestamptz default now(). RLS: admin write, admin + submitter read.
Create the is_admin() SECURITY DEFINER plpgsql function:
```sql
CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
RETURN (SELECT role FROM public.profiles WHERE id = auth.uid()) = 'admin';
END;
$$;
```
**Layouts:**
- `PublicLayout.tsx` — minimal layout for the widget (/widget) and roadmap (/roadmap/[slug]). No sidebar.
- `AdminLayout.tsx` — sidebar for admin: Inbox / Board / Roadmap / Boards / Settings. Wrap in AdminGuard.
**Pages and routes:**
- `/widget` — the embeddable feedback form: a shadcn Sheet (bottom drawer on mobile) with a multi-line Textarea, optional name + email fields, and a Submit button. This page is designed to be iframed on other sites.
- `/roadmap/[slug]` — public roadmap view: feedback grouped by status columns (Planned / In Progress / Shipped). Each card shows text snippet, topic label, upvote count. Signed-in users see an upvote button; anon users see the count only.
- `/admin/feedback` — admin inbox: table of all feedback sorted by created_at desc by default, with sort options (sentiment_score, upvotes, date). Each row shows: text snippet, sentiment chip (color-coded), topic, status, date. Click a row to open FeedbackDetail.
- `/admin/feedback/[id]` — detail view: full text, submitter info (if provided), sentiment score + label, topic, status workflow (dropdown to change status with optional note), status change audit log, and Upvote count.
- `/admin/boards` — list of feedback boards (for multi-product setups), create/edit.
- `/signin` — Supabase Auth form.
**Edge Function: submit-feedback**
Create supabase/functions/submit-feedback/index.ts:
1. Read the request body: { text, submitter_email, submitter_name, page_url, board_id }.
2. Validate: char_length(text) between 5 and 2000. Return 422 if invalid.
3. Insert into feedback: status='new', all fields. Get the inserted id.
4. Call OpenAI (gpt-5.4-mini) for sentiment + topic:
```typescript
const openaiRes = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.4-mini',
response_format: { type: 'json_object' },
messages: [{
role: 'user',
content: `Analyze this feedback and return JSON with exactly these keys: sentiment_score (float -1 to 1), sentiment_label (one of: positive, neutral, negative, urgent), topic (3-word label). Feedback: "${text}"`
}],
max_tokens: 100,
})
});
const aiData = await openaiRes.json();
const { sentiment_score, sentiment_label, topic } = JSON.parse(aiData.choices[0].message.content);
```
5. UPDATE feedback SET sentiment_score, sentiment_label, topic WHERE id = insertedId.
6. If sentiment_label is 'negative' or 'urgent' and SLACK_WEBHOOK_URL is set, fire a Slack webhook with the feedback text snippet.
7. Wrap the OpenAI call in try/catch — if it fails, log the error but DO NOT fail the submission. The feedback row already exists with NULL sentiment fields, which can be re-processed later.
8. Return { ok: true, feedback_id: insertedId }.
Always include CORS preflight:
```typescript
if (req.method === 'OPTIONS') return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization,content-type' } });
```
**Key components:**
- `FeedbackWidget.tsx` — the submission form: Textarea (5-2000 chars, character counter), optional name + email fields, Submit button with loading state. Calls the submit-feedback edge function.
- `FeedbackCard.tsx` — shows text snippet, SentimentChip, topic badge, status, upvote count in the admin board and roadmap.
- `StatusBadge.tsx` — color-coded status chip (new=gray, reviewing=blue, planned=purple, in_progress=amber, shipped=green, wont_do=red).
- `SentimentChip.tsx` — color-coded sentiment label (positive=green, neutral=slate, negative=red, urgent=orange).
- `RoadmapColumn.tsx` — one column per status in the public roadmap, with feedback cards inside.
**Styling:** friendly, low-friction widget — small footprint, single text area, rounded soft shadows; admin board uses Trello-like columns for the roadmap view; shadcn Sheet, Card, Badge, DropdownMenu, Textarea.
**Deliverables:** schema with anon-INSERT-only feedback RLS, the is_admin() function, the embeddable widget page, the submit-feedback edge function with OpenAI sentiment scoring, the admin inbox with sentiment-sorted table, status change workflow with audit log, and the public roadmap view.What this prompt generates
- Generates the full migration with 5 tables, anon-INSERT-only RLS on feedback, and the is_admin() SECURITY DEFINER function.
- Creates the embeddable /widget page with FeedbackWidget component (Sheet-based).
- Deploys the submit-feedback edge function that calls OpenAI gpt-5.4-mini for sentiment and topic labels — with the key safely from Deno.env.get, never from the browser.
- Builds the admin inbox with sentiment-sorted table and FeedbackDetail with status workflow.
- Creates the public /roadmap/[slug] view with feedback grouped by status and upvote counts.
- Wires optional Slack webhook notification for negative or urgent sentiment.
Paste into: Lovable Build mode (the default chat at the bottom-left of the editor)
Expected output
What Lovable will generate after the starter prompt runs successfully.
Files
src/layouts/PublicLayout.tsxMinimal layout for /widget and /roadmap pages
src/layouts/AdminLayout.tsxSidebar layout for /admin/* with AdminGuard
src/pages/Widget.tsxEmbeddable feedback submission form page
src/pages/Roadmap.tsxPublic roadmap grouped by status with upvotes
src/pages/admin/FeedbackInbox.tsxAdmin table of all feedback sorted by sentiment/votes/date
src/pages/admin/FeedbackDetail.tsxFull feedback detail with status workflow and audit log
src/pages/admin/Boards.tsxBoard management (multi-product setup)
src/components/FeedbackWidget.tsxThe submission form: Textarea + optional fields + Submit
src/components/FeedbackCard.tsxFeedback card for inbox and roadmap views
src/components/StatusBadge.tsxColor-coded status chip
src/components/SentimentChip.tsxColor-coded sentiment label chip
src/components/RoadmapColumn.tsxOne status column for the public roadmap view
supabase/functions/submit-feedback/index.tsInsert + OpenAI sentiment scoring + Slack notification
supabase/migrations/0001_feedback_schema.sql5 tables + RLS + is_admin() function
Routes
Embeddable feedback form — designed to be iframed
Public roadmap with status-grouped feedback and upvotes
Admin inbox — all feedback sortable by sentiment/votes/date
Feedback detail with status workflow and audit log
Board management for multi-product setups
Admin sign-in form
DB Tables
feedback| Column | Type |
|---|---|
| id | uuid primary key |
| user_id | uuid nullable references auth.users |
| submitter_email | text nullable |
| text | text not null check (char_length between 5 and 2000) |
| sentiment_score | float |
| sentiment_label | text check in ('positive','neutral','negative','urgent') |
| topic | text |
| status | text check in ('new','reviewing','planned','in_progress','shipped','wont_do') default 'new' |
| upvotes | int default 0 |
| board_id | uuid nullable references boards |
RLS: Anon + authenticated INSERT WITH CHECK text length 5-2000. NO SELECT for anon. Admin SELECT all via is_admin(). Submitter SELECT own where user_id=auth.uid().
votes| Column | Type |
|---|---|
| id | uuid primary key |
| feedback_id | uuid references feedback |
| user_id | uuid references auth.users |
| voted_at | timestamptz default now() |
RLS: Authenticated INSERT own (user_id=auth.uid()). Unique (feedback_id, user_id) prevents double-vote. Public SELECT via aggregated count only.
status_changes| Column | Type |
|---|---|
| id | uuid primary key |
| feedback_id | uuid references feedback |
| actor_id | uuid references auth.users |
| from_status | text |
| to_status | text |
| note | text |
RLS: Admin write. Admin + submitter read (where submitter is user_id on the feedback row).
boards| Column | Type |
|---|---|
| id | uuid primary key |
| owner_id | uuid references auth.users |
| slug | text unique |
| name | text |
| public_voting | bool default true |
RLS: Public-read where public_voting=true. Per-owner write.
profiles| Column | Type |
|---|---|
| id | uuid primary key references auth.users |
| full_name | text |
| role | text default 'user' |
RLS: Per-user write own.
Components
FeedbackWidgetSubmission form with character counter and Submit loading state
FeedbackCardFeedback card for inbox and roadmap with sentiment + status chips
StatusBadgeColor-coded status chip for workflow stages
SentimentChipColor-coded sentiment label (positive/neutral/negative/urgent)
RoadmapColumnOne column per status in the public roadmap view
AdminGuardChecks auth + is_admin(), redirects non-admins
Follow-up prompts
Paste these into Agent Mode one by one, in order, after the starter prompt finishes.
OpenAI sentiment edge function and RLS lockdown audit
Confirms OpenAI scoring is wired correctly and verifies anon has no SELECT access to feedback rows
Two tasks in one prompt:
**Task 1 — Verify and complete the submit-feedback edge function:**
Confirm supabase/functions/submit-feedback/index.ts exists and includes:
1. `const rawBody = await req.json()` to read the submission.
2. OpenAI call using `Deno.env.get('OPENAI_API_KEY')` — never from any /src/ file.
3. `response_format: { type: 'json_object' }` so the JSON parse is reliable.
4. try/catch around the OpenAI call — if it fails, log the error but DO NOT fail the insertion. The feedback row exists with NULL sentiment.
5. CORS preflight handler at the top.
If submit-feedback is missing the OpenAI call, add it now. Show the final function code with all five components.
**Task 2 — RLS lockdown audit:**
Run in Cloud → Database → SQL Editor:
```sql
SELECT policyname, cmd, roles FROM pg_policies WHERE tablename = 'feedback';
```
You should see:
- INSERT for {anon, authenticated}
- SELECT for {authenticated} — restricted to is_admin() OR user_id = auth.uid()
- NO SELECT for anon
If SELECT for anon appears, run: `DROP POLICY IF EXISTS feedback_anon_select ON feedback;`
Verify in an incognito browser console: `supabase.from('feedback').select()` must return 0 rows or an RLS error.When to use: Paste immediately after the starter prompt finishes — verify both the OpenAI integration and the RLS before any public launch
Slack or Resend notification on negative sentiment
Real-time Slack notification when negative or urgent feedback arrives
Extend the submit-feedback edge function to notify you in real time when feedback with sentiment_label='negative' or 'urgent' arrives.
In supabase/functions/submit-feedback/index.ts, after the UPDATE with sentiment results, add:
```typescript
if (['negative', 'urgent'].includes(sentiment_label)) {
const slackUrl = Deno.env.get('SLACK_WEBHOOK_URL');
if (slackUrl) {
await fetch(slackUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `New ${sentiment_label} feedback received:`,
blocks: [
{ type: 'section', text: { type: 'mrkdwn', text: `*Sentiment:* ${sentiment_label} (${sentiment_score?.toFixed(2)})\n*Topic:* ${topic}\n*Feedback:* ${text.slice(0, 300)}${text.length > 300 ? '...' : ''}` }},
{ type: 'actions', elements: [{ type: 'button', text: { type: 'plain_text', text: 'View in admin' }, url: `${Deno.env.get('SITE_URL')}/admin/feedback/${insertedId}` }]}
]
})
});
}
}
```
Add SLACK_WEBHOOK_URL to Cloud → Secrets. Add SITE_URL to Cloud → Secrets (your production domain). Test by submitting feedback with negative text and checking your Slack channel within 10 seconds.When to use: After the RLS audit — high value for product teams who want to catch angry feedback immediately
Public roadmap with upvotes
Public roadmap with per-status columns and authenticated upvoting — turns feedback into community engagement
Build out the public roadmap page at /roadmap/[slug] so users can upvote feedback items and see the product direction. 1. Query feedback for this board ordered by upvotes desc within each status. Group by status: Planned / In Progress / Shipped. 2. Show each group as a RoadmapColumn with feedback cards. Each card shows: text (truncated at 120 chars), topic badge, upvote count. 3. For signed-in users, show an upvote button (thumbs-up icon) next to each card. On click: - INSERT into votes (feedback_id, user_id) — if unique constraint fires (already voted), show 'Already upvoted.' - UPDATE feedback SET upvotes = upvotes + 1 WHERE id = feedback_id (use a Postgres function for atomic increment: `CREATE OR REPLACE FUNCTION increment_upvotes(feedback_id uuid) RETURNS void LANGUAGE sql AS $$ UPDATE feedback SET upvotes = upvotes + 1 WHERE id = feedback_id; $$;`) - Optimistically update the upvote count in the UI before the DB confirms. 4. For anonymous users, show only the upvote count (no button). Add a 'Sign in to upvote' tooltip. 5. Add a meta title and description for the roadmap page for SEO: `<title>[Board name] — Public Roadmap</title>`
When to use: After the core admin inbox is working — the public roadmap is the user-facing value that builds product trust
Embeddable widget via iframe with theme matching
Standalone /widget route embeddable on external sites via iframe with theme and postMessage API
Make the /widget route genuinely embeddable on external sites via an iframe. Three changes:
1. In Widget.tsx, read theme params from the URL query string: `/widget?theme=dark&primary=indigo&board_id=xxx`. Apply the theme via Tailwind's dark mode class or inline CSS variables.
2. After successful submission, communicate back to the parent page via postMessage:
```typescript
window.parent.postMessage({ type: 'feedback_submitted', feedback_id: responseData.feedback_id }, '*');
```
3. Add a close button that fires `window.parent.postMessage({ type: 'close_widget' }, '*')` so the parent page can hide the iframe after submission.
4. In vercel.json (or equivalent), add a Content-Security-Policy header that allows the widget to be framed by your client domains:
```json
{ "headers": [{ "source": "/widget", "headers": [{ "key": "X-Frame-Options", "value": "ALLOWALL" }] }] }
```
5. Provide an example embed snippet in /admin/boards/[id] so admins can copy-paste the iframe code for their site.When to use: After the admin inbox is stable — embed support is needed for the widget to actually collect feedback from your product
Weekly digest email to admins
pg_cron-triggered weekly summary email with sentiment breakdown and top-voted items
Create a weekly summary email sent every Monday morning to all admin users.
1. Create supabase/functions/weekly-digest/index.ts that:
- Queries feedback from the last 7 days grouped by sentiment_label and status.
- Gets the top 5 most-upvoted items added this week.
- Gets the 3 most recent 'negative' or 'urgent' items.
- Sends an email via Resend to all admin-role profiles with a clean summary.
2. Schedule it with pg_cron:
```sql
SELECT cron.schedule(
'weekly-feedback-digest',
'0 9 * * 1',
$$ SELECT net.http_post(url := current_setting('app.supabase_functions_endpoint') || '/weekly-digest', headers := jsonb_build_object('Authorization', 'Bearer ' || current_setting('app.service_role_key')))::void; $$
);
```
3. Format the email body as plain HTML table: New (N) | Planned (N) | Shipped (N) this week, plus the top-voted items and urgent items as bullet lists.
Add RESEND_API_KEY to Cloud → Secrets if not already present. Test by triggering the edge function manually from Cloud → Edge Functions → Invoke.When to use: After the core build is stable and you have real feedback coming in — keeps the inbox in your actual inbox
Common errors
Real error strings you'll see. Find yours, paste the fix prompt.
OpenAI API call returns 401 UnauthorizedOPENAI_API_KEY is not set in Cloud → Secrets, or it was set as VITE_OPENAI_KEY (a client-side build variable that is not available inside Deno edge functions).
Open Cloud → Secrets (not Environment Variables, not .env). Add OPENAI_API_KEY without any VITE_ prefix. In the edge function, access it only via `Deno.env.get('OPENAI_API_KEY')`. Verify with a debug log: `console.log('key length:', Deno.env.get('OPENAI_API_KEY')?.length ?? 'MISSING')` in the function. View the log output in Cloud → Logs.OpenAI key visible in built JS bundle (sk-proj- found in /_app/static/*.js)Someone added VITE_OPENAI_KEY to .env or the Lovable environment variables panel — Vite bakes all VITE_ prefixed variables into the production JavaScript bundle, where they are readable by anyone who opens DevTools or downloads the bundle.
URGENT — act immediately: (1) Rotate the key at platform.openai.com → API Keys → revoke and create new. (2) Delete VITE_OPENAI_KEY from anywhere it appears in .env or Lovable environment variables. (3) Add the new key to Cloud → Secrets as OPENAI_API_KEY (no VITE_ prefix). (4) Move all OpenAI calls from any /src/ file to the submit-feedback edge function. (5) Set a usage cap on the new key in OpenAI dashboard → Limits so future leaks are cost-bounded.
new row violates row-level security policy for table "feedback"Default scaffold granted INSERT only to authenticated users — but the widget is designed for anonymous visitor submission.
Add explicit anon INSERT policy in SQL Editor: `CREATE POLICY anon_feedback ON feedback FOR INSERT TO anon WITH CHECK (char_length(text) BETWEEN 5 AND 2000);` Do not add any SELECT policy for anon. Admin SELECT is via is_admin(); submitter SELECT is via user_id=auth.uid().
sentiment_score shows NULL for all feedback rowsThe OpenAI call failed silently (timeout, invalid API key, JSON parse error) and the try/catch swallowed the error without updating the feedback row.
In submit-feedback edge function, add explicit error logging: `console.error('OpenAI call failed:', err)`. Check Cloud → Logs to see the actual error. Common causes: wrong API key (fix in Secrets), model name typo (use 'gpt-5.4-mini', verify at platform.openai.com), or the response JSON was not parseable (add `response_format: { type: 'json_object' }` to the OpenAI request to force valid JSON output).anyone can read all feedback via /rest/v1/feedback (incognito test shows rows)Lovable scaffolded SELECT-for-anon on feedback when it added the INSERT policy — this exposes all user feedback including email addresses to anyone with your Supabase project URL.
Drop all SELECT policies for anon: `DROP POLICY IF EXISTS feedback_anon_select ON feedback;` Add the correct authenticated-only SELECT: `CREATE POLICY feedback_admin_select ON feedback FOR SELECT TO authenticated USING (is_admin() OR user_id = auth.uid());` Verify in incognito: `supabase.from('feedback').select()` must return 0 rows or an RLS error.submit-feedback function times out on long text submissionsThe OpenAI API call on long inputs can take 5-10 seconds; Supabase edge function default timeout may cut it off, causing a timeout error and a NULL sentiment row.
Set max_tokens=100 in the OpenAI request — sentiment + topic label only needs a short JSON output. If still slow, switch to an async pattern: return { ok: true, feedback_id } immediately after INSERT, and run the OpenAI call in a Supabase background job or a separate edge function invoked asynchronously. The user sees instant acknowledgment; sentiment is scored seconds later.Cost reality
What this build actually costs — no surprises on your card.
Recommended Lovable plan
Free is enough — the full build runs ~30-60 credits, fits within Free's monthly cap. Pro $25/mo only if you iterate heavily on the AI prompt or the public roadmap layout.
Monthly run cost breakdown
~30-60 credits: starter ~35, RLS audit ~20, Slack notification ~25, public roadmap ~40; duplicate detection via pgvector adds ~50 if needed total| Item | Cost |
|---|---|
| Lovable Stop iterating after build is complete | Free $0 |
| Supabase Feedback table stays tiny; even 500K feedback rows is under 500MB | Free $0 |
| OpenAI gpt-5.4-mini At 10K feedback/mo, that is $10/mo — set a usage cap in OpenAI dashboard | ~$0.001/feedback item |
| Resend Weekly digest + negative notifications; free tier is ample for most teams | Free $0 (up to 3K emails/mo) |
| Custom domain Optional — widget works on lovable.app subdomain | ~$12/yr |
Scaling notes: OpenAI cost scales linearly with feedback volume — at 10K items/month you are at ~$10/mo, still cheap. Set a usage cap at platform.openai.com to bound the blast radius if someone floods the widget. Supabase Free handles ~500K feedback rows. The only real scaling cost is human review time as the inbox grows — the weekly digest and Slack notifications reduce the need for daily inbox checks.
Production checklist
Steps to take before you share the URL with real users.
API Key Safety
Confirm OPENAI_API_KEY is in Cloud → Secrets and nowhere else
Search your codebase for 'OPENAI' or 'sk-' strings: in Lovable, click the Code panel (if on Pro Dev Mode) or export to GitHub and grep. If you find any reference in /src/ files or as a VITE_ variable, remove it immediately, rotate the key at platform.openai.com, and re-add it only to Cloud → Secrets.
Set a usage cap on the OpenAI key
Go to platform.openai.com → Settings → Limits → Set usage limit to $10-20/mo. This caps the damage if the key is ever leaked or if someone floods your widget with submissions.
RLS Verification
Verify anon INSERT / no SELECT on feedback table before launch
Open an incognito browser → browser console → run `supabase.from('feedback').select()`. Must return 0 rows. If it returns data, run the RLS audit follow-up prompt immediately.
Domain & SSL
Connect your custom domain and update SITE_URL Secret
Click Publish → Settings → Custom Domain → follow DNS instructions. After domain is live, update SITE_URL in Cloud → Secrets to your production URL so Slack notification links point to the right place.
Monitoring
Monitor Cloud → Logs for edge function errors
Click + → Cloud tab → Logs → filter by 'submit-feedback'. Check for 401 (key issue), 422 (validation failures indicating spam), or timeouts. Set up a Slack alert from the function itself for 5xx errors.
Frequently asked questions
Where exactly does my OpenAI API key go — and where should it never go?
The only correct location is Cloud → Secrets (click the + icon → Cloud tab → Secrets → add OPENAI_API_KEY). Inside the edge function, access it with Deno.env.get('OPENAI_API_KEY'). It should never appear in: any file inside /src/, any VITE_ environment variable, the .env file at project root, or any client-side config. Any VITE_ prefix bakes the value into your production JavaScript bundle, where it is readable by anyone who opens DevTools. Bots scan production bundles for 'sk-' strings constantly — a leaked OpenAI key typically racks up $100+ in charges within 6 hours.
Can users submit feedback anonymously without an account?
Yes. The feedback table has an anon INSERT RLS policy with a text-length check, so any visitor can submit without creating an account. The submitter_email and submitter_name fields are optional — useful if you want to follow up but not required. If you want to gate submission to signed-in users only (for compliance or to reduce spam), change the INSERT policy to authenticated only and remove the anon policy.
Why is OpenAI sentiment scoring server-side instead of client-side?
Three reasons. First, security: the API key would be in your JavaScript bundle client-side and readable by anyone, leading to key theft and charges on your card. Second, reliability: server-side calls can retry on failure, run asynchronously without blocking the UI, and write results back to the database independently of the browser session. Third, cost control: server-side calls go through a single endpoint where you can rate-limit, cap, and monitor usage — client-side calls give every visitor direct access to your API key's spending limit.
How accurate is gpt-5.4-mini for sentiment scoring compared to a fine-tuned model?
For broad categories (positive / neutral / negative / urgent), gpt-5.4-mini is accurate enough for product feedback — typically 85-90% agreement with human annotation on customer feedback data. It struggles with sarcasm ('great, another bug') and very short inputs ('no'). For most feedback tools, the 3-word topic label is more useful than the sentiment score anyway — it lets you cluster feedback by theme without reading every item. If you need higher accuracy for high-stakes decisions (legal, medical, financial), consider Claude Sonnet 4.6 at slightly higher cost or a fine-tuned classifier.
Can I migrate from Canny to this Lovable build?
Yes, with manual data migration. Export your Canny data as CSV (Canny supports CSV export from the board settings). Transform the CSV to match the feedback table schema (text, status, votes). Import via Supabase's bulk insert: upload the CSV to a temporary table in SQL Editor, then INSERT INTO feedback SELECT ... FROM temp_table. The status mapping: Canny's 'Under Review' → 'reviewing', 'Planned' → 'planned', 'In Progress' → 'in_progress', 'Complete' → 'shipped'. Historical votes are harder — Canny does not export individual voter data, so you will lose the per-user vote attribution and only keep counts.
How do upvotes work and why do they need authenticated users?
Upvotes are stored in the votes table with a unique (feedback_id, user_id) constraint, so each user can upvote a feedback item exactly once. Requiring authentication is necessary for this deduplication to be meaningful — anonymous upvotes would be trivially ballot-stuffable by refreshing the page or using a VPN. When a user upvotes, the edge function or RPC atomically increments feedback.upvotes and inserts a vote row. The UI reads the upvotes count from the feedback row directly (not by counting vote rows) for performance.
What is the monthly OpenAI cost at different feedback volumes?
gpt-5.4-mini pricing is approximately $0.75 per million input tokens and $4.50 per million output tokens. Each feedback analysis request uses roughly 150 input tokens (the feedback text + system prompt) and 50 output tokens (the JSON response). That is about $0.0004 per feedback item, or roughly $0.40 per 1,000 feedback items. At 1K feedback/mo: $0.40. At 10K/mo: $4. At 100K/mo: $40. Set a usage cap in OpenAI dashboard to bound costs; $10-20/mo cap is appropriate for most indie tools.
If your feedback tool needs CRM sync, multi-product boards, or enterprise compliance, what does RapidDev offer?
If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.
Need a production-grade version?
RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.
Book a free consultation30-min call. No commitment.