<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ lovable - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ lovable - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 22 Oct 2026 23:32:09 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/lovable/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Your Own AI App Builder Like Lovable with Next.js, AWS and Sandboxes ]]>
                </title>
                <description>
                    <![CDATA[ Tools like Lovable, Bolt, and v0 feel a bit like magic the first time you use them. Honestly, I was shocked the first time I saw something like that...and you get to do all that from a chat window! It ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-your-own-ai-app-builder-with-next-js-aws-and-sandboxes/</link>
                <guid isPermaLink="false">6abf9aa749124019f564574f</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ lovable ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Sandbox ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Shrijal Acharya ]]>
                </dc:creator>
                <pubDate>Fri, 02 Oct 2026 11:51:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/32b191cd-400c-44ea-bc95-8815897f3f82.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Tools like Lovable, Bolt, and v0 feel a bit like magic the first time you use them. Honestly, I was shocked the first time I saw something like that...and you get to do all that from a chat window! It was just wild. I had this whole existential crisis the first time I tried Lovable.</p>
<p>But have you ever wondered what's actually happening under the hood? Like, how is it even possible to have one app set up a whole other app that's ready to test, share, download, and so on?</p>
<p>Somewhere, an AI model is writing code. That's clear. But how? That code needs to be installed, built, and run. And the part I was a bit skeptical about was that nobody even seems to read that code nowadays before it runs. It could be broken, it could be slow, or it could even try to do something it shouldn't like wipe out your whole system with <code>rm -rf</code>. We've had such things happen from AI, so we can't be 100% sure.</p>
<p>That's the problem this article addresses.</p>
<p>Here, you'll build your own Lovable-style AI app builder. This isn't going to be a clone. Rather, we'll dive into the logic behind Lovable to see how it really works. We'll keep the UI pretty basic.</p>
<p>For our project, a user will be able to describe an app in plain English, and an AI agent will write the code inside an isolated cloud sandbox. The AI will fixe its own errors, and then show a live preview. From there, the user can keep building on top of it through chat, roll back to any version, and publish the finished app with a single click.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This tutorial gets into some more advanced concepts, so before you start, it’ll help if you’re comfortable with:</p>
<ul>
<li><p>JavaScript or TypeScript</p>
</li>
<li><p>React and basic Next.js concepts</p>
</li>
<li><p>Node.js and a bit of working with APIs</p>
</li>
<li><p>Git and basic version control</p>
</li>
<li><p>Docker</p>
</li>
<li><p>Postgres</p>
</li>
<li><p>AWS is optional. You can follow the entire tutorial locally using MinIO instead.</p>
</li>
</ul>
<p>To run the project yourself, you’ll also need:</p>
<ul>
<li><p>Node.js 22+</p>
</li>
<li><p>pnpm</p>
</li>
<li><p>Docker installed locally</p>
</li>
<li><p>An API key for a sandbox provider (here we'll use Tensorlake Sandboxes)</p>
</li>
<li><p>An Anthropic (recommended) or OpenAI API key</p>
</li>
</ul>
<p>You don’t need to be an expert in any of these. A basic understanding is enough, and we’ll go through the important parts as we build.</p>
<h2 id="heading-whats-covered-here">What's Covered Here:</h2>
<p>In this tutorial, you'll build the whole thing from scratch. Here's what you'll learn along the way:</p>
<ul>
<li><p>Why AI generated code needs a sandbox</p>
</li>
<li><p>How to get a fresh sandbox ready in about 4 seconds instead of 30+ using memory snapshots</p>
</li>
<li><p>How to build an agent loop that writes code, checks its own work, and fixes its own errors</p>
</li>
<li><p>How to stream what the agent is doing to the browser in real time (and not lose it on a page refresh)</p>
</li>
<li><p>How to show a live preview with hot reload through your own gateway</p>
</li>
<li><p>How to version every change with Git without ever putting a token inside the sandbox</p>
</li>
<li><p>How to put idle sandboxes to sleep, wake them up when needed, and publish the final app</p>
</li>
</ul>
<p>This gets into some advanced concepts, but follow along and you'll learn a lot along the way. I definitely did while building it. 😉</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-whats-the-plan-the-architecture">What's the Plan (the Architecture)</a></p>
</li>
<li><p><a href="#heading-why-do-we-need-a-sandbox">Why Do We Need a Sandbox?</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-project">How to Set Up the Project</a></p>
</li>
<li><p><a href="#heading-core-components-in-the-application">Core Components in the Application</a></p>
<ul>
<li><p><a href="#heading-starting-a-sandbox-fast">Starting a Sandbox Fast</a></p>
</li>
<li><p><a href="#heading-the-agent-loop">The Agent Loop</a></p>
</li>
<li><p><a href="#heading-letting-the-agent-check-its-own-work">Letting the Agent Check Its Own Work</a></p>
</li>
<li><p><a href="#heading-streaming-progress-to-the-browser">Streaming Progress to the Browser</a></p>
</li>
<li><p><a href="#heading-the-live-preview-gateway">The Live Preview Gateway</a></p>
</li>
<li><p><a href="#heading-versions-with-git">Versions with Git</a></p>
</li>
<li><p><a href="#heading-sleeping-waking-and-sharing-the-sandboxes">Sleeping, Waking, and Sharing the Sandboxes</a></p>
</li>
<li><p><a href="#heading-publishing-the-app">Publishing the App</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-app-builder-in-action">App Builder in Action</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-whats-the-plan-the-architecture">What's the Plan (the Architecture)</h2>
<p>Before diving into the code, it helps to understand how everything fits together, because there are quite a few concepts worth understanding earlier.</p>
<p>The app is split into three processes and a couple of pieces of infrastructure. One thing I was very strict about from the start is that the web app should never run the AI agent itself, and the agent should never run inside the sandbox. You'll see why that matters in a bit.</p>
<img src="https://cdn.hashnode.com/uploads/covers/641fd8b0be4ca15b2ad2a590/c3274cbe-ac2d-4fbc-ab45-b0a2899a99c9.png" alt="AI app builder architecture using Next.js, Tensorlake sandboxes, Postgres, LLM workers, Git, and AWS S3." style="display: block;" width="2580" height="1635" loading="lazy">

<p>Here's the flow from start to finish:</p>
<h3 id="heading-sending-a-prompt">Sending a Prompt</h3>
<p>When a user types a prompt, the web app (Next.js) saves it, creates a "run" in Postgres, puts a job on a queue, and returns right away. So the user isn't sitting there waiting on some HTTP request while the agent does its thing.</p>
<h3 id="heading-running-the-agent">Running the Agent</h3>
<p>A worker process picks up that job. First it makes sure the project has a running sandbox, and then it starts the agent loop. The LLM decides what to do, and every tool call it makes (write a file, run a command, install a package and all) actually happens inside the sandbox.</p>
<p>Every single step is also saved to the database as an event, and that's how the browser gets them live.</p>
<h3 id="heading-showing-the-preview">Showing the Preview</h3>
<p>The generated app runs its own Vite dev server inside the sandbox. We have a small gateway service that proxies <code>http://&lt;project-id&gt;.preview.localhost:4000</code> to that dev server, including the WebSocket that Vite uses for hot reload.</p>
<p>So as the agent edits files, the preview just updates by itself, which honestly still feels kinda cool every time I see it.</p>
<h3 id="heading-saving-and-publishing">Saving and Publishing</h3>
<p>Once the agent is done, the worker commits the changes as a new version. And when the user clicks Publish, the app gets built and the static files are uploaded to S3, so the published site keeps working even when the sandbox is asleep.</p>
<p>That's pretty much the high level architecture of our application. To put it simply:</p>
<ul>
<li><p><strong>Next.js:</strong> UI and API layer</p>
</li>
<li><p><strong>Worker (Node.js +</strong> <code>pg-boss</code><strong>):</strong> Agent layer</p>
</li>
<li><p><strong>Gateway (Node.js proxy):</strong> Preview layer</p>
</li>
<li><p><strong>Postgres:</strong> State and job queue layer</p>
</li>
<li><p><strong>Cloud sandbox:</strong> Execution layer</p>
</li>
<li><p><strong>S3 (MinIO locally):</strong> Storage layer</p>
</li>
<li><p><strong>Any LLM of your choice (Claude or GPT in our case):</strong> Reasoning layer</p>
</li>
</ul>
<h2 id="heading-why-do-we-need-a-sandbox">Why Do We Need a Sandbox?</h2>
<p>If you think about it, the whole product is basically "run code that nobody has reviewed." The agent writes it, <code>npm install</code> pulls in packages whose install scripts can run pretty much anything, and then a dev server starts executing all of it. There's no way I'm running that on my own server, and you shouldn't either.</p>
<p>So every project gets its own isolated sandbox, which is basically a small VM in the cloud. When I was picking a sandbox provider for this, these are the things I actually needed:</p>
<ul>
<li><p><strong>Suspend and resume:</strong> Most projects sit idle most of the time. I wanted to put them to sleep and wake them back up in a couple of seconds, with their memory intact.</p>
</li>
<li><p><strong>Files, commands, and a terminal:</strong> The agent needs to read and write files and run commands, and it's nice to give the user a real shell too.</p>
</li>
</ul>
<p>💁 There's much more to check on when considering using something like this on prod, but these were my only hard requirements.</p>
<p>For this, I'm using <a href="https://tensorlake.ai">Tensorlake</a> sandboxes. To be clear, there's no specific reason to use this particular one. E2B, Daytona, Modal, or even your own Firecracker setup would all work, and you're free to choose whatever you prefer. I've just been using it for a few projects already, and for sandboxes it works perfectly, especially for a use case like ours.</p>
<p><strong>Note:</strong> All the sandbox code lives in a single package (<code>packages/sandbox</code>). So if you want to switch providers, that's pretty much the only place you need to touch. The rest of the app doesn't even know which provider it's talking to.</p>
<h2 id="heading-how-to-set-up-the-project">How to Set Up the Project</h2>
<p>Before you start, make sure you have the following installed:</p>
<ul>
<li><p>Node.js 22 or newer</p>
</li>
<li><p>pnpm</p>
</li>
<li><p>Docker (for Postgres and MinIO if you plan to test it locally first)</p>
</li>
</ul>
<p>You'll also need API keys for your sandbox provider and for an LLM (<a href="https://console.anthropic.com">Anthropic</a> or <a href="https://platform.openai.com">OpenAI</a>, whichever you like).</p>
<p>Start by cloning the repository and installing the dependencies:</p>
<pre><code class="language-bash">git clone https://github.com/shricodev/lovable-build-tensorlake-aws.git
cd lovable-build-tensorlake-aws
pnpm install
</code></pre>
<p>Next, create your environment file and fill in the keys:</p>
<pre><code class="language-bash">cp .env.example .env
# Add your sandbox and LLM keys, and generate AUTH_SECRET with:
openssl rand -base64 32
</code></pre>
<p>Now start the local infrastructure, create the database tables, and set up the storage bucket:</p>
<pre><code class="language-bash"># Start Postgres and MinIO
pnpm infra:up

# Create the database tables
pnpm db:migrate

# Create the bucket and lock it down
pnpm s3:setup
</code></pre>
<p>Then build the base snapshot that every new project starts from (more on this in a bit). It takes about 45 seconds:</p>
<pre><code class="language-bash">pnpm sandbox:build-base
</code></pre>
<p>Finally, start everything:</p>
<pre><code class="language-bash"># web on :3000, preview gateway on :4000
pnpm dev
</code></pre>
<p>Open <code>http://localhost:3000</code>, sign in, and describe an app. That's it! 🎉</p>
<p><strong>Note:</strong> The app supports GitHub sign in, but it also has a simple dev login so you can try it out before creating a GitHub OAuth app. Don't worry, the dev login is always disabled in production.</p>
<h2 id="heading-core-components-in-the-application">Core Components in the Application</h2>
<p>The project is huge. Walking through every single line would turn this into an hours long read, so instead I'll focus on the core components that actually make the system work. Things like the dashboard, sign in, and the code editor are pretty standard stuff, so I'll skip those.</p>
<p><strong>Note:</strong> This means that the code snippets below are trimmed down to the important parts. You can find the complete code in the repository.</p>
<h3 id="heading-starting-a-sandbox-fast">Starting a Sandbox Fast</h3>
<p>Every generated app starts from the same template: Vite, React, TypeScript, Tailwind, and a few common libraries. The naÏve way of doing it looks something like this for every new project:</p>
<ol>
<li><p>Create a fresh sandbox</p>
</li>
<li><p>Upload the template</p>
</li>
<li><p>Run <code>npm install</code></p>
</li>
<li><p>Start the dev server</p>
</li>
</ol>
<p>And that works, but it took <strong>33.4 seconds</strong> before the preview was even reachable in my tests. Most of that (about 23 seconds) was just <code>npm install</code> running on a single vCPU. I don't know about you, but I'm not staring at a spinner for that long every time I start a new project.</p>
<p>The fix is to do all of that <strong>once</strong>, and save the result as a memory snapshot. A memory snapshot captures the files, the RAM, and the running processes. So when you restore it, the dev server is already running. Nothing has to boot again.</p>
<p>Here's the core of the base snapshot builder:</p>
<pre><code class="language-ts">export async function buildBaseSnapshot(log: Logger) {
  // Cold path, done once: create, upload template, npm install, git init,
  // verify it builds, start the dev server and warm up Vite's cache.
  const { ps } = await coldCreateFromTemplate({
    name: `base-${Date.now()}`,
    log,
    verify: true,
  });

  try {
    // Memory checkpoint: files + RAM + running processes.
    const snapshotId = await ps.checkpoint();
    writeBaseSnapshot({ snapshotId, createdAt: new Date().toISOString() });
  } finally {
    await ps.terminate();
  }
}
</code></pre>
<p>With that in place, creating a sandbox for a new project is just a restore, and then we lock it down:</p>
<pre><code class="language-ts">static async createFromSnapshot(opts: { snapshotId: string; name: string; log: Logger }) {
  const sb = await Sandbox.create({ snapshotId: opts.snapshotId, name: opts.name, timeoutSecs: 600 });
  const ps = new ProjectSandbox(sb, opts.log);

  await sb.update({
    exposedPorts: [5173],              // the Vite dev server, reachable through the proxy
    allowUnauthenticatedAccess: false, // the port URL is never public
    network: {
      allowInternetAccess: true,
      allowOut: ["registry.npmjs.org"], // npm and nothing else
      denyOut: [],
    },
  });
  return ps;
}
</code></pre>
<p>A few things worth noting here:</p>
<ul>
<li><p><code>exposedPorts</code> makes the dev server reachable through the provider's proxy, but only if you have our API key. We'll use that later in the gateway.</p>
</li>
<li><p>The <code>network</code> block is an allow list. Once <code>allowOut</code> has an entry in it, everything else is blocked.</p>
<p>💁 I actually tested this with <code>example.com</code>, <code>github.com</code>, and the cloud metadata IP, and all of them were blocked while npm still worked just fine.</p>
</li>
<li><p>There are no secrets in the sandbox environment at all. No LLM keys, no database URL, no AWS keys, nothing. So even if a generated app or some prompt injection tries to steal something, there's simply nothing in there to steal.</p>
</li>
</ul>
<p>Here are the numbers I got, measured all the way until the preview actually loads:</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Time</th>
</tr>
</thead>
<tbody><tr>
<td>Cold (create, install, start dev server)</td>
<td>33.4s</td>
</tr>
<tr>
<td>Restore from the memory snapshot</td>
<td>4.0s</td>
</tr>
<tr>
<td>Wake a sleeping sandbox</td>
<td>2.4s</td>
</tr>
</tbody></table>
<p>That's roughly 8 times faster. How cool is that? 😎</p>
<h3 id="heading-the-agent-loop">The Agent Loop</h3>
<p>The agent loop is the brain of the entire system. Every time a user sends a prompt, this is what runs.</p>
<p>The idea is simple even if the implementation isn't. You give the LLM a goal and some tools, let it call them, feed the results back, and repeat until it's done.</p>
<p>These are the tools the agent gets:</p>
<ul>
<li><p><code>list_files</code>, <code>read_file</code>, <code>write_file</code>, <code>edit_file</code>, and <code>delete_file</code></p>
</li>
<li><p><code>run_command</code> for quick checks like <code>npx tsc --noEmit</code></p>
</li>
<li><p><code>install_packages</code> for adding npm packages</p>
</li>
<li><p><code>get_dev_server_logs</code> and <code>get_browser_errors</code> for debugging</p>
</li>
<li><p><code>finish</code>, which the agent calls when it thinks it's done</p>
</li>
</ul>
<p>Each tool is just a small file with a Zod schema and a <code>run</code> function. Here's <code>edit_file</code> for example:</p>
<pre><code class="language-ts">export const editFile = defineTool({
  name: "edit_file",
  description:
    "Replace one exact snippet in a file. `search` must match exactly and occur exactly once.",
  schema: z.object({
    path: z.string(),
    search: z.string().min(1),
    replace: z.string(),
  }),
  async run({ path, search, replace }, { sandbox }) {
    const text = await sandbox.readFile(path);
    const count = text.split(search).length - 1;
    if (count !== 1) {
      return {
        isError: true,
        content: `search text occurs ${count} times in ${path}`,
      };
    }
    await sandbox.writeFile(
      path,
      text.replace(search, () =&gt; replace),
    );
    return { content: `Edited ${path}`, changedFiles: [path] };
  },
});
</code></pre>
<p>The Zod schema is doing two jobs here. It gets converted to JSON Schema for the LLM (with <code>z.toJSONSchema</code>), and it also validates whatever the model sends back before anything touches the sandbox. If the input is invalid, the error just goes back to the model instead of crashing the whole run.</p>
<p>Then the actual loop runs:</p>
<pre><code class="language-ts">while (true) {
  if (signal.aborted) return result("cancelled");

  const res = await llm.chat({
    system: SYSTEM_PROMPT,
    messages,
    tools,
    signal,
    onText,
  });
  messages.push(res.message);

  const results = [];
  for (const call of res.message.toolCalls) {
    const out = await executeTool(call.name, call.input, ctx); // validate + run
    results.push({
      type: "tool_result",
      toolCallId: call.id,
      content: out.content,
      isError: out.isError,
    });
    if (out.finish) finishCalled = true;
  }

  if (!finishCalled) {
    messages.push({ role: "user", content: results });
    continue;
  }

  // The agent says it's done. Now we check. (next section)
}
</code></pre>
<p>The <code>llm.chat</code> call is a small wrapper I wrote that supports both Anthropic and OpenAI with the same interface, so you can switch models from a dropdown in the UI.</p>
<p>On the Anthropic side, prompt caching is turned on, and to be honest, it does most of the heavy lifting on cost. A typical turn reads over 120K tokens, and almost all of them come straight from the cache.</p>
<h4 id="heading-keeping-file-paths-safe">Keeping File Paths Safe</h4>
<p>Now this one's a bit sneaky, and I only found it because I was poking around. The sandbox file API blocks paths with <code>..</code> in them, but it happily <strong>follows symlinks</strong>. So if the generated code creates a <code>leak.txt</code> that points to <code>/etc/passwd</code>, reading <code>leak.txt</code> gives you back the password file. Not great.</p>
<p>So every file tool resolves the real path inside the sandbox before touching anything:</p>
<pre><code class="language-ts">async safePath(relPath: string) {
  const abs = resolveProjectPath(relPath); // rejects "..", absolute paths, NUL bytes
  const r = await this.sb.run("realpath", { args: ["-m", "--", abs] });
  const real = r.stdout.trim();
  if (!isInsideApp(real)) throw new SandboxPathError(relPath, "resolves outside the project");
  return real;
}
</code></pre>
<p><code>realpath -m</code> follows every symlink and tells you where the path actually points to. If that ends up outside the project folder, the call just fails.</p>
<h4 id="heading-keeping-the-context-small">Keeping the Context Small</h4>
<p>Instead of replaying every past tool call on each new prompt, every turn starts a fresh conversation with:</p>
<ul>
<li><p>a one line summary of each earlier turn (what the user asked, and what the agent did)</p>
</li>
<li><p>the file tree and the list of installed packages</p>
</li>
<li><p>the current <code>src/App.tsx</code></p>
</li>
</ul>
<p>The agent reads anything else it needs with its tools. This keeps the cost of a turn pretty much flat, even after a project has had 20 prompts.</p>
<h3 id="heading-letting-the-agent-check-its-own-work">Letting the Agent Check Its Own Work</h3>
<p>LLMs are really weird. They'll happily tell you everything works when the build is basically on fire. So when the agent calls <code>finish</code>, we don't just take its word for it. We run three checks inside the sandbox:</p>
<pre><code class="language-ts">export async function runChecks(sandbox: ProjectSandbox) {
  const tsc = await sandbox.exec("npx tsc --noEmit -p . 2&gt;&amp;1", {
    timeoutSecs: 120,
  });
  const build = await sandbox.exec(
    "npx vite build --outDir /tmp/build --emptyOutDir --logLevel error 2&gt;&amp;1",
    { timeoutSecs: 180 },
  );
  const render = await renderCheck(sandbox); // renders the app once in a fake DOM

  return {
    ok: tsc.exitCode === 0 &amp;&amp; build.exitCode === 0 &amp;&amp; render.ok,
    typecheck: { ok: tsc.exitCode === 0, output: tsc.stdout },
    build: { ok: build.exitCode === 0, output: build.stdout },
    render,
  };
}
</code></pre>
<p>The first check runs TypeScript’s type checker to catch type errors without generating any files. The second runs a full Vite production build to make sure the app can actually compile successfully.</p>
<p>The third one is where it gets interesting. A lot of bugs only show up at runtime, stuff like <code>Cannot read properties of undefined (reading 'map')</code>. The usual answer to this is a headless browser, but that's heavy and slow in a small sandbox, and I really didn't want to go down that road.</p>
<p>So instead, the template ships a tiny script that renders the app once with <a href="https://github.com/capricorn86/happy-dom"><code>happy-dom</code></a> (a fake DOM for Node.js) and Vite's <code>ssrLoadModule</code>:</p>
<pre><code class="language-js">GlobalRegistrator.register({ url: "http://localhost:5173/" });
document.body.innerHTML = '&lt;div id="root"&gt;&lt;/div&gt;';
console.error = (...args) =&gt; errors.push(args.join(" "));

await server.ssrLoadModule("/src/main.tsx"); // runs the real app entry
await new Promise((r) =&gt; setTimeout(r, 1500)); // let React render

const rendered = document.getElementById("root").innerHTML.trim().length &gt; 0;
console.log(
  JSON.stringify({ ok: rendered &amp;&amp; errors.length === 0, rendered, errors }),
);
</code></pre>
<p>It takes about 2 seconds, and it caught the <code>undefined.map</code> crash along with the exact line in <code>App.tsx</code>. Noiceee!</p>
<p>If any of the checks fail, the errors go straight back to the agent as a new message, and it gets another round to fix them:</p>
<pre><code class="language-ts">lastCheck = await runChecks(sandbox);
if (lastCheck.ok) return result("succeeded");
if (healRounds &gt;= maxHeal)
  return result("failed", { error: describeFailures(lastCheck) });

healRounds++;
messages.push({
  role: "user",
  content: [
    ...results,
    {
      type: "text",
      text: `Verification failed. Fix these problems, then call finish again.\n\n${describeFailures(lastCheck)}`,
    },
  ],
});
</code></pre>
<p>It stops after 3 rounds by default. If it's still broken after that, the user gets an honest "I couldn't finish this one" with the actual errors.</p>
<h3 id="heading-streaming-progress-to-the-browser">Streaming Progress to the Browser</h3>
<p>The agent runs in the worker, but the user is looking at the browser. So somehow every step ("Wrote <code>src/App.tsx</code>", "Ran <code>npx tsc --noEmit</code>", "Checks passed" and all) has to get from one to the other as it happens.</p>
<p>The worker writes each step as a row in a <code>run_events</code> table and then fires a Postgres <code>NOTIFY</code>:</p>
<pre><code class="language-ts">export async function appendEvent(
  db: Db,
  e: { projectId: string; runId?: string; type: string; payload: unknown },
) {
  const [row] = await db
    .insert(runEvents)
    .values(e)
    .returning({ id: runEvents.id });
  await db.$client.notify(
    "events",
    JSON.stringify({ projectId: e.projectId, id: row.id }),
  );
  return row.id;
}
</code></pre>
<p>On the web side, a Next.js route handler streams these events to the browser with Server Sent Events. When the browser connects, it first replays everything from the active run, and then it just keeps listening for new rows:</p>
<pre><code class="language-ts">const pump = async () =&gt; {
  const rows = await db
    .select()
    .from(runEvents)
    .where(and(eq(runEvents.projectId, project.id), gt(runEvents.id, cursor)))
    .orderBy(asc(runEvents.id));

  for (const r of rows) {
    cursor = r.id;
    send(`id: ${r.id}\ndata: ${JSON.stringify(r)}\n\n`);
  }
};

bus.on(project.id, pump); // fired by a single LISTEN connection per process
pump(); // replay first
</code></pre>
<p>Since all the events live in the database, refreshing the page in the middle of a run doesn't lose anything. The browser reconnects, replays the run so far, and continues from where it left off. The Stop button works the same way, just in reverse. The web app sends a <code>NOTIFY</code> with the run ID, and whichever worker is holding that run aborts it.</p>
<p><strong>Note:</strong> Streamed text from the LLM comes in token by token, and saving every token would mean hundreds of rows per turn. So the worker buffers it and writes one row every 250ms instead. I also had a small bug here where events landed out of order because each write was its own promise. Pushing every write through a single promise chain fixed it.</p>
<h3 id="heading-the-live-preview-gateway">The Live Preview Gateway</h3>
<p>The generated app's dev server runs inside the sandbox on port 5173, and the provider exposes it at a URL like <code>https://5173-&lt;sandbox-id&gt;.sandbox.example</code>. Now, you could just put that URL in an iframe and call it a day, but there are two problems with that:</p>
<ol>
<li><p>To load it, the browser would need our sandbox API key. Yeah, that's clearly not happening.</p>
</li>
<li><p>And if we made it public instead, anyone who guessed the URL could open it, and we'd have no control over who sees what.</p>
</li>
</ol>
<p>So we put our own small gateway in front of it. It maps <code>&lt;project-id&gt;.preview.localhost:4000</code> to the right sandbox and adds the API key on the server side:</p>
<pre><code class="language-ts">const proxy = createProxyServer({ changeOrigin: true, secure: true, ws: true });

const server = http.createServer(async (req, res) =&gt; {
  const projectId = HOST_RE.exec(req.headers.host ?? "")?.[1];
  const target = await resolve(projectId); // project -&gt; sandbox, cached for a few seconds
  if (!target.sandboxId) return send(res, noPreviewPage());

  delete req.headers.cookie; // never forward the visitor's credentials
  delete req.headers.authorization;
  proxy.web(req, res, {
    target: previewUrlFor(target.sandboxId),
    headers: { authorization: `Bearer ${API_KEY}` },
  });
});

// Vite's hot reload runs over a WebSocket, so upgrades get proxied too.
server.on("upgrade", async (req, socket, head) =&gt; {
  const target = await resolve(HOST_RE.exec(req.headers.host ?? "")?.[1]);
  proxy.ws(req, socket, head, {
    target: previewUrlFor(target.sandboxId).replace(/^https/, "wss"),
    headers: { authorization: `Bearer ${API_KEY}` },
  });
});
</code></pre>
<p>That <code>upgrade</code> handler is what makes the preview feel alive. Whenever the agent edits a file, Vite pushes the change over the WebSocket, and the preview updates without a reload.</p>
<p>There's also another reason for having the gateway that's pretty easy to miss. The preview runs on a <strong>different origin</strong> (<code>*.preview.localhost</code>) than the main app (<code>localhost:3000</code>). So a generated app can never read the main app's cookies or call its API as the logged in user. And since <code>*.localhost</code> resolves to <code>127.0.0.1</code> in modern browsers, you don't even need to touch your hosts file for this.</p>
<p><strong>Note:</strong> The template also injects a tiny script into the preview that listens for <code>window.onerror</code>, and posts them to the parent window. The workspace then forwards those to the backend, and that's where the agent's <code>get_browser_errors</code> tool gets real runtime errors from.</p>
<h3 id="heading-versions-with-git">Versions with Git</h3>
<p>Every finished prompt becomes a version. So, the user can pretty much revert to a specific "prompt", more like <code>git reset</code>.</p>
<p>For this, plain old Git inside the sandbox works great. The base snapshot already has a repo with the template as the first commit, and after each successful turn, the worker commits everything:</p>
<pre><code class="language-ts">export async function commitAll(ps: ProjectSandbox, message: string) {
  const out = await git(
    ps,
    `git add -A
if git diff --cached --quiet; then
  echo NOCHANGE
else
  git commit -q -m "$MSG"
  git rev-parse HEAD
  git show --name-only --format= HEAD
fi`,
    { MSG: message },
  );
  if (out.trim() === "NOCHANGE") return null;
  const [sha, ...files] = out.trim().split("\n");
  return { sha, files };
}
</code></pre>
<p><strong>Note:</strong> You might be wondering why there's no <code>exit 0</code> in there. Commands run under <code>bash -l</code>, and an explicit <code>exit</code> in a login shell runs <code>~/.bash_logout</code>, whose last command failed on this image and turned my exit code into a failure. This one honestly took me way longer to figure out. 😭</p>
<p>Restoring never rewrites history. It makes the files match the old commit exactly, and then commits that as a brand new version on top. So "restore version 1" creates version 5, and you can still go back to version 4 whenever you want. Nothing gets lost.</p>
<h4 id="heading-keeping-the-history-durable-without-tokens-in-the-sandbox">Keeping the History Durable (Without Tokens in the Sandbox)</h4>
<p>Git inside the sandbox is great, but it only lives as long as the sandbox does. So after each version, the history also gets pushed to a hosted Git repository.</p>
<p>The straightforward way to do this would be to give the sandbox a Git token and just run <code>git push</code>. But the tokens I had access to were scoped to the <strong>whole project</strong>, not a single repo. Putting one of those inside a sandbox full of untrusted code? Yeah, no thanks.</p>
<p>So the sandbox never pushes anything. It creates a <code>git bundle</code> (basically the entire repo in a single file), the worker reads that file out, and the worker does the push itself:</p>
<pre><code class="language-ts">export async function pushToHostedGit(
  ps: ProjectSandbox,
  repo: string,
  dataDir: string,
) {
  await git(ps, "git bundle create -q /tmp/repo.bundle main");
  const bundle = await ps.sb.readFile("/tmp/repo.bundle");

  const mirror = join(dataDir, "git", `${repo}.git`); // a bare repo on the worker
  writeFileSync(join(mirror, "incoming.bundle"), bundle);
  await run("git", [
    "-C",
    mirror,
    "fetch",
    "-q",
    "--force",
    "incoming.bundle",
    "+refs/heads/main:refs/heads/main",
  ]);

  const cred = await repos.credential(repo); // short lived token, only on the worker
  await run("git", [
    "-C",
    mirror,
    "-c",
    `http.extraHeader=Authorization: Basic ${basic(cred)}`,
    "push",
    "-q",
    "--force",
    url,
    "main",
  ]);
}
</code></pre>
<p>A nice side effect of this is that it also powers <strong>remix</strong>. When someone copies a shared project, the worker loads the source project's bundle from the hosted repo into a brand new sandbox, and the original sandbox doesn't even have to wake up for it.</p>
<h3 id="heading-sleeping-waking-and-sharing-the-sandboxes">Sleeping, Waking, and Sharing the Sandboxes</h3>
<p>A running sandbox costs money even when nobody's working on it. So there's a small job that runs every minute and suspends any sandbox that hasn't had an agent run for 10 minutes. Suspending keeps the memory, so the dev server comes back exactly as it was.</p>
<p>Waking up happens in the gateway. If someone opens the preview of a sleeping project, the gateway shows a small "Waking up your app..." page that keeps refreshing itself, and resumes the sandbox in the background:</p>
<pre><code class="language-ts">if (isPage &amp;&amp; target.status === "suspended") {
  const outcome = wake(projectId, target, log); // deduplicated per project
  const done = await Promise.race([outcome, timeout(4000, "pending")]);
  if (done === "busy") return send(res, busyPage());
  if (done !== "running") return send(res, wakingPage()); // auto refreshes
}
</code></pre>
<p>In my tests, a sleeping sandbox woke up in about 3 seconds, and the app loaded right after. 🎊</p>
<h4 id="heading-sharing-a-limited-number-of-sandboxes">Sharing a Limited Number of Sandboxes</h4>
<p>Most sandbox providers limit how many sandboxes you can run at the same time, especially on a free plan. So I added a small concurrency checker, and it all happens in Postgres with an advisory lock, so two workers never end up making the same decision at the same time:</p>
<pre><code class="language-ts">async function decide(db: Db, projectId: string) {
  return db.transaction(async (tx) =&gt; {
    await tx.execute(sql`select pg_advisory_xact_lock(${LOCK_KEY})`);

    const live = await runningSandboxesOldestFirst(tx);
    if (live.some((s) =&gt; s.projectId === projectId)) return { kind: "ok" };
    if (live.length &lt; concurrencyLimit()) return { kind: "ok" };

    // Full. Free a slot by suspending the least recently used idle sandbox.
    const victim = live.find((s) =&gt; !busyProjects.has(s.projectId));
    if (victim) return { kind: "evict", sandboxId: victim.sandboxId };

    return { kind: "wait", position }; // everything is busy, just wait...
  });
}
</code></pre>
<p>To test this, I set the limit to 1 and sent a prompt to one project while another project's sandbox was just sitting idle. The idle one went to sleep, and the new one took its slot. Then I sent a prompt to the first project while the second was still working, and the UI showed "Waiting for a free sandbox, #1 in line" until the slot freed up. If you have a bigger plan, you just raise the limit in <code>.env</code> and nothing else changes.</p>
<h3 id="heading-publishing-the-app">Publishing the App</h3>
<p>The preview is great while you're building, but you don't want your published app to depend on a sandbox that goes to sleep every 10 minutes. 🫩</p>
<p>So publishing builds the app once and turns it into plain static files:</p>
<pre><code class="language-ts">export async function publish(project: Project) {
  const ps = await projectSandbox(project.id); // wakes it if needed
  const build = await ps.exec(
    "rm -rf dist &amp;&amp; npx vite build --outDir dist --emptyOutDir 2&gt;&amp;1",
    { timeoutSecs: 180 },
  );
  if (build.exitCode !== 0)
    throw new HttpError(422, `The build failed:\n${build.stdout.slice(-1500)}`);

  const files = await listFiles(ps, "dist");
  const prefix = `published/${slug}/${versionId}/`;

  for (const f of files) {
    const bytes = await ps.readBytes(`dist/${f}`);
    await storage.put(prefix + f, bytes, contentTypeFor(f), cacheControlFor(f));
  }

  await savePublishedSite({ projectId: project.id, slug, s3Prefix: prefix });
  return { url: publishedUrl(slug) };
}
</code></pre>
<p>The gateway then serves those files from S3 at <code>http://&lt;slug&gt;.app.localhost:4000</code>. Files that Vite hashes (like <code>assets/index-CScgwd68.js</code>) get cached forever, and <code>index.html</code> always gets revalidated, so updates show up right away. Any path without a file extension falls back to <code>index.html</code>, so client side routing works as well.</p>
<p>And published sites get their own subdomain on purpose. If they lived under the main app's domain, a published app's JavaScript could call your API with the cookies of whoever is looking at it, and you really don't want that. 😺</p>
<p>In my tests, publishing took about 6 seconds, and the published site loaded in 4ms with the sandbox asleep, because it never touches the sandbox at all.</p>
<h2 id="heading-app-builder-in-action">App Builder in Action</h2>
<p>Here's a quick demo of the app builder in action:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/hE96nLJo_fc" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>I also ran a small eval out of curiosity. It sends the same three prompts (a habit tracker, a kanban board, and an expense dashboard with charts) to two different models, each in a fresh sandbox:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Passed (build + render)</th>
<th>Avg time</th>
</tr>
</thead>
<tbody><tr>
<td>Claude Sonnet 5</td>
<td>3/3</td>
<td>135s</td>
</tr>
<tr>
<td>GPT 5.5</td>
<td>3/3</td>
<td>92s</td>
</tr>
</tbody></table>
<p>All six apps built and rendered on the first check, without needing a single fix round, which honestly surprised me a bit. On Claude, each app cost somewhere around $0.15 to $0.20.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>So, what do you think of the project? This was truly one of the most fun projects I've worked on in a while since this AI stuff has taken over raw coding. 🤦‍♂️</p>
<p>When you use tools like Lovable, it's easy to think it's all about the prompt and the model. But once you build one yourself, you realize most of the work is everything around the model, like where the code runs, how fast it starts, how you show it to the user, and how you keep it from doing something it shouldn't.</p>
<p>If there's one thing I'd want you to take away from this, it's the sandbox part. Treat AI generated code as untrusted, give it its own small machine with no secrets and almost no network access. Memory snapshots and suspend/resume then take care of making it fast and cheap.</p>
<p>There's still a lot of room to extend this. You could add a small backend (like Hono and SQLite) to the template so users can build full stack apps, let users click an element in the preview to edit exactly that component, or generate a few design variations of the same prompt side by side. I'm just too exhausted to implement that right now. I'll leave it up to you. ✌️</p>
<p>The foundation is there. The rest is just building on top of it.</p>
<p>You can find the complete source code here: <a href="https://github.com/shricodev/lovable-build-tensorlake-aws">shricodev/lovable-build-tensorlake-aws</a></p>
<p>So, that's it for this article. Thank you so much for reading! See you next time. 🫡</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Lovable Responsibly ]]>
                </title>
                <description>
                    <![CDATA[ Building an app used to feel like assembling furniture without instructions, while missing half the screws. Today, AI-powered tools such as Lovable can help you turn an idea into a working web applica ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-lovable-responsibly/</link>
                <guid isPermaLink="false">6aa2d601395968ffa3528970</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ lovable ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Eva J Patel ]]>
                </dc:creator>
                <pubDate>Thu, 10 Sep 2026 16:08:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/985f1786-0356-43fe-94c5-697f1938b118.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Building an app used to feel like assembling furniture without instructions, while missing half the screws. Today, AI-powered tools such as Lovable can help you turn an idea into a working web application by describing what you want in plain language.</p>
<p>That's exciting. It's also a responsibility.</p>
<p>Lovable can help you move quickly, experiment with ideas, and create useful software. But speed shouldn't replace careful thinking. A generated app can contain security problems, confusing user experiences, inaccurate information, or code that works in a demonstration but falls apart in real life.</p>
<p>In this guide, you'll learn practical ways to use Lovable while keeping security, privacy, accessibility, and user safety in mind. We'll cover how to write clearer prompts, protect sensitive information, test authentication and authorization, validate user input, work with realistic test data, review AI-generated code, and decide when an application is ready to share.</p>
<p>By the end, you'll have a simple workflow for building with Lovable more responsibly without giving up the speed and creativity that make AI-powered development useful.</p>
<h2 id="heading-what-well-cover">What We'll Cover:</h2>
<ul>
<li><p><a href="#heading-what-is-lovable">What Is Lovable?</a></p>
</li>
<li><p><a href="#heading-why-responsible-use-matters">Why Responsible Use Matters</a></p>
</li>
<li><p><a href="#heading-start-with-a-clear-and-straightforward-idea">Start With a Clear and Straightforward Idea</a></p>
</li>
<li><p><a href="#heading-do-not-enter-sensitive-information-unnecessarily">Do Not Enter Sensitive Information Unnecessarily</a></p>
</li>
<li><p><a href="#heading-protect-secrets-with-environment-variables">Protect Secrets With Environment Variables</a></p>
</li>
<li><p><a href="#heading-understand-what-your-app-does">Understand What Your App Does</a></p>
</li>
<li><p><a href="#heading-build-security-into-your-prompts">Build Security Into Your Prompts</a></p>
</li>
<li><p><a href="#heading-test-authentication-and-authorization-separately">Test Authentication and Authorization Separately</a></p>
</li>
<li><p><a href="#heading-validate-all-user-input">Validate All User Input</a></p>
</li>
<li><p><a href="#heading-be-careful-with-generated-dependencies">Be Careful With Generated Dependencies</a></p>
</li>
<li><p><a href="#heading-design-for-accessibility">Design for Accessibility</a></p>
</li>
<li><p><a href="#heading-avoid-dark-patterns">Avoid Dark Patterns</a></p>
</li>
<li><p><a href="#heading-handle-errors-effectively">Handle Errors Effectively</a></p>
</li>
<li><p><a href="#heading-be-honest-about-ai-generated-features">Be Honest About AI-Generated Features</a></p>
</li>
<li><p><a href="#heading-protect-personal-data">Protect Personal Data</a></p>
</li>
<li><p><a href="#heading-respect-copyright-and-ownership">Respect Copyright and Ownership</a></p>
</li>
<li><p><a href="#heading-test-with-realistic-but-fake-data">Test With Realistic but Fake Data</a></p>
</li>
<li><p><a href="#heading-test-before-you-share-the-app">Test Before You Share the App</a></p>
</li>
<li><p><a href="#heading-ask-lovable-to-review-its-own-work">Ask Lovable to Review Its Own Work</a></p>
</li>
<li><p><a href="#heading-learn-from-the-generated-code">Learn From the Generated Code</a></p>
</li>
<li><p><a href="#heading-use-lovable-for-prototyping-without-pretending-it-is-production-ready">Use Lovable for Prototyping Without Pretending It Is Production-Ready</a></p>
</li>
<li><p><a href="#heading-create-a-simple-responsible-development-workflow">Create a Simple Responsible Development Workflow</a></p>
</li>
<li><p><a href="#heading-a-responsible-prompt-template">A Responsible Prompt Template</a></p>
</li>
<li><p><a href="#heading-the-golden-rule-of-ai-app-building">The Golden Rule of AI App Building</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-what-is-lovable">What Is Lovable?</h2>
<p>Lovable is an AI-powered app-building platform that allows you to describe an application using natural language. Instead of writing every line of code manually, you can explain what you want and let the tool generate parts of the interface, functionality, and application structure.</p>
<p>For example, you might write:</p>
<pre><code class="language-text">Create a task manager with user accounts, a dashboard, task categories, due dates, and a button for marking tasks as complete.
</code></pre>
<p>Lovable may then generate a starting point that you can review, test, and improve.</p>
<p>The key phrase is “starting point.” AI-generated software isn't automatically finished software. Think of Lovable as a very fast coding partner that needs clear instructions, thoughtful reviews, and occasional reminders not to put a banana-shaped button in the middle of your login form.</p>
<h2 id="heading-why-responsible-use-matters">Why Responsible Use Matters</h2>
<p>AI app builders make software development more accessible, but accessibility comes with responsibility. When you create an app, you're making decisions that can affect real people.</p>
<p>Your application might collect names, email addresses, messages, payment details, health information, or location data. It might make recommendations, display important information, or control access to something valuable.</p>
<p>A small mistake can create large problems.</p>
<p>Responsible development helps you:</p>
<ul>
<li><p>Protect user information</p>
</li>
<li><p>Reduce security risks</p>
</li>
<li><p>Avoid misleading users</p>
</li>
<li><p>Create accessible experiences</p>
</li>
<li><p>Respect copyright and ownership</p>
</li>
<li><p>Test your application before sharing it</p>
</li>
<li><p>Understand the code and services your app uses</p>
</li>
<li><p>Make decisions that are fair and explainable</p>
</li>
</ul>
<p>You don't need to be a security expert to use Lovable responsibly. But you do need to slow down long enough to ask good questions.</p>
<h2 id="heading-start-with-a-clear-and-straightforward-idea">Start With a Clear and Straightforward Idea</h2>
<p>Before asking Lovable to build an app, describe the problem you want to solve.</p>
<p>A vague prompt such as this:</p>
<pre><code class="language-plaintext">Build a cool productivity app.
</code></pre>
<p>leaves a lot of room for confusion.</p>
<p>A clearer prompt might look like this:</p>
<pre><code class="language-plaintext">Build a simple productivity app for students. Users should be able to create tasks, assign a due date, mark tasks as complete, and filter tasks by status. Use plain language, a calm color palette, and a layout that works well on phones and desktop screens.
</code></pre>
<p>A strong prompt usually explains:</p>
<ul>
<li><p>Who the app is for</p>
</li>
<li><p>What problem it solves</p>
</li>
<li><p>What users should be able to do</p>
</li>
<li><p>What information the app stores</p>
</li>
<li><p>What the interface should feel like</p>
</li>
<li><p>What the app should not do</p>
</li>
<li><p>What platform or screen sizes it should support</p>
</li>
</ul>
<p>Clear instructions make it easier to review the result. They also reduce the chance that the AI invents unnecessary features that make your project more complicated than a group project with twelve shared spreadsheets.</p>
<h2 id="heading-dont-enter-sensitive-information-unnecessarily">Don't Enter Sensitive Information Unnecessarily</h2>
<p>When working with an AI-powered development tool, avoid including sensitive information in prompts unless it's genuinely necessary and handled through an appropriate process.</p>
<p>Don't paste in:</p>
<ul>
<li><p>Passwords</p>
</li>
<li><p>Private API keys</p>
</li>
<li><p>Authentication tokens</p>
</li>
<li><p>Credit card numbers</p>
</li>
<li><p>Personal identification numbers</p>
</li>
<li><p>Private customer records</p>
</li>
<li><p>Confidential business documents</p>
</li>
<li><p>Unreleased product details</p>
</li>
<li><p>Medical records</p>
</li>
<li><p>Private conversations</p>
</li>
</ul>
<p>Use placeholders instead:</p>
<pre><code class="language-plaintext">Use a placeholder for the payment provider API key.
</code></pre>
<p>Or:</p>
<pre><code class="language-plaintext">Connect to an email service using an environment variable named EMAIL\_API\_KEY. Do not hardcode the key in the source code.
</code></pre>
<p>A placeholder keeps your project easier to share, review, and maintain. It also prevents the classic “I accidentally published a secret to the internet” plot twist.</p>
<h2 id="heading-protect-secrets-with-environment-variables">Protect Secrets With Environment Variables</h2>
<p>Secrets shouldn't be placed directly in frontend code or committed to a public repository.</p>
<p>A safer pattern is to use environment variables:</p>
<pre><code class="language-javascript">const apiKey = process.env.API\_KEY;
</code></pre>
<p>For a client-side application, be especially careful. Environment variables used in browser code may be visible to users. A secret that must remain private should usually be used on a secure server or through a protected backend service.</p>
<p>Never use this pattern:</p>
<pre><code class="language-javascript">const apiKey = "your-real-secret-key";
</code></pre>
<p>Use a placeholder during development:</p>
<pre><code class="language-javascript">const apiKey = process.env.API\_KEY || "";
</code></pre>
<p>Then configure the real value through the appropriate secret-management system for your hosting platform.</p>
<p>Before deploying, search your project for common secret patterns such as:</p>
<pre><code class="language-plaintext">API\_KEYSECRETTOKENPASSWORDPRIVATE\_KEY
</code></pre>
<p>Finding a suspicious value doesn't always mean it's a secret, but it's worth checking.</p>
<h2 id="heading-understand-what-your-app-does">Understand What Your App Does</h2>
<p>Don't publish an application that you can't explain at a basic level.</p>
<p>You should know:</p>
<ul>
<li><p>What data the app collects</p>
</li>
<li><p>Where that data is stored</p>
</li>
<li><p>Which external services receive the data</p>
</li>
<li><p>Who can view or modify the data</p>
</li>
<li><p>How users delete their accounts or information</p>
</li>
<li><p>Which parts of the app require authentication</p>
</li>
<li><p>What happens when a request fails</p>
</li>
<li><p>What happens when a user enters unexpected input</p>
</li>
</ul>
<p>You don't need to understand every line immediately. But you should understand the major building blocks.</p>
<p>If Lovable generates code that you don't understand, ask it to explain a specific section:</p>
<pre><code class="language-plaintext">Explain how user authentication works in this project. Identify where sessions are created, how access is checked, and what could go wrong if authentication is misconfigured.
</code></pre>
<p>You can also ask:</p>
<pre><code class="language-plaintext">List all external services used by this application and explain what data each service receives.
</code></pre>
<p>Explanations are useful, but they aren't proof that the code is safe. Treat them as a map, not a magical safety certificate.</p>
<h2 id="heading-build-security-into-your-prompts">Build Security Into Your Prompts</h2>
<p>Security should be part of the original request, not an emergency patch added after someone discovers that every user can view every account.</p>
<p>Include security requirements in your prompts:</p>
<pre><code class="language-plaintext">Only authenticated users should be able to access the dashboard. Users must only be able to view and edit their own tasks. Validate all form inputs, display safe error messages, and never expose secrets in frontend code.
</code></pre>
<p>For an administrative area, you might write:</p>
<pre><code class="language-plaintext">Create an admin section that is available only to users with an admin role. Check authorization on the server for every admin action instead of relying only on hiding buttons in the interface.
</code></pre>
<p>For user-generated content:</p>
<pre><code class="language-plaintext">Allow users to submit comments, but sanitize and safely render the content to reduce cross-site scripting risks. Limit comment length and reject empty submissions.
</code></pre>
<p>Detailed prompts help the generated application start from better assumptions.</p>
<h2 id="heading-test-authentication-and-authorization-separately">Test Authentication and Authorization Separately</h2>
<p>Authentication answers the question, “Who are you?”, while authorization answers the question, “What are you allowed to do?”</p>
<p>These are different.</p>
<p>A user may be successfully logged in but still not be allowed to view another user’s private records. A responsible application checks both.</p>
<p>Test cases should include:</p>
<ol>
<li><p>A logged-out visitor tries to open a private page.</p>
</li>
<li><p>A regular user tries to open an administrator page.</p>
</li>
<li><p>A user tries to access another user's record by changing an identifier in the URL.</p>
</li>
<li><p>A user submits a request without the required session information</p>
</li>
<li><p>A user logs out and them presses the browser's back button.</p>
</li>
</ol>
<p>Don't rely only on hiding navigation links. A hidden button isn't a security system. If a user can still call a backend endpoint directly, the application may be vulnerable.</p>
<p>For example, suppose your application has a page at /admin that should only be available to administrators. You could test authentication and authorization separately like this:</p>
<h4 id="heading-authentication-test">Authentication Test</h4>
<p>Log out of the application and then try to open <code>/admin</code> directly. The application should redirect you to the login page or return an appropriate unauthorized response.</p>
<p>Then log in with a valid account and confirm that the application recognizes the authenticated session.</p>
<h4 id="heading-authorization-test">Authorization Test</h4>
<p>Log in with a normal user account that doesn't have an admin role. Then try to open <code>/admin</code> directly instead of using the navigation menu. The application should deny access.</p>
<p>Try the same test against the backend endpoint used by an admin action. Confirm that the server also rejects the request.</p>
<p>You can also test whether changing an identifier in a URL or request allows one user to access another user's information. The important part is to verify the behavior from the user's perspective and, where possible, confirm that the server is enforcing the permission rather than simply hiding parts of the interface.</p>
<h2 id="heading-validate-all-user-input">Validate All User Input</h2>
<p>Users will enter unexpected information. Sometimes this happens by accident. Sometimes it happens because users are testing the boundaries of your application. Occasionally, it happens because someone has decided that a username should be 4,000 characters long and contain seventeen emojis.</p>
<p>Validate input on the client for a better user experience and on the server for security.</p>
<p>Examples of validation include:</p>
<ul>
<li><p>Required fields</p>
</li>
<li><p>Maximum and minimum lengths</p>
</li>
<li><p>Valid email formats</p>
</li>
<li><p>Allowed file types</p>
</li>
<li><p>Maximum file sizes</p>
</li>
<li><p>Valid dates</p>
</li>
<li><p>Acceptable numeric ranges</p>
</li>
<li><p>Safe content handling</p>
</li>
</ul>
<p>A frontend check might look like this:</p>
<pre><code class="language-javascript">if (username.trim().length &lt; 3) {
     showError("Username must be at least 3 characters long.");
     return;
}                
</code></pre>
<p>But don't assume that frontend validation is enough. A user can bypass browser checks by sending requests directly to your backend.</p>
<p>The server should validate the data again before storing or processing it.</p>
<p>Server-side validation means treating everything received from the browser as untrusted input. The server should check that the submitted data has the expected type, format, length, and range before using it. It should also reject unexpected fields or values when appropriate.</p>
<p>For example, if an API accepts a username and age, the server could verify that the username is a non-empty string within the allowed length and that the age is a number within the application's acceptable range. If the request fails validation, the server should reject it rather than storing or processing the invalid data.</p>
<p>You can ask Lovable to help create these checks and generate test cases:</p>
<ul>
<li><p>Add server-side validation for every field in this form.</p>
</li>
<li><p>Reject missing, incorrectly formatted, oversized, or out-of-range values before they're stored or processed.</p>
</li>
<li><p>Then create tests for valid input, missing fields, invalid formats, boundary values, and unexpected input.</p>
</li>
</ul>
<p>AI-generated tests can be useful, but don't rely on them as the only verification. Run the tests yourself and manually try important edge cases as well. The goal is to use AI to speed up the work while keeping human judgment involved in checking whether the validation actually protects the application.</p>
<h2 id="heading-be-careful-with-generated-dependencies">Be Careful With Generated Dependencies</h2>
<p>AI-generated projects may use libraries, packages, plugins, and external services. These tools can be helpful, but each dependency adds another piece to understand and maintain.</p>
<p>Ask Lovable:</p>
<pre><code class="language-plaintext">List the main packages used in this project and explain why each one is needed.
</code></pre>
<p>You can also ask:</p>
<pre><code class="language-plaintext">Identify dependencies that are unnecessary for the current features and suggest a simpler alternative.
</code></pre>
<p>Fewer dependencies can mean:</p>
<ul>
<li><p>Less code to maintain</p>
</li>
<li><p>Fewer security updates</p>
</li>
<li><p>Smaller application size</p>
</li>
<li><p>Fewer compatibility problems</p>
</li>
<li><p>Easier debugging</p>
</li>
</ul>
<p>You don't need to remove every package. Just avoid collecting dependencies like digital souvenirs.</p>
<h2 id="heading-design-for-accessibility">Design for Accessibility</h2>
<p>An application isn't truly successful if many people can't use it.</p>
<p>Ask Lovable to include accessibility from the beginning:</p>
<pre><code class="language-plaintext">Make the interface accessible. Use semantic HTML, keyboard navigation, visible focus states, descriptive labels, sufficient color contrast, and accessible error messages.
</code></pre>
<p>Check whether:</p>
<ul>
<li><p>Buttons have clear names</p>
</li>
<li><p>Form inputs have labels</p>
</li>
<li><p>Keyboard users can reach every interactive element</p>
</li>
<li><p>Focus indicators are visible</p>
</li>
<li><p>Text has enough contrast</p>
</li>
<li><p>Images have useful alternative text</p>
</li>
<li><p>Error messages explain how to fix a problem</p>
</li>
<li><p>The layout works at different screen sizes</p>
</li>
<li><p>Content remains usable when text is enlarged</p>
</li>
</ul>
<p>Avoid using color as the only way to communicate meaning. For example, don't show errors only with a red border. Add text such as:</p>
<pre><code class="language-plaintext">Email address is required.
</code></pre>
<p>Accessibility isn't just a compliance task. It usually makes the application easier for everyone to use.</p>
<h2 id="heading-avoid-dark-patterns">Avoid Dark Patterns</h2>
<p>A responsible app should help users make informed choices. It shouldn't trick them into doing something they didn't intend.</p>
<p>Avoid:</p>
<ul>
<li><p>Preselected marketing consent</p>
</li>
<li><p>Hidden cancellation links</p>
</li>
<li><p>Confusing double negatives</p>
</li>
<li><p>Misleading buttons</p>
</li>
<li><p>Fake countdown timers</p>
</li>
<li><p>Notifications that look like system warnings</p>
</li>
<li><p>Subscriptions that are easy to start but difficult to stop</p>
</li>
<li><p>Important information hidden in tiny text</p>
</li>
</ul>
<p>Use clear labels:</p>
<pre><code class="language-plaintext">Delete account
</code></pre>
<p>is better than:</p>
<pre><code class="language-plaintext">Continue
</code></pre>
<p>when the action permanently deletes an account.</p>
<p>For destructive actions, provide a confirmation step that clearly explains what will happen:</p>
<pre><code class="language-plaintext">This will permanently delete your account and all saved tasks. This action cannot be undone.
</code></pre>
<p>Good design respects the user’s ability to choose.</p>
<h2 id="heading-handle-errors-effectively">Handle Errors Effectively</h2>
<p>Every application experiences errors. Networks fail. Services go offline. Users close tabs at inconvenient moments. Servers occasionally decide to take an unscheduled vacation.</p>
<p>Don't display vague or misleading messages such as:</p>
<pre><code class="language-plaintext">Something went wrong.
</code></pre>
<p>when you can provide useful guidance.</p>
<p>Better:</p>
<pre><code class="language-plaintext">We could not save your task because the connection was interrupted. Check your internet connection and try again.
</code></pre>
<p>For developers, log enough information to investigate the problem without exposing sensitive data:</p>
<pre><code class="language-javascript">try {  await saveTask(task);} catch (error) {  console.error("Task save failed", {    operation: "create\_task",    message: error.message  });
  showError("Your task could not be saved. Please try again.");}
</code></pre>
<p>Avoid sending passwords, tokens, private messages, or personal records into logs.</p>
<h2 id="heading-be-honest-about-ai-generated-features">Be Honest About AI-Generated Features</h2>
<p>If your application uses AI to generate text, recommendations, summaries, images, or decisions, users should understand that the output may be wrong.</p>
<p>Use clear language:</p>
<pre><code class="language-plaintext">This summary was generated automatically and may contain mistakes. Review it before sharing.
</code></pre>
<p>Avoid presenting AI-generated information as guaranteed fact, especially in areas such as:</p>
<ul>
<li><p>Health</p>
</li>
<li><p>Finance</p>
</li>
<li><p>Education</p>
</li>
<li><p>Employment</p>
</li>
<li><p>Legal information</p>
</li>
<li><p>Safety</p>
</li>
<li><p>Personal identity</p>
</li>
<li><p>News and public information</p>
</li>
</ul>
<p>Give users ways to correct, reject, or report problematic output. If an AI feature affects important decisions, provide human review whenever possible.</p>
<h2 id="heading-protect-personal-data">Protect Personal Data</h2>
<p>Collect only the information your app actually needs.</p>
<p>If a task manager only needs an email address for account recovery, it probably doesn't need a user’s home address, phone number, favorite color, and childhood nickname.</p>
<p>Before adding a data field, ask:</p>
<pre><code class="language-plaintext">Why do we need this information?
</code></pre>
<p>Then ask:</p>
<pre><code class="language-plaintext">What could happen if this information were exposed?
</code></pre>
<p>Good data practices include:</p>
<ul>
<li><p>Collecting less information</p>
</li>
<li><p>Explaining why information is needed</p>
</li>
<li><p>Restricting access</p>
</li>
<li><p>Deleting information when it's no longer necessary</p>
</li>
<li><p>Avoiding unnecessary analytics</p>
</li>
<li><p>Protecting data during transmission and storage</p>
</li>
<li><p>Giving users meaningful control over their information</p>
</li>
</ul>
<p>Data isn't free just because a form field is free to add.</p>
<h2 id="heading-respect-copyright-and-ownership">Respect Copyright and Ownership</h2>
<p>Don't ask Lovable to copy an existing product exactly, reproduce copyrighted artwork, or imitate a brand in a way that could confuse users.</p>
<p>Instead, describe the qualities you want:</p>
<pre><code class="language-plaintext">Create a clean project-management interface with a left sidebar, clear status labels, and a spacious layout. Use original styling and avoid copying any specific company's branding.
</code></pre>
<p>Be careful with:</p>
<ul>
<li><p>Images</p>
</li>
<li><p>Logos</p>
</li>
<li><p>Icons</p>
</li>
<li><p>Fonts</p>
</li>
<li><p>Code snippets</p>
</li>
<li><p>Written content</p>
</li>
<li><p>Product names</p>
</li>
<li><p>Brand colors</p>
</li>
<li><p>User-generated material</p>
</li>
</ul>
<p>Use assets that you created, licensed, or are allowed to use. When in doubt, choose an original design.</p>
<h2 id="heading-test-with-realistic-but-fake-data">Test With Realistic but Fake Data</h2>
<p>Use fictional data during development:</p>
<pre><code class="language-plaintext">Name: Jordan 
ExampleEmail: jordan@example.test
testOrder ID: TEST-1001
</code></pre>
<p>Don't use real customer records just because they're convenient.</p>
<p>Create test cases for:</p>
<ul>
<li><p>Empty states</p>
</li>
<li><p>Long names</p>
</li>
<li><p>Very long text</p>
</li>
<li><p>Invalid email addresses</p>
</li>
<li><p>Duplicate records</p>
</li>
<li><p>Missing images</p>
</li>
<li><p>Slow connections</p>
</li>
<li><p>Failed requests</p>
</li>
<li><p>Expired sessions</p>
</li>
<li><p>Multiple users</p>
</li>
<li><p>Different screen sizes</p>
</li>
<li><p>Keyboard-only navigation</p>
</li>
</ul>
<p>Fake data helps you test realistic behavior without exposing real people’s information.</p>
<p>You can create fake data yourself by using clearly fictional names, addresses, email addresses, identifiers, and other values that can't be mistaken for real customer information. For larger datasets, you can also <a href="https://www.freecodecamp.org/news/how-to-fine-tune-easyocr-with-a-synthetic-dataset/#heading-how-to-generate-your-synthetic-dataset">use a reputable fake-data generator</a> or ask Lovable to create a dataset specifically for testing.</p>
<p>For example, you could ask:</p>
<pre><code class="language-plaintext">Create 100 fictional user records for testing. Use clearly fake names and email addresses under `example.test`. Include different account types, missing optional fields, long names, and other edge cases. Do not use real people's information.
</code></pre>
<p>Review generated data before using it, especially if you obtain it from an external source. Avoid datasets containing real personal information unless you have a legitimate reason, appropriate authorization, and proper safeguards. When possible, use synthetic data designed specifically for testing so that realistic application behavior can be tested without exposing real people's information.</p>
<h2 id="heading-test-before-you-share-the-app">Test Before You Share the App</h2>
<p>Before showing your project to others, follow a basic release checklist.</p>
<pre><code class="language-plaintext">1. The app works on mobile and desktop screens.
2. Forms validate input correctly.
3. Authentication behaves as expected.
4. Users can't access data belonging to other users.
5. Secrets aren't included in frontend code.
6. Error messages are clear and safe.
7. Keyboard navigation works.
8. Important buttons have clear labels.
9. Empty states are understandable.
10. Loading states are visible.
11. Destructive actions require confirmation.
12. Test data doesn't contain real personal information.
13. External services are configured correctly.
14. The production environment uses secure settings.
15. The app has been tested after the final changes.
</code></pre>
<p>A checklist may feel less exciting than clicking a shiny “Publish” button, but it's much more exciting than explaining to users why the app deleted everything.</p>
<h2 id="heading-ask-lovable-to-review-its-own-work">Ask Lovable to Review Its Own Work</h2>
<p>AI tools can help with review tasks when given specific instructions.</p>
<p>Try prompts such as:</p>
<pre><code class="language-plaintext">Review this application for authentication and authorization problems. Identify any route, database query, or API endpoint that may expose data to the wrong user.
</code></pre>
<pre><code class="language-plaintext">Review the forms for missing validation, unclear error messages, and accessibility problems.
</code></pre>
<pre><code class="language-plaintext">Review the project for hardcoded secrets, unsafe logging, and sensitive information that might appear in the browser.
</code></pre>
<pre><code class="language-plaintext">Review the application for mobile layout problems and explain the changes you recommend.
</code></pre>
<p>Don't accept the review blindly. Compare the suggestions with your own testing and, for serious applications, get help from an experienced developer or security professional.</p>
<h2 id="heading-learn-from-the-generated-code">Learn From the Generated Code</h2>
<p>Using Lovable responsibly doesn't mean avoiding AI-generated code. It means using the tool as an opportunity to learn.</p>
<p>When you receive a result, ask:</p>
<pre><code class="language-plaintext">Explain this function in beginner-friendly language.
</code></pre>
<pre><code class="language-plaintext">Show me a simpler version of this code.
</code></pre>
<pre><code class="language-plaintext">What assumptions does this implementation make?
</code></pre>
<pre><code class="language-plaintext">What are the possible failure cases?
</code></pre>
<pre><code class="language-plaintext">How would this code behave with two users at the same time?
</code></pre>
<p>Try changing one small part manually. Read the error messages. Compare the before-and-after versions. Over time, the generated code will become less mysterious.</p>
<p>The goal isn't to memorize every programming concept immediately. The goal is to become confident enough to ask better questions and recognize risky answers.</p>
<h2 id="heading-use-lovable-for-prototyping-without-pretending-its-production-ready">Use Lovable for Prototyping Without Pretending It's Production-Ready</h2>
<p>Lovable is excellent for exploring ideas quickly.</p>
<p>You can use it to:</p>
<ul>
<li><p>Test a product concept</p>
</li>
<li><p>Build a portfolio project</p>
</li>
<li><p>Create a prototype for user feedback</p>
</li>
<li><p>Learn how web applications are structured</p>
</li>
<li><p>Experiment with interfaces</p>
</li>
<li><p>Build an internal tool</p>
</li>
<li><p>Turn a rough idea into something people can react to</p>
</li>
</ul>
<p>A prototype may not have the same security, reliability, monitoring, documentation, and scalability requirements as a public production application.</p>
<p>Be honest about the stage of your project. Use labels such as "Prototype", "Demo", "Work in Progress", and so on.</p>
<p>Don't treat a prototype like a finished product simply because it has a nice gradient and a button that says “Launch.”</p>
<h2 id="heading-create-a-simple-responsible-development-workflow">Create a Simple Responsible Development Workflow</h2>
<p>A practical workflow might look like this:</p>
<ol>
<li><p>Define the problem</p>
</li>
<li><p>Identify the users</p>
</li>
<li><p>Decide what information the app needs</p>
</li>
<li><p>Write a clear prompt</p>
</li>
<li><p>Generate a small feature</p>
</li>
<li><p>Review the result</p>
</li>
<li><p>Test normal and unexpected behavior</p>
</li>
<li><p>Fix security and accessibility problems</p>
</li>
<li><p>Repeat for the next feature</p>
</li>
<li><p>Test the complete app</p>
</li>
<li><p>Remove test data and secrets</p>
</li>
<li><p>Document important decisions</p>
</li>
<li><p>Deploy only when the app is ready for its intended audience</p>
</li>
</ol>
<p>This process isn't slow. It's controlled. The fastest path is often the one that avoids rebuilding the entire application after discovering that the foundation was made of optimism and unvalidated form fields.</p>
<h2 id="heading-a-responsible-prompt-template">A Responsible Prompt Template</h2>
<p>You can use this template when asking Lovable to create a feature:</p>
<pre><code class="language-text">Build [feature] for [type of user].

The goal is to [explain the problem being solved].

Users should be able to:
- [action one]
- [action two]
- [action three]

The application should:
- Validate all user input.
- Protect authenticated routes.
- Ensure users can access only data they are authorized to access.
- Avoid hardcoded secrets.
- Use clear loading and error states.
- Support keyboard navigation.
- Work on mobile and desktop screens.
- Use accessible labels and sufficient color contrast.

Do not:
- Collect unnecessary personal information.
- Expose private data.
- Add unrelated features.
- Change existing authentication behavior without explaining the change.

After building the feature, explain:
- Which files changed.
- What data is stored.
- Which external services are used.
- What security risks remain.
- How I should test the feature.
</code></pre>
<p>This template encourages Lovable to think about more than appearance.</p>
<h2 id="heading-the-golden-rule-of-ai-app-building">The Golden Rule of AI App Building</h2>
<p>If an AI-generated feature affects another person, review it as if you will be the person affected.</p>
<p>Would you want your data stored there?</p>
<p>Would you understand what the app is doing?</p>
<p>Would you be able to correct a mistake?</p>
<p>Would you know how to delete your information?</p>
<p>Would you feel comfortable using the application on a phone, with a keyboard, or with a slow internet connection?</p>
<p>Would you trust the app if you knew how it was built?</p>
<p>These questions turn responsible development from an abstract idea into a practical habit.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Lovable can make app development more approachable, faster, and more fun. It can help beginners build their first projects and help experienced developers explore ideas without spending hours creating every screen from scratch.</p>
<p>But responsible use requires more than generating attractive interfaces. Write clear prompts. Protect secrets. Collect less data. Validate inputs. Test permissions. Design for accessibility. Respect ownership. Explain AI-generated features. Review the code. Keep people involved in important decisions.</p>
<p>The best AI-built applications aren't the ones created with the fewest clicks. They're the ones built with curiosity, care, and enough testing to survive contact with real users.</p>
<p>Use Lovable to move faster, but use your judgment to decide where you're going.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
