# Before you build something new (https://lazyfrog.dev/docs/guide/before-you-build-something-new)

{/* Generated by scripts/generate-docs.ts from services/gateway/src/guide.md. Do not edit. */}

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:

0. **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.
1. **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).
2. **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.
3. **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.
4. **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.
5. **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.
6. **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.
7. **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.
