firecrawl/open-lovable
Next.js prompt-to-app builder that clones sites via Firecrawl into a React/Vite app previewed in a cloud sandbox.
Overview
Open Lovable is Firecrawl’s open-source prompt-to-app builder, a small Lovable clone. You paste a URL or describe a page. It scrapes the site with the Firecrawl API, asks one LLM to write a React + Tailwind app, writes the files into a remote Vite project in an E2B or Vercel sandbox, and shows that sandbox’s dev server in an iframe. Follow-up chat messages edit the same app. It is not a terminal or IDE coding agent. There is no local repository, no git, and no tool-calling loop. The output is a throwaway preview plus a ZIP download.
Architecturally it is the simplest of the app builders reviewed here. Every generation is one streamed completion. The model emits <file path="…"> blocks (and optionally <package> and <command> tags). Server routes parse those tags with regexes and push the files into the sandbox. Edits add a pre-step: a structured-output call produces a search plan, a local grep turns that into target files, and those files go into the prompt. Nothing checks the result and feeds errors back to the model automatically.
The app is a single Next.js 15 project. All session state (current sandbox, file cache, conversation) lives in Node global variables on the server, so one running instance serves effectively one user at a time.
Architecture
flowchart LR
UI["app/generation/page.tsx"] -->|"clone mode"| SCR["/api/scrape-url-enhanced"]
SCR --> FC["Firecrawl API"]
UI --> SB["/api/create-ai-sandbox-v2"]
SB --> FAC["SandboxFactory: E2B or Vercel"]
UI -->|"prompt + isEdit"| GEN["/api/generate-ai-code-stream"]
GEN -->|"edits only"| INT["/api/analyze-edit-intent (generateObject)"]
INT --> GREP["executeSearchPlan (local grep)"]
GEN --> LLM["streamText via provider-manager"]
UI -->|"generated text"| APP["/api/apply-ai-code-stream"]
APP --> PKG["/api/install-packages"]
APP --> FAC
APP -->|"optional"| MORPH["Morph Fast Apply"]
FAC --> VITE["Vite dev server in sandbox"]
VITE --> UI
| Component | Path | Role |
|---|---|---|
| Builder UI | app/generation/page.tsx |
~4,000-line client page: chat, scrape/clone flow, code view, preview iframe |
| Code generation | app/api/generate-ai-code-stream/route.ts |
Builds the giant system prompt, selects edit context, streams the LLM output as SSE |
| Edit planner | app/api/analyze-edit-intent/route.ts |
generateObject with a Zod search-plan schema |
| Search executor | lib/file-search-executor.ts |
Runs the plan’s terms and regexes over cached file contents |
| Context selector | lib/context-selector.ts, lib/edit-intent-analyzer.ts |
Regex fallback for picking files; formats primary and context files |
| Apply | app/api/apply-ai-code-stream/route.ts |
Parses <file>/<package>/<command>, installs, writes, runs commands |
| Fast apply | lib/morph-fast-apply.ts |
Optional <edit> blocks merged by Morph’s morph-v3-large |
| Sandboxes | lib/sandbox/ |
SandboxProvider abstraction, E2BProvider, VercelProvider, sandboxManager |
| Model routing | lib/ai/provider-manager.ts |
Maps model ids to OpenAI, Anthropic, Google or Groq clients, or to Vercel AI Gateway |
| Settings | config/app.config.ts |
Model list, sandbox timeouts, token limits, feature flags |
How a request flows
Take “clone example.com” followed by “make the header black”:
- Sandbox.
createSandboxposts to/api/create-ai-sandbox-v2(page.tsx). The route first terminates every existing sandbox, then callsSandboxFactory.create()andsetupViteApp()and stores the provider inglobal.activeSandboxProvider(route.ts). The provider isSANDBOX_PROVIDER, defaulting toe2b(factory.ts). - Scrape. In clone mode the page calls
/api/scrape-url-enhanced(page.tsx). That route posts to Firecrawl’s/v1/scrapefor markdown, HTML and a screenshot (route.ts). The scraped content is folded into the generation prompt. - Generate.
/api/generate-ai-code-streampushes the message intoglobal.conversationState, capping history at 20 messages and 10 edits (route.ts). It assembles a very long system prompt and callsstreamTextwithmaxTokens: 8192and up to two retries on rate-limit or timeout errors (route.ts). Text is relayed to the browser as SSE. - Edit context (second turn). Because
appliedCodeis non-empty, the page sendsisEdit: true(page.tsx). The route calls/api/analyze-edit-intent, which usesgenerateObjectto returneditType,searchTermsandregexPatterns(route.ts). It then runsexecuteSearchPlanover the cached files (route.ts). If that fails it falls back to the regex classifier inselectFilesForEdit. - Apply. When streaming ends, the client posts the full text to
/api/apply-ai-code-stream(page.tsx).parseAIResponseextracts files,<package>tags and imports. The route streams an install through/api/install-packages, drops any generatedpackage.json/vite.config.js/Tailwind config (route.ts), writes each file withproviderInstance.writeFile(route.ts), then runs any<command>lines in the sandbox. - Preview. Vite’s hot reload in the sandbox picks up the files, and the iframe shows the provider’s public URL. If something breaks, the user sees the Vite overlay and has to describe the error in chat.
Key components
The prompt is the program
Most of generate-ai-code-stream/route.ts (about 1,900 lines) is prompt text and string assembly. It holds rules about Tailwind, full-file output, never truncating, which files to touch in edit mode, examples, and “recent edits” and user-preference summaries rebuilt from the global conversation state. There is no tool calling. A comment says the providers in use do not support it here, so packages are signalled with tags instead (route.ts).
Edit targeting
The two-stage plan-then-grep design is the most interesting idea in the repo. The model is told not to guess files, only to propose exact strings and patterns. A deterministic search then picks the files. formatFilesForAI sends primary files in full with “do not truncate” warnings and context files cut to 2,000 characters (context-selector.ts).
Morph Fast Apply
With MORPH_API_KEY set and isEdit true, the prompt switches to <edit target_file> blocks with an <update> snippet. The apply route reads the original file from the sandbox and sends instruction, code and snippet to morph-v3-large, then writes the merged result back (morph-fast-apply.ts). It is the only path that avoids regenerating whole files.
Sandboxes
SandboxProvider is an abstract class with createSandbox, runCommand, writeFile, readFile, listFiles, installPackages, setupViteApp and restartViteServer (types.ts). The E2B provider writes a Vite 4 + React scaffold through an inline Python script and starts vite --host on port 5173. The Vercel provider does the same under /vercel/sandbox. Sandboxes time out after 30 minutes (E2B) or 15 minutes (Vercel) by default (app.config.ts).
Models
getProviderForModel routes by prefix: anthropic/, openai/ and google/ go to their SDKs, the Kimi K2 id goes to Groq, and anything else also goes to Groq. If AI_GATEWAY_API_KEY is set, every client points at Vercel AI Gateway instead (provider-manager.ts). The picker offers GPT-5, Kimi K2, Sonnet 4 and Gemini 3 Pro (the default) (app.config.ts).
Extending it
- Another sandbox. Subclass
SandboxProviderand add acaseinSandboxFactory.create. - Another model. Add it to
availableModels, plusmodelApiConfigif it needs a non-default provider, base URL or key. - Behaviour. Edit the prompt strings in
generate-ai-code-stream/route.tsand the knobs inconfig/app.config.ts. There is no plugin, MCP, hook or instruction-file mechanism, and no API meant for headless use.
Running it
Run pnpm install (or npm/bun), create .env.local with FIRECRAWL_API_KEY, at least one model key (GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, or AI_GATEWAY_API_KEY) and sandbox credentials (E2B_API_KEY, or Vercel OIDC/PAT with SANDBOX_PROVIDER=vercel), then run pnpm dev. Several routes call back into the app over HTTP using NEXT_PUBLIC_APP_URL (default http://localhost:3000), so set it when you deploy behind another host. All code execution happens in the paid remote sandbox.
Strengths and caveats
- Strength: easy to read. One prompt, one stream, one regex parser and one write loop. If you want to see the minimum moving parts of a Lovable-style builder, this is it.
- Strength: smart edit targeting. Plan-then-grep avoids sending the whole project for small edits, and Morph turns those edits into snippet merges.
- Strength: website cloning. The Firecrawl scrape (markdown + screenshot) and brand-style extraction are what set it apart from generic builders.
- Caveat: single-tenant by construction. State lives in
global.*, and creating a sandbox kills all others. Two users on one deployment will collide. - Caveat: no feedback loop.
lib/build-validator.tsexists but nothing calls it. Truncation repair is gated byenableTruncationRecovery, which isfalseby default (route.ts). Errors reach the model only if the user pastes them. - Caveat: no history. Files are overwritten in place, with no git, checkpoints or undo. Generated
package.jsonand config files are silently discarded. - Caveat: docs drift. The README calls Vercel the default sandbox, but the factory falls back to E2B. The last commit at this pin is from November 2025.
Sources: code at 69bd93b, verified Q&A.
How it answers the Open-source coding agents questions
Each answer was drafted by a code-reading agent at commit 69bd93b. Its citations were checked mechanically. Compare with the other open-source coding agents →
How is the agent loop implemented?
answeredThere is no iterative agent loop with tool-calling; it is a linear single-shot pipeline per user request. The flow: (1) user prompt hits /api/generate-ai-code-stream (app/api/generate-ai-code-stream/route.ts:91), (2) for edits, it first calls /api/analyze-edit-intent which uses Vercel AI SDK's generateObject() with a Zod schema to produce a structured search plan, (3) the search plan is executed locally by executeSearchPlan() in lib/file-search-executor.ts:42 which greps file contents line-by-line, (4) a single LLM completion is called via streamText() from the Vercel AI SDK (lib/ai/provider-manager.ts wraps the providers), (5) the text stream is parsed in real-time for XML tags (<file>, <package>, <command>) in generate-ai-code-stream/route.ts:1397-1504, and (6) parsed files are sent to /api/apply-ai-code-stream (app/api/apply-ai-code-stream/route.ts:264) which writes them to the sandbox. The system does NOT use tool/function calling — the code explicitly states: "Neither Groq nor Anthropic models support tool/function calling in this context" (route.ts:1310-1311). Instead it relies on XML tags in text output. Stop conditions are simple: up to 2 retries on rate-limit/service errors with exponential backoff (route.ts:1329-1382), and truncation recovery that fires focused API calls to complete files missing closing braces or tags (route.ts:1658-1810). No sub-agents, no multi-turn planning loop.
How is repository context gathered and kept within the context window?
answeredRepository context is gathered through a FileManifest system (types/file-manifest.ts:44-51) that records every file's full content, exports, imports, component dependencies, and last-modified time. For edit requests, context flows through a multi-stage pipeline. First, analyzeEditIntent() in lib/edit-intent-analyzer.ts:6 uses regex pattern-matching against the user prompt to classify the edit type and resolve target files. A more advanced path calls the /api/analyze-edit-intent endpoint (app/api/analyze-edit-intent/route.ts:126-168) which uses generateObject() with a Zod schema to let the AI produce a search plan with search terms, regex patterns, and file-type filters. Then executeSearchPlan() (lib/file-search-executor.ts:59-133) performs line-by-line grep over file contents. Finally formatFilesForAI() (lib/context-selector.ts:308-347) builds the system prompt: primary files shown in full, context files truncated to 2000 chars. The conversation state (types/conversation.ts:45-49) is tracked in global variables and includes message history, edit records, and project evolution. Context window management is aggressive: messages are capped at 20 (trimmed to 15), edits at 10 (trimmed to 8), conversation context truncated to 2000 chars (generate-ai-code-stream/route.ts:130-575). There are no embeddings, no vector search, and no summarisation or compaction of long sessions beyond simple splicing. Web-scraped content (from the Firecrawl API) is included as-is with a 1000-char preview.
How are code edits applied?
answeredCode edits are applied by parsing the LLM's text output for XML tags. The primary format is <file path="...">full file content</file> (app/api/apply-ai-code-stream/route.ts:70-74) — the LLM is instructed to output COMPLETE files, never diffs. Two additional tag types exist: <package>name</package> signals packages to install, and <command>...</command> commands to run. The parser also handles markdown code blocks (route.ts:130-131). A secondary edit format is the Morph Fast Apply block: <edit target_file="..."><instructions>...</instructions><update>snippet</update></edit> (lib/morph-fast-apply.ts:60-76). When MORPH_API_KEY is set, these blocks are sent to the morph-v3-large model via an OpenAI-compatible API to merge the update snippet into the existing file read from the sandbox (morph-fast-apply.ts:180-217). Validation is handled by build-validator.ts:12-80 which fetches the sandbox URL after writing, checks for a default template page, and looks for Vite error overlays. Truncation detection (unmatched braces, unclosed XML tags) can trigger automatic recovery API calls (generate-ai-code-stream/route.ts:1658-1810), though enableTruncationRecovery defaults to false. File-writing uses the sandbox provider's writeFile() method with a fallback to shell echo or Python open() (vercel-provider.ts:124-189, e2b-provider.ts:108-137). There is no git integration, no undo mechanism, and no diff-based edit format.
How are shell commands and file writes kept safe?
answeredAll code execution and file writes happen inside sandboxed cloud environments, never on the host machine. Two sandbox providers are available: Vercel Sandbox (@vercel/sandbox) and E2B (@e2b/code-interpreter), selected via the SANDBOX_PROVIDER env var (lib/sandbox/factory.ts:6-21). The abstract SandboxProvider class (lib/sandbox/types.ts:35-65) defines the interface. The SandboxManager (lib/sandbox/sandbox-manager.ts) manages lifecycle: creating sandboxes, reconnecting to existing ones (E2B only), and cleanup after 1 hour of inactivity. There is no user approval prompt for any command — the system writes files and runs commands against the sandbox without confirmation. There are no command allow/deny lists, no network restriction configuration, and no checkpoint/rollback mechanism. For package installation, npm install is run directly in the sandbox (vercel-provider.ts:266-323, e2b-provider.ts:185-229). File paths are normalized by prepending src/ when they don't match config-file patterns about config files (apply-ai-code-stream/route.ts:529,603-612). The build-validator.ts checks whether the sandbox serves working content after edits, but it's a read-only health check with no recovery beyond retrying. The safety model is entirely outsourced to the sandbox provider's own isolation — the host Next.js app has no sandboxing (no Landlock, no seccomp, no containerization) and global variables hold all mutable state.
Which models are supported and how are they called?
answeredFour providers are supported via the Vercel AI SDK (@ai-sdk/*): Anthropic, OpenAI, Google Generative AI, and Groq. All are wrapped in lib/ai/provider-manager.ts. The getProviderForModel() function (provider-manager.ts:79-117) routes based on model name prefixes: anthropic/ → Anthropic SDK, openai/ → OpenAI SDK, google/ → Google Gemini SDK, and moonshotai/kimi-k2-instruct-0905 → Groq SDK. Without a prefix it defaults to Groq. Models listed in config/app.config.ts:56-62: openai/gpt-5, moonshotai/kimi-k2-instruct-0905, anthropic/claude-sonnet-4-20250514, google/gemini-3-pro-preview (the default). Custom model routing can be configured via modelApiConfig in the app config (provider-manager.ts:81-86). Vercel AI Gateway is supported as a unified proxy — all requests go to https://ai-gateway.vercel.sh/v1 when AI_GATEWAY_API_KEY is set (provider-manager.ts:21-23). Tool/function calling is not used — the code at generate-ai-code-stream/route.ts:1310-1311 explicitly notes no support in the available models. Instead, XML tags in text output are parsed. generateObject() (Vercel AI SDK) is used solely for the edit-intent search-plan endpoint with Zod schemas (analyze-edit-intent/route.ts:34-60,126). Temperature defaults to 0.7 for non-reasoning models and is unset for GPT-5. Max tokens is 8192. GPT-5 uses reasoningEffort: 'high' (route.ts:1320-1326). No local models are supported. No cost tracking or per-model prompt tuning exists — the same massive system prompt template is used for all models.
How can it be extended and customised?
answeredExtensibility is limited — there are no MCP servers, no plugin system, no custom tool API, no webhook system, and no headless/SDK mode. The primary extension mechanism is the Sandbox provider abstraction: the SandboxProvider abstract class (lib/sandbox/types.ts:35-65) defines a contract (createSandbox, runCommand, writeFile, readFile, etc.) and SandboxFactory (lib/sandbox/factory.ts:6-21) registers new providers by adding a case. This is how E2B and Vercel are swapped. Environment variable configuration (.env.example) lets users change AI providers, sandbox backends, API keys, and internal endpoints without code changes. config/app.config.ts:1-184 centralizes all tunable parameters: model lists, timeouts, UI toggles, file exclusion patterns, etc. Morph Fast Apply (lib/morph-fast-apply.ts) is an optional external integration enabled via MORPH_API_KEY, adding a surgical edit capability. .cursor/rules/ — four Markdown files in .cursor/rules/ directories provide IDE-level guidance for components, effects, and styles but are Cursor-specific and not consumed by the app at runtime. There are no instruction files like AGENTS.md or CLAUDE.md, no hooks system for pre/post-apply actions, no API that external tools can call to drive the agent programmatically. The architecture is a monolith with global variables for all mutable state (declare global var sandboxState, conversationState, existingFiles, activeSandbox across multiple route files).