<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  
  <title>Hugo Amorim | Blog</title>
  <subtitle>Notes on designing and building full-stack products for startups.</subtitle>
  <link href="https://hugoamorimdev.com/blog/feed.xml" rel="self" />
  <link href="https://hugoamorimdev.com/" />
  <updated>2026-09-03T00:00:00Z</updated>
  <id>https://hugoamorimdev.com/</id>
  <author>
    <name>Hugo Amorim</name>
  </author>
  <entry>
    <title>Seven Failure Tests to Run on Your Lovable App Before Taking Payments</title>
    <link href="https://hugoamorimdev.com/blog/seven-failures-tests-to-run-on-your-lovable-app-before-taking-payments/" />
    <updated>2026-09-03T00:00:00Z</updated>
    <id>https://hugoamorimdev.com/blog/seven-failures-tests-to-run-on-your-lovable-app-before-taking-payments/</id>
    <summary>A working app is not a safe app. These tests check that a Lovable app fails correctly when users misbehave, webhooks arrive late, services time out, or a deployment goes wrong.</summary>
    <content type="html">&lt;p&gt;Most pre-launch checklists for a Lovable app ask the same questions: is authentication working, is the payment provider connected, is the database wired up? Passing those checks proves the &lt;strong&gt;happy path&lt;/strong&gt;. It does not prove the app fails safely. Real users will change URLs, refresh a checkout page mid-payment, hit a third-party outage, or arrive right after a bad deployment. An app built quickly with AI is especially likely to handle the normal case beautifully and the abnormal case not at all.&lt;/p&gt;
&lt;p&gt;Below are seven failure tests worth running on a staging copy of your app before a single real customer pays. Each one takes minutes, and each one targets a class of bug that shows up repeatedly in Lovable and Supabase projects.&lt;/p&gt;
&lt;h2&gt;1. Try to read another user&#39;s data&lt;/h2&gt;
&lt;p&gt;Create two test accounts. Sign in as the second user, then take any request or URL that includes a record ID and swap in an ID that belongs to the first user. Try it on profile pages, invoices, uploaded files, settings, and API calls.&lt;/p&gt;
&lt;p&gt;The server must reject every attempt. If the app simply hides other users&#39; records in the interface but the underlying request succeeds, that is not authorization; it is decoration. In a Supabase-backed app, the fix is &lt;a href=&quot;https://supabase.com/docs/guides/database/postgres/row-level-security&quot;&gt;Row Level Security&lt;/a&gt; with an explicit policy on every table, so the database itself refuses the query regardless of what the frontend asks for.&lt;/p&gt;
&lt;h3&gt;Check the tables you forgot you created&lt;/h3&gt;
&lt;p&gt;A subtle version of this problem is common in AI-built apps. The main tables (users, orders, projects) usually get RLS and sensible policies. But builders often create side tables along the way, such as &lt;code&gt;invites&lt;/code&gt;, &lt;code&gt;notifications&lt;/code&gt;, or &lt;code&gt;activity_log&lt;/code&gt;, and those may end up with RLS enabled but &lt;strong&gt;no policies at all&lt;/strong&gt;. RLS with zero policies blocks everything, which produces errors, and the tempting &amp;quot;fix&amp;quot; is to read those tables with the &lt;code&gt;service_role&lt;/code&gt; key instead. Once that key is in client-side code, RLS is bypassed entirely and the leak is sitting in a table nobody remembers creating.&lt;/p&gt;
&lt;p&gt;The test: list &lt;strong&gt;every&lt;/strong&gt; table, view, RPC function, and storage bucket in the project, not just the ones you know about. Confirm each has RLS on and at least one policy, and exercise every access path with an ordinary user&#39;s token, never the service key.&lt;/p&gt;
&lt;h2&gt;2. Tamper with the checkout request&lt;/h2&gt;
&lt;p&gt;Open the browser&#39;s developer tools during checkout and edit what the client sends: the price, the plan name, the quantity, or the currency. Then complete the purchase.&lt;/p&gt;
&lt;p&gt;The server should ignore all of it and compute the real amount from trusted product data it holds itself. Just as important, the app must never grant access because the user &lt;em&gt;reached a success page&lt;/em&gt;. Success URLs can be typed in manually. Entitlements should be granted only when the payment provider confirms the payment through a verified webhook or a server-side lookup, as described in &lt;a href=&quot;https://docs.stripe.com/checkout/fulfillment&quot;&gt;Stripe&#39;s checkout fulfillment guide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;3. Replay and delay the payment webhook&lt;/h2&gt;
&lt;p&gt;Send the same payment webhook to your endpoint three times. The result should be one subscription, one order, or one credit top-up, not three. Payment providers retry deliveries and occasionally send duplicates, so webhook handlers need to be &lt;strong&gt;idempotent&lt;/strong&gt;: record the event ID and skip anything already processed. Stripe covers this pattern in its &lt;a href=&quot;https://docs.stripe.com/webhooks#handle-duplicate-events&quot;&gt;webhook best practices&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then test the opposite. Delay the webhook by several minutes and confirm the account still becomes active when it eventually arrives, without the user having to do anything. If activation depends on the user being on a particular page at the moment the webhook lands, it will fail for real customers.&lt;/p&gt;
&lt;h2&gt;4. Hunt for secrets in the client&lt;/h2&gt;
&lt;p&gt;Search the built frontend bundle, browser local storage, application logs, and the full Git history for API keys and tokens. Pay attention to anything exposed through a public environment-variable prefix such as &lt;code&gt;VITE_&lt;/code&gt; or &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, which is intentionally shipped to the browser.&lt;/p&gt;
&lt;p&gt;The Supabase anon key is designed to be public and is safe in the client as long as RLS is enforced. The &lt;code&gt;service_role&lt;/code&gt; key, payment provider secret keys, and any third-party API keys are not. Anything found in client code or in a past commit should be treated as compromised and &lt;strong&gt;rotated immediately&lt;/strong&gt;, because deleting it from the current code does not remove it from history or from copies already downloaded.&lt;/p&gt;
&lt;h2&gt;5. Force an external service to fail&lt;/h2&gt;
&lt;p&gt;Pick a third-party dependency your app relies on (an email API, a payment provider, an AI model endpoint) and make it fail on staging: point it at a dead URL, or set an absurdly short timeout.&lt;/p&gt;
&lt;p&gt;Two things should happen. The user should see a safe, understandable error rather than a spinner that never ends or a blank page. And the system should record enough context to retry or investigate later, while making sure logs do not capture passwords, tokens, or sensitive customer data. Logging the full request body &amp;quot;for debugging&amp;quot; is a common way secrets end up in plain text.&lt;/p&gt;
&lt;h2&gt;6. Actually restore a backup&lt;/h2&gt;
&lt;p&gt;&amp;quot;Backups enabled&amp;quot; is a checkbox. A backup you have never restored is a hope, not a plan. Take the most recent database backup and restore it into a temporary environment. Confirm the app starts against it and the data is intact.&lt;/p&gt;
&lt;p&gt;Supabase&#39;s &lt;a href=&quot;https://supabase.com/docs/guides/platform/backups&quot;&gt;backup documentation&lt;/a&gt; explains what is included at each plan level and how point-in-time recovery differs from daily snapshots. Whichever you have, do the restore once before launch so the first time you try it is not during an incident.&lt;/p&gt;
&lt;h2&gt;7. Roll back the latest deployment&lt;/h2&gt;
&lt;p&gt;Deploy a new version, then roll back to the previous one and use the app. The older code must still work against the &lt;strong&gt;current&lt;/strong&gt; database schema. If the new release added a column and the old code cannot handle its presence, rollback is not a safe option and you have no escape hatch when a bad deploy ships.&lt;/p&gt;
&lt;p&gt;While you are there, check background jobs. If a job was in progress during the rollback, confirm it cannot run a second time and double-charge, double-email, or double-write.&lt;/p&gt;
&lt;h2&gt;The release gate&lt;/h2&gt;
&lt;p&gt;A simple test of readiness is whether you can explain, in plain language, what your app does in each of these situations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A payment succeeds but the webhook arrives ten minutes late.&lt;/li&gt;
&lt;li&gt;One user requests another user&#39;s record by ID.&lt;/li&gt;
&lt;li&gt;A third-party API disappears for an hour.&lt;/li&gt;
&lt;li&gt;The latest deployment fails and has to be rolled back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any answer is &amp;quot;I&#39;m not sure,&amp;quot; that is the test to run next. None of these require deep engineering skill. They require setting aside an hour to try to break the thing you built, before your customers do it for you.&lt;/p&gt;
&lt;h2&gt;Quick checklist&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Cross-account ID swapping is rejected by the server on every table, view, function, and bucket.&lt;/li&gt;
&lt;li&gt;Tampered checkout prices and quantities are ignored; access is granted only on verified payment.&lt;/li&gt;
&lt;li&gt;Duplicate webhooks produce one result; delayed webhooks still activate the account.&lt;/li&gt;
&lt;li&gt;No secrets in the bundle, storage, logs, or Git history; anything found is rotated.&lt;/li&gt;
&lt;li&gt;External service failures show a safe error and log useful, non-sensitive context.&lt;/li&gt;
&lt;li&gt;A backup has been restored successfully into a scratch environment.&lt;/li&gt;
&lt;li&gt;The previous deployment runs against the current schema, and jobs cannot run twice.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/lovable/comments/1w57xbl/if_your_lovable_app_works_run_these_failure_tests/&quot;&gt;If your Lovable app works, run these failure tests before taking payments (r/lovable)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://supabase.com/docs/guides/database/postgres/row-level-security&quot;&gt;Supabase: Row Level Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://supabase.com/docs/guides/platform/backups&quot;&gt;Supabase: Database Backups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.stripe.com/checkout/fulfillment&quot;&gt;Stripe: Fulfill orders after Checkout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.stripe.com/webhooks#handle-duplicate-events&quot;&gt;Stripe: Webhook best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
</feed>