Skip to content
LazyFrog

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:

  1. 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.
  2. 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 (site capture with 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. If account shows 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).
  3. One round of questions, each with a default, the look among them. Save a short draft (plan save with core: brief in two or three sentences, audience, size, mustHaves, later, data, integrations, intake (below), look, a doneWhen checklist of what "done" means for the first build, and an honest estimate band in minutes). Design the look directions first and look propose them (see "Give it its own look" below), then plan ask once with at most five questions, each with two to six options and a sensible default among 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 with kind: "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 (no look choose needed). 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: an intake entry in the core with what, 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) and keptFor (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 a doneWhen item for it, such as "a test submission arrives where the plan says". The plan save result says when a draft in planning still has no questions, no structured core or no intake.
  4. 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 plan answer (answers, or defaults: true). Answers tapped on the card, given on the Plan tab or typed in the chat all land in the plan: read them with plan_read get, never from memory of the chat.
  5. 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 plan agree and said: their own words, quoted ("Yes, go ahead"), unless the card or your app's form already did (plan_read get says 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, plan agree with defaults: 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.
  6. Build against the plan. Tick doneWhen items with plan progress as each one lands; the card shows "3 of 7 done, next: …" and the time against the estimate.
  7. Done means checked. Before you say the site is done, check every doneWhen item yourself in the preview and run design_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.
  8. 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 plan save says 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. look get gives starting points seeded for this project (use, mix or ignore them; the brief comes first). Each direction needs a name, a mood (a few words), six colors (background, surface, text, muted, primary, accent), fonts (heading and body, Google Fonts families), radius (none, small, medium, large, pill) and density (compact, comfortable, airy); it can also set typeScale (compact, classic, dramatic), headingWeight (300–900) and headingItalic (an editorial serif in light italic, a bold grotesk), letterSpacing for headings (tight, normal, wide), imagery (photo-led, illustrated, typographic), layout (airy, balanced, dense, editorial, grid), voice (a sentence on the tone of the copy) and motion (none, subtle). look propose them (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. look choose still records a choice on its own (or the brand they brought), and look apply writes design/tokens.css with the fonts as the project's own files. Import it once, and look apply says what the project's stylesheet still needs. In the nextjs-app template (Next.js with Tailwind CSS 4) that is app/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 inline block maps the tokens to classes (bg-primary, text-on-primary, bg-surface, text-muted, border-line, font-heading, text-step-1 to text-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 (look propose it, then look choose) 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-primary for button labels and --color-link for links), the type scale --step--1 (small print) to --step-0 (body) to --step-5 (the largest heading), spacing --space-1 to --space-8 and --section-space between sections, --measure for line length, --container and --gutter, --radius, --shadow-1/--shadow-2, and --heading-weight/--heading-style/--heading-tracking on 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 (look choose with look) 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 (images status with 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, then fs_import it (files: that image, generated: true) and images place or edit optimise it 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 refuses generated as 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 (images stock) for photographs. Recraft (images generate, style and 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. images stock searches licensed photos from Unsplash (query: the mood or scenery; orientation, color) and shows one numbered list with previews, each marked unsplash:…; 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. Call images stock_use for 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, drop loading="lazy" and add fetchpriority="high". When you swap one, pass replaces with 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 stay data-lf-placeholder until their photos arrive. The site needs no photo credit; add the credit snippet stock_use gives 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 in preview_screenshot or design_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: set object-position (or background-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 (plan save with core.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 as content, with what is missing. The platform records where every image its tools put in came from (images place from an upload or from a file the user attached in the chat that fs_import saved, stock_use, a capture, generate, an image your app made that fs_import saved with generated: true, an SVG you wrote yourself with fs_write, recorded as drawn by you, and every web version images makes from a recorded image); an image copied in any other way (exec curl, cp of something unrecorded, a PNG drawn with magick in exec) 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 with fs_write, then images edit optimise it with save_to: "site/share.png", which keeps its source (2026-11-05: a share card drawn with magick asked the owner to confirm it was theirs).
  • More than a few pages? Read guide topic page-layout before 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 so look apply restyles 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 of sections/ and delete the folder.
  • Sample data on purpose (example clients or bookings in a preview, for trying the app): put data-lf-demo on the element that shows it. design_check then 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 when LF_ENV is preview.
  • 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 into data/import. When their photos arrive, look at them with images view (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 with images place (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.com and TODO are 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 plan progress as they land (milestone lines in a text-only plan with plan save), and check your work before saying it is ready. Before you tell the user a site or page is ready, run design_check on 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, gets data-lf-dynamic and 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 marked data-lf-placeholder is for the owner to supply: tell the user what is still needed. To see every width while you work, use preview_screenshot with viewport: "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 with http_request and pass its session cookie to preview_screenshot (and design_check) as headers.
  • 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: https and X-Forwarded-Host; health checks, http_get and http_request send the same. So: allow .lazyfrog.app and the user's domain in the framework's host check (Django ALLOWED_HOSTS, Rails config.hosts, Vite server.allowedHosts, which previews already allow); a dev server that refuses other origins allows *.lazyfrog.app and 10.200.*.* (Next.js allowedDevOrigins; the templates and imports set it), or LazyFrog's checking browser logs blocked live-reload requests; trust the forwarded protocol (Django SECURE_PROXY_SSL_HEADER, Rails config.assume_ssl, Laravel TrustProxies, Express trust proxy, uvicorn --proxy-headers --forwarded-allow-ips '*' on the preview's command as well as the live one, or the preview's request.base_url says 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 are SITE_URL (Phoenix: PHX_HOST and check_origin from it, plus the user's own domain once connected, or LiveView sockets get 403); and X-Frame-Options or a frame-ancestors 'self' default (Phoenix's put_secure_browser_headers, Rails' X-Frame-Options: SAMEORIGIN) hides the site from the owner's portal: allow https://app.lazyfrog.dev in routing's frame-ancestors instead. LF_ENV says preview or production. A one-off command on the live site (Django createsuperuser, a Rails task): exec runs on the preview only, so do it through a confirmed production db_query, a data migration or task the migrate command runs at the next publish, or an owner-only route behind routing auth.
  • Laravel: start from blank with composer create-project (the laravel new installer takes the newest major; pin the one the user named, e.g. laravel/livewire-starter-kit:^1.0 for 12), then delete the .env it writes: its DB_CONNECTION=sqlite fills in whatever is unset, so the preview would quietly run on SQLite while production runs on PostgreSQL. Default the connection to pgsql when DATABASE_URL is set, and map Laravel's DB_URL to it in config/database.php (the migrate command gets only the platform's variables, not a service's env). trustProxies(at: '*') in bootstrap/app.php (the edge sets the forwarded headers; without it every asset URL is http:// and pages load unstyled). 'url' => env('APP_URL', env('SITE_URL')), or queued mail and scheduled jobs link to localhost. Set APP_ENV and APP_DEBUG from LF_ENV, and timezone from TZ (Laravel hard-codes UTC). APP_KEY: secrets set with generate: "laravel" and build: true. Mail: the smtp mailer with host and port from SMTP_HOST and SMTP_PORT. Production: php-fpm behind Caddy, with clear_env = no and error_log in a file under /tmp (a service's stderr can't be reopened by path). view:cache and config:cache may write into the release as usual. Uploads go under /data (storage:link points into the release, which a publish replaces); public ones in /data/public, served at /files. Queue: the database driver with queue:work as a worker; scheduler: a cron service running schedule:run (below). Tests: without a .env (deleted above) Pest warns on every test, so make the test command touch .env && ./vendor/bin/pest; services.json env reaches it, and PHPUnit's <env> doesn't override what is already set, so mark those lines force="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 with http_get (offset where the body was cut), then post with http_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-fpm and Caddy (or php -S on 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 a backup command (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's migrate command (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 say en). For two or more: a path per language (/fr/…, /en/…), hreflang alternates and a sitemap listing them, a switch that keeps the page, / choosing by Accept-Language, and a _next page 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 routing auth, 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 it no-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