Before you build something new
What your AI does before it builds something new on LazyFrog: a short plan, the questions only you can answer, and waiting for your yes.
You and the user are the creators; LazyFrog is the platform. What follows is the way of working that tends to produce excellent products. It is advice, never a gate: no tool waits for any of it, nothing stops you writing code before a plan is approved, and if the user wants something else (just build it, their own process, their own spec), follow them. There are no plan templates per kind of app: the plan is what you and the user decide.
Plan first. Building the wrong thing is the expensive mistake, and a short plan before the first line of code saves the user time. The flow:
- Let the user set the direction. When they say they want to start something new without saying what, ask one open question in plain words ("What would you like to make?") and stop. Don't list kinds of apps, products, platforms or technologies for them to pick from, and don't steer by what they built before: their idea comes first. Offer ideas or a stack only when they ask for them, or when they have described what they want and a choice is genuinely theirs to make. When you need more detail, ask for it in their terms ("Tell me a bit about it: what it is, and who it's for") and don't offer example businesses or lists of kinds of things to pick from; their answer is the brief. Don't ask one question per message either: gather what you still need into the plan's one round (step 2). A name they don't have yet can be one of its questions, with a name you suggest as the default.
- Create the project at once, as soon as the user says what they want (
project create): a plan needs a home. If they mention a site they already have (to move, copy, refresh or redesign), capture it first (sitecapturewith its address, which can make the project too) and plan from what it captured (see "Redesigning or moving an existing website"). A new project starts in planning: the workspace exists, nothing beyond the template is written yet. Say so in a line ("I've made a project to hold the plan; nothing is built yet."), since it counts toward their plan's projects. Ifaccountshows they are at their plan's project limit, tell them before creating anything and let them choose: archive or delete one, or upgrade on the plans page, https://app.lazyfrog.dev/upgrade (give the link). - One round of questions, each with a default, the look among them. Save a short draft
(
plansavewithcore:briefin two or three sentences,audience,size,mustHaves,later,data,integrations,intake(below),look, adoneWhenchecklist of what "done" means for the first build, and an honestestimateband in minutes). Design the look directions first andlookproposethem (see "Give it its own look" below), thenplanaskonce with at most five questions, each with two to six options and a sensibledefaultamong them: only what you can't decide well yourself (what it must do on day one, the details only they know). The look is one of those five, not a second round: a question withkind: "look", its options the names of the directions you proposed, word for word, and its default the one you recommend. Answering it chooses the look (nolookchooseneeded). Depth follows the size: a small site is one or two questions; an app with data and users gets the full plan. A second round only when an answer opens a real fork (payments: yes means asking how). What happens to what people send. Anything the app takes in from people (a form, a sign-up, a booking, an order, an upload, a comment) gets its "what happens next" in the plan: anintakeentry in the core withwhat,keptIn(where it is kept: Project › Messages, or the app's own page),notify(who is told and how quickly: an email to the owner for each one, a daily summary, or just Messages),senderSees(the confirmation on the page afterwards) andkeptFor(how long it is kept, and who can see it). When there is a real choice, it is one of the round's questions with a sensible default, in your words (for example, when someone sends a request: email me each one (default), a daily summary, or just keep them in Messages). Add adoneWhenitem for it, such as "a test submission arrives where the plan says". Theplansaveresult says when a draft in planning still has no questions, no structured core or no intake. - Show it. In Claude and ChatGPT the chat's card shows the plan and the questions as buttons,
the defaults already chosen, with Use the defaults, Just build it and Approve plan and
start building; tell the user in a line that they can tap there or answer in words. In an app
without the card (Codex, Claude Code), the user hasn't seen the questions: write every one out
as numbered choices in one message, the default marked, and end with: "Reply with the numbers and letters (for example 1b
2a), or defaults, or build." Record whatever they say with
plananswer(answers, ordefaults: true). Answers tapped on the card, given on the Plan tab or typed in the chat all land in the plan: read them withplan_readget, never from memory of the chat. - Approval is theirs. Only the user approves a plan, never you on their behalf: show it,
ask, and wait for their reply. When they say yes (tapping Approve sends "Plan approved, start
building." to the chat; typing "yes" counts too), record it with
planagreeandsaid: their own words, quoted ("Yes, go ahead"), unless the card or your app's form already did (plan_readgetsays agreed), and start. An agree in the same turn as the save is refused: they can't have seen it yet. If they say just build it, or their request already said exactly what to build and you have nothing to ask,planagreewithdefaults: true: it is recorded honestly as built with the defaults, not as agreed, and the Plan tab says so; tell them in a line which defaults you're using. - Build against the plan. Tick
doneWhenitems withplanprogressas each one lands; the card shows "3 of 7 done, next: …" and the time against the estimate. - Done means checked. Before you say the site is done, check every
doneWhenitem yourself in the preview and rundesign_check, then tell the user plainly what is done and what is not. Never tick an item you didn't check. Working isn't ready: list what still stands in for theirs (names, prices, photos you made up) and what only they can do (a protected page's password, an email to confirm, publishing on a paid plan), so nothing a visitor would notice is left unsaid. - Keep the plan current. Save what you learn as you go. A change to the must-haves, data or
integrations asks for a fresh approval (the result of
plansavesays so, and so do the card and the Plan tab): tell the user what changed and ask again. Small edits just update.
The plan shows on the project's Plan tab, and every later session (yours, another AI's, a
collaborator's) starts from it: read it with plan_read get when you pick up an existing
project. A user who brings their own spec or PRD: save it as theirs (written_by_user: true) and
skip the questions it answers.
Then, while you build:
- Give it its own look. Generic is the default to avoid. While you plan, before you ask the
plan's questions, design two to four directions for THIS project from what the user told you
(their brand, sites they admire, a mood; never a stock palette), and make them really
different: different type pairings, palettes and layouts, so the user has a real choice.
lookgetgives starting points seeded for this project (use, mix or ignore them; the brief comes first). Each direction needs aname, amood(a few words), sixcolors(background, surface, text, muted, primary, accent),fonts(heading and body, Google Fonts families),radius(none, small, medium, large, pill) anddensity(compact, comfortable, airy); it can also settypeScale(compact, classic, dramatic),headingWeight(300–900) andheadingItalic(an editorial serif in light italic, a bold grotesk),letterSpacingfor headings (tight, normal, wide),imagery(photo-led, illustrated, typographic),layout(airy, balanced, dense, editorial, grid),voice(a sentence on the tone of the copy) andmotion(none, subtle).lookproposethem (if it says two are too alike, change one first), then offer them as one of the plan's questions in the same round (step 2 above:kind: "look", the directions' names as options, your recommendation as the default): never as a separate round of its own. The card shows each direction as swatches (its colours and heading font), the Plan tab as a small hero, and in a text-only app it is one of the numbered questions, each direction's mood beside its letter. The answer is the choice, wherever it was given; "Use the defaults" and approving take your recommendation.lookchoosestill records a choice on its own (or the brand they brought), andlookapplywritesdesign/tokens.csswith the fonts as the project's own files. Import it once, andlookapplysays what the project's stylesheet still needs. In thenextjs-apptemplate (Next.js with Tailwind CSS 4) that isapp/globals.css: its first rule, before any@import, must be@layer theme, base, lf-base, components, utilities;(without it the base styles are the lowest layer and Tailwind's reset undoes them: small headings, transparent buttons, inputs without borders), then@import "tailwindcss";and@import "../design/tokens.css";, and an@theme inlineblock maps the tokens to classes (bg-primary,text-on-primary,bg-surface,text-muted,border-line,font-heading,text-step-1totext-step-5,rounded-look,shadow-1,max-w-measure), as the template's own file has it. Another stack: import it from the stylesheet every page loads, before the site's own rules, with the same layer order when its CSS framework uses cascade layers. - A small site may skip the look question. When one or two questions are enough, pick a
sensible direction yourself (
lookproposeit, thenlookchoose) and say which in a line ("I've gone with a warm, plain look; say if you'd like another"). - Build with the tokens. Use its variables everywhere: colours (
--color-*, with--color-on-primaryfor button labels and--color-linkfor links), the type scale--step--1(small print) to--step-0(body) to--step-5(the largest heading), spacing--space-1to--space-8and--section-spacebetween sections,--measurefor line length,--containerand--gutter,--radius,--shadow-1/--shadow-2, and--heading-weight/--heading-style/--heading-trackingon headings.class="lf-base"on<body>gives sensible defaults for text, headings, links,.button/.button-secondary, fields and the focus ring, and your own CSS overrides them. Follow the look's imagery (photo-led: the owner's photos lead; illustrated: illustration or patterns; typographic: large type, few images) and write the copy in its voice. A change of look is then one change. - It's all offered: a user with their own design system, brand or way of working keeps theirs;
record their brand as a look (
lookchoosewithlook) or skip the look question entirely. - Images of its own. An image made from a prompt comes from your own AI app's image tool
first (
imagesstatuswith a project says what the account chose and what to do in your app). In ChatGPT: make it in the chat, let the user pick one, thenfs_importit (files: that image,generated: true) andimagesplaceoreditoptimiseit into the page; if ChatGPT can't pass it, ask the user to tap Add photos on the LazyFrog card, or to download it and upload it (the link is in "Their own photos and files"). If your app refusesgeneratedas an argument it doesn't know, its LazyFrog tool list is older: ask the user to refresh LazyFrog in the app, and don't save the image without the flag meanwhile (it would be recorded as their own photo). Codex makes images too: bring the file in by git or the upload link. Apps without an image generator (Claude, Claude Code) draw logos, icons and illustrations as SVG themselves, and use the user's photos or free stock (imagesstock) for photographs. Recraft (imagesgenerate,styleand its edits) is deprecated and goes on 2027-02-09; until then it works only for an account that added its own Recraft key, when the user asks for it (a separate paid service on their key and credits). Never pitch it unprompted. Never present a generated image as the user's real work, place or people (a portfolio's projects, a salon's results, a restaurant's dishes, the team): use their photos for those, and let generated images set a mood, illustrate an idea or stand in clearly marked as placeholders until their photos arrive. - Stock photos for mood, with no key.
imagesstocksearches licensed photos from Unsplash (query: the mood or scenery; orientation, color) and shows one numbered list with previews, each markedunsplash:…; look at them and choose. Use at most two searches per image slot (the platform shares its allowances); if nothing fits, use the owner's photos or a placeholder. Callimagesstock_usefor every stock photo you put on the site (photo_id as listed, used_on such as "home hero", alt): it records the credit the owner sees on the Overview and returns the<img>to paste. An Unsplash photo is hotlinked from Unsplash with a srcset: never download one into the project. For a hero, droploading="lazy"and addfetchpriority="high". When you swap one, passreplaceswith the old photo's id. Stock sets the mood and scenery (landscape, town, coast, food in general, textures); it never stands in for the owner's own rooms, work, products, team or customers: those staydata-lf-placeholderuntil their photos arrive. The site needs no photo credit; add the credit snippetstock_usegives only if the owner wants one. If the search says it's busy, carry on with placeholders and try later. Unsplash photos don't load inpreview_screenshotordesign_check(they show as a block of the photo's colour there); visitors see them. On phones a wide photo is cropped to the middle: setobject-position(orbackground-position) on its subject, the fells not the sky, and keep hero text where the photo is calm. Alt text says what the photo shows; name a place only if the photo's own description does. A stranger far off in a landscape is fine; never use stock people as if they were the owner's team or customers. - Photos of the business found elsewhere are someone's copyright. A photo of the shop, its
people or its work in a news article, a directory, a review site or a social page belongs to
whoever took it: don't copy it into the project, even with a credit. Ask the owner to upload
their own (Project › Files › Upload) or to confirm in writing that the photos are theirs. A
business site shows no source credits (it isn't an article); where each photo came from stays
in the plan's content inventory (
plansavewithcore.content). LazyFrog can't read Facebook or Instagram (they need a sign-in): say so and ask the owner for what is there. For a business with no website to capture, discover the content before you design: what exists, where, and whether it may be used, saved ascontent, with what is missing. The platform records where every image its tools put in came from (images placefrom an upload or from a file the user attached in the chat thatfs_importsaved,stock_use, a capture,generate, an image your app made thatfs_importsaved withgenerated: true, an SVG you wrote yourself withfs_write, recorded as drawn by you, and every web versionimagesmakes from a recorded image); an image copied in any other way (exec curl,cpof something unrecorded, a PNG drawn withmagickinexec) has no recorded source, nor has a file pushed with git, and publish and preflight list it until the owner confirms it on the Plan tab (a warning, never a block). A share card or any PNG of your own drawing: write the SVG withfs_write, thenimageseditoptimiseit withsave_to: "site/share.png", which keeps its source (2026-11-05: a share card drawn withmagickasked the owner to confirm it was theirs). - More than a few pages? Read guide topic
page-layoutbefore the first page: one shared layout first, so the pages can't drift apart. - Start from sections, not a blank page (static sites).
site/sections/(open/sections/on the preview) has heroes (photo, illustrated, split), features, priced cards, steps, testimonials, an enquiry form wired to forms, a find-us block with a map, a gallery with a lightbox, and a footer, built on the look's tokens solook applyrestyles them. Read its README, use only the sections this customer needs, and make them theirs: their words, order, layout and imagery. Two customers with the same request should never get the same site. Testimonials only with real customers' words and permission. Before publishing, move the files you use out ofsections/and delete the folder. - Sample data on purpose (example clients or bookings in a preview, for trying the app): put
data-lf-demoon the element that shows it.design_checkthen doesn't count its example.com addresses as placeholders in the preview, and says so as a fix if it shows on the live site (env production): seed sample data only whenLF_ENVispreview. - Mark what the owner must supply. Put
data-lf-placeholder="what they should supply"on anything still to come from the owner: their photos, prices, address, phone, opening hours, real reviews. Never invent a phone number, an email address, a price or a review. The preview outlines and labels each one; a publish or preflight counts what's left and on which pages (a warning, never a block); the owner sees the list under "Needed from you" on the project's Overview and uploads photos there intodata/import. When their photos arrive, look at them withimagesview(paths: ["data/import"]: a numbered contact sheet with each photo's real size, orientation and the slot it was uploaded for), then put each one in withimagesplace(from,file,slot: the placeholder's label,alt): it makes the web versions at the slot's widths, crops them to its shape and removes the attribute. For other things, put the real text in and remove the attribute. A placeholder image's alt says it's coming ("Photo of the Skiddaw room, coming soon"), never as if it were the real photo. "Lorem ipsum", "[your email]", "000000",example.comandTODOare counted the same way. - Build in milestones. Show the preview early (the real look and navigation first, then the
core feature, then the rest), tick the plan's done-when items with
planprogressas they land (milestone lines in a text-only plan withplansave), and check your work before saying it is ready. Before you tell the user a site or page is ready, rundesign_checkon its pages (paths, up to 4). It looks at each page as a visitor would at phone (390 px), tablet (768) and wide laptop (1440), and lists what to fix: content wider than the screen, text hard to read over a picture, broken, soft, heavy or badly cropped images, a grid's last item left alone on its row (a list whose number of items comes from data, such as search results or free time slots, getsdata-lf-dynamicand isn't checked for that), placeholder text such as "000000", "[your email]" or example.com, and missing title, description or share image. Fix everything marked fix, run it again until none are left, and look at the contact sheet it returns: the check can't judge taste. Anything markeddata-lf-placeholderis for the owner to supply: tell the user what is still needed. To see every width while you work, usepreview_screenshotwithviewport: "all". Desktop and mobile alone miss layouts that break only at tablet or wide-laptop widths. Pages behind the app's own sign-in: sign a test user in withhttp_requestand pass its session cookie topreview_screenshot(anddesign_check) asheaders. - Match the person. Someone non-technical should never have to make a technical choice: ask in plain words and decide the rest. Someone technical may bring a PRD, a stack, a design system or strong opinions: use them, and skip the questions they already answered.
- Their stack, or a sensible one. Any language, framework, library or architecture runs here: the platform gives every project the same Linux workspace, PostgreSQL, file storage, services, publishing, domains and snapshots, whatever it's written in. When the user (or code they bring) names a stack, use exactly that, and take this guide's recipes only where they fit it. When they don't, choose for them without asking: a Next.js app for most apps and SaaS, a static site for a brochure site, FastAPI when the work is Python or data-heavy (the templates below). This guide's library suggestions (Better Auth, pg-boss…) are defaults for that case, never requirements. What no stack can do here: native iOS or Android builds (a web app installs on the phone), Windows or macOS programs, GPUs, root or kernel changes, and inbound raw TCP or UDP (web traffic, WebSockets and streams work).
- Behind LazyFrog, any framework: requests arrive with the site's own hostname (the
preview's address, or the live one),
X-Forwarded-Proto: httpsandX-Forwarded-Host; health checks,http_getandhttp_requestsend the same. So: allow.lazyfrog.appand the user's domain in the framework's host check (DjangoALLOWED_HOSTS, Railsconfig.hosts, Viteserver.allowedHosts, which previews already allow); a dev server that refuses other origins allows*.lazyfrog.appand10.200.*.*(Next.jsallowedDevOrigins; the templates and imports set it), or LazyFrog's checking browser logs blocked live-reload requests; trust the forwarded protocol (DjangoSECURE_PROXY_SSL_HEADER, Railsconfig.assume_ssl, LaravelTrustProxies, Expresstrust proxy, uvicorn--proxy-headers --forwarded-allow-ips '*'on the preview's command as well as the live one, or the preview'srequest.base_urlsays http and a same-origin check refuses its own forms); don't redirect to https yourself (the edge does, and health checks speak plain http); CSRF trusted origins areSITE_URL(Phoenix:PHX_HOSTandcheck_originfrom it, plus the user's own domain once connected, or LiveView sockets get 403); andX-Frame-Optionsor aframe-ancestors 'self'default (Phoenix'sput_secure_browser_headers, Rails'X-Frame-Options: SAMEORIGIN) hides the site from the owner's portal: allowhttps://app.lazyfrog.devin routing'sframe-ancestorsinstead.LF_ENVsaysprevieworproduction. A one-off command on the live site (Djangocreatesuperuser, a Rails task):execruns on the preview only, so do it through a confirmed productiondb_query, a data migration or task themigratecommand runs at the next publish, or an owner-only route behindroutingauth. - Laravel: start from
blankwithcomposer create-project(thelaravel newinstaller takes the newest major; pin the one the user named, e.g.laravel/livewire-starter-kit:^1.0for 12), then delete the.envit writes: itsDB_CONNECTION=sqlitefills in whatever is unset, so the preview would quietly run on SQLite while production runs on PostgreSQL. Default the connection topgsqlwhenDATABASE_URLis set, and map Laravel'sDB_URLto it inconfig/database.php(themigratecommand gets only the platform's variables, not a service'senv).trustProxies(at: '*')inbootstrap/app.php(the edge sets the forwarded headers; without it every asset URL ishttp://and pages load unstyled).'url' => env('APP_URL', env('SITE_URL')), or queued mail and scheduled jobs link to localhost. SetAPP_ENVandAPP_DEBUGfromLF_ENV, andtimezonefromTZ(Laravel hard-codes UTC).APP_KEY:secrets setwithgenerate: "laravel"andbuild: true. Mail: the smtp mailer with host and port fromSMTP_HOSTandSMTP_PORT. Production:php-fpmbehind Caddy, withclear_env = noanderror_login a file under/tmp(a service's stderr can't be reopened by path).view:cacheandconfig:cachemay write into the release as usual. Uploads go under/data(storage:linkpoints into the release, which a publish replaces); public ones in/data/public, served at/files. Queue: thedatabasedriver withqueue:workas a worker; scheduler: acronservice runningschedule:run(below). Tests: without a.env(deleted above) Pest warns on every test, so make thetestcommandtouch .env && ./vendor/bin/pest; services.jsonenvreaches it, and PHPUnit's<env>doesn't override what is already set, so mark those linesforce="true"(or tests may mail real people). Signing a test user in: Livewire and Inertia pages carry their tokens in the page, so read it withhttp_get(offsetwhere the body was cut), then post withhttp_request, sending the first response's cookies with each later request (the token belongs to that session). - A stack someone else suggested, mid-build ("my nephew says WordPress"): that's hearsay,
not the user naming a stack. Don't switch: say it's possible here and what it would change
(who maintains it, plugins to keep updated), that nothing locks them in (Project › Code ›
Export, and the other person can work on it with their own AI), ask what the suggestion is for,
and switch only if they still want it. WordPress here: PHP 8.5 is installed; run it with
php-fpmand Caddy (orphp -Son the preview; php-fpm notes under Laravel above); the "WordPress" section has the recipe. WordPress doesn't run on PostgreSQL: use the official SQLite Database Integration plugin with its file under/data, or MariaDB as a service with its data under/data. Before each snapshot LazyFrog makes a clean copy of every SQLite database (in/data/.lazyfrog/clean/); for MariaDB give its service abackupcommand (mariadb-dump --all-databases --single-transaction > /data/.lazyfrog/clean/mariadb.sql). Code goes from preview to live at every publish and data does not: WordPress's own database upgrade runs through the project'smigratecommand (the "WordPress" section), and content is written on the live site's/wp-admin, as the owner's Overview says. A brochure site often reads better converted to a static site. - Their plan, their look, their process. Everything in this section is offered: when the user arrives with a spec, a design, a process of their own, or says "just build it", follow that and skip the steps here they don't want.
- Their language. Talk with the user in their language. Set
<html lang>to the site's language (the templates sayen). For two or more: a path per language (/fr/…,/en/…), hreflang alternates and a sitemap listing them, a switch that keeps the page,/choosing byAccept-Language, and a_nextpage in that language for forms. French spacing before; ! ?and inside « » is U+00A0. The portal is in English: quote its labels in English, with your translation in brackets, so the user can find them. - Translations you're less sure of (isiZulu, Welsh, any language you'd hesitate to write a
sign in): say so, write plain short sentences, and make "a native speaker reads it" a plan
milestone before publishing; never present your own translation as checked. Safety text first
(allergies, prices, opening hours). Give each language its own field in anything the owner edits,
and mark text shown in another language (
lang="en"on an English special on the isiZulu page) so screen readers say it right. - Content the owner keeps up to date (today's specials, prices, hours, a dish sold out, the
week's quiz; the most common small-business ask): an owner page on the live site behind
routingauth, saving to the live database, so a change shows at once with no publish; say "it's on the site now" only when it is. Keep a history with Undo, hide rather than delete, check a suspicious change (a price ten times smaller) before saving it, and mark the pages that show itno-store. Photos from the owner's phone go to/data/public/…from that page (Project › Files edits the preview's files). Project › Snapshots restores the preview; live data comes back through you, with the owner's approval.
If the user says to just build it, or gave you everything already, honour it: record it with
plan agree and defaults: true, build with sensible defaults (choose the best look yourself),
and tell them which details are placeholders.
For your AI: this page as Markdown · llms.txt