Building good apps here
Recipes for particular kinds of app on LazyFrog, such as offline, installable phone apps (PWA) and API products.
- Offline and installable apps (PWA), e.g. a technician's phone app: a service worker and a
queue in IndexedDB with an id per change (so a resend does nothing twice). Cache only
okresponses: while the site wakes, the platform answers 503 withno-store, and a worker that caches it saves the waking page as the app. Leave/_lf/*(uploads, forms) to the network./sw.jsis served withmax-age=0, so updates arrive. The preview runs the dev server, which can't work offline: test offline on the live site. A request to a sleeping site waits while it wakes (up to about 25 s) and then succeeds, so a queue drains after a wake without special handling. - API products (a paid API, a dashboard beside it): one project, a web service and an api
service, with routing
rewritessending/api/:path*to the api; FastAPI behind that rewrite needsroot_path="/api"(or keep the prefix) for/docs. CORS in routing (exposethe rate-limit headers,max_age); preflights are answered without waking the site. When the platform itself answers (asleep, restarting, a preview password, too many waking at once), API clients (notext/htmlin Accept) get JSON{"error": {code, message, request_id, from: "lazyfrog"}}with the status andRetry-After. There's no platform rate limit on a site's own traffic: count per key in Postgres (a sliding window, not a fixed minute). Outbound calls are limited per destination (pool connections and reuse them); private addresses are refused. Webhooks wake a sleeping site (they are visitors); cron jobs and the app's own retries don't run while it sleeps, so an app that needs them on time (a sale opening, expiring holds) is always on, at least for those days. A custom domain is the whole site's:api.andapp.as two entry hostnames of one project both reach the same app, which routes byHost(Custom domains). Integration tests against a private preview send the preview password as Basic auth (any username).service runruns in the preview only. Rolling the database back also undoes customers' sign-ups, keys and usage since then: prefer a forward fix. Bandwidth counts toward the plan's month; live sites pause when it reaches the cap (Hobby after 48 hours' grace), not at the month's end, so watchusage_get: from 80% it has the owner's way to more (a paid plan's one-tap 100 GB boost with its price, until the period ends; an upgrade from Free). You can't pay for them. - Accessible by default: labelled fields,
fieldsetandlegendfor groups of choices, 44 px tap targets, visible focus, never colour alone for right and wrong, a timer announced at a few moments rather than every second, and a readable font served from the project (Atkinson Hyperlegible reads well for dyslexic readers). - Quizzes and tests: the answer key and the time limit stay on the server, one attempt per person is a unique constraint, and the answers show once everyone has finished.
- Menus and food: allergens and diets in words, not colour alone ("V Vegetarian", "Contains nuts"), a line asking guests to tell staff about allergies ("we can't guarantee no traces"), and never "nut-free". On the owner's form, allergen questions are a yes or no that must be answered, not a box that starts unticked.
- Keep the health check route cheap and unauthenticated (
/api/health,/health). - Web apps: server-render or build static assets; keep dependencies few and pinned (the lockfile is
committed). Add a package at its current release (
pnpm add better-auth,uv add httpx: no version), never a version you remember, which is often old and has known holes (2026-11-13: a build pinned a sign-in library a year out of date with 15 advisories); the lockfile then pins it, andproject_scanchecks it. New npm and Python package versions must be at least a week old (pnpm and uv enforce it); for other ecosystems pick releases that have been out a week or more too. - Go: put build tools (sqlc, goose) in go.mod as
go tooldependencies (Go 1.24+) rather thango install, so they are pinned with the project. Builds reuse the preview's cache only with the same flags: the first build with new flags (-trimpath,-ldflags) recompiles everything, later ones reuse the cache. - Background work: a queue in the project's PostgreSQL needs nothing else to run or back up
(pg-boss or Graphile Worker on Node, Procrastinate on Python, Solid Queue or GoodJob on Rails, whose
Rails 8 default expects separate cache, queue and cable databases: use the one
DATABASE_URL(dropconfig.solid_queue.connects_toand the cache and cabledatabase:keys) and copydb/queue_schema.rb,db/cache_schema.rbanddb/cable_schema.rbinto a migration, or the tables are missing, Oban on Elixir, River on Go). PostgreSQL is the project's database, kept in every snapshot and publish; another engine (Redis, MySQL, MongoDB, SQLite) can run as a service in the workspace with its data under/data, or as a hosted one. Before each snapshot LazyFrog makes a clean copy of every SQLite database (/data/.lazyfrog/clean/) and runs a service'sbackupcommand (amariadb-dumpormongodumpinto that folder), so a restore has a clean copy beside the files as a crash would leave them; publishing carries code, not their data, so content an app keeps there (a CMS's posts) is written on the live site, not carried over from the preview (the first publish can copy it once, if the user asks: see "Publishing"); amigratecommand runs on a copy of the live data first, then on the live data after a snapshot. - Store uploaded files under
/data(through the S3 API for visitors' uploads). - Cookies: scope them to the site's own host (or use the
__Host-prefix).
For your AI: this page as Markdown · llms.txt