clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / docs / deployment.md
deployment.md 11.2K rendered
1# Building and deploying a lofi app23lofi produces a static PWA. The production build contains prerendered HTML, JavaScript, the Jazz4WASM/runtime assets, the web manifest, and a revisioned service worker.56```mermaid7flowchart LR8 Source["Application source"] --> Doctor["doctor + test"]9 Doctor --> Build["deno task build"]10 Build --> Fingerprint["source fingerprint"]11 Build --> Precache["service-worker precache"]12 Build --> Scan{"secret scan passes?"}13 Scan -- "no" --> Stop["Stop and remove leak"]14 Scan -- "yes" --> Dist["dist/"]15 Dist --> Preview["Local preview"]16 Dist --> Deno["Deno Deploy"]17 Dist --> Static["Other static host"]18```1920## Build and preview2122```sh23deno task build24deno task preview25```2627`build` writes `dist/`, records a source fingerprint in `dist/lofi-build.json`, generates the28precache list, and scans for server-secret values. Before Astro starts, the shared doctor preflight29validates the author-owned manifest and referenced assets. After Astro finishes, build verifies the30emitted HTML links, manifest, icons, nested routes, worker revision/scope, build identity, and exact31precache set together. `preview` refuses to start when the build identity is missing or invalid.3233To use another preview port:3435```sh36deno task preview --port 417337```3839## Configure the public application surface4041Before deploying, review:4243- `src/app.ts` — name, database namespace, stable credential origins, and repository URL;44- `public/manifest.webmanifest` — stable install identity, locale, icons, colors, launch behavior,45 and shortcuts;46- `public/favicon.svg` and any added icon files;47- page titles, descriptions, and starter copy;48- `.env` — either no public Jazz pair for local-only mode or a complete pair for optional sync.4950The default deployment base is `/`. When a static host mounts the application below the origin root,51set the path once in `.env` before running `dev` or `build`:5253```dotenv54LOFI_BASE_PATH=/field-notes/55```5657The value must be an absolute path, not an origin. Lofi feeds it into Astro's `base` setting and58uses the resulting base for public asset links, the manifest, the service worker, its scope, build59identity, and local preview. Upload the contents of `dist/` to that same mount point. A root build60and a subpath build are different deployment artifacts; rebuild after changing `LOFI_BASE_PATH`.6162Run `deno task doctor` and `deno task test` before the production build.6364### Customize the web manifest before launch6566Treat `public/manifest.webmanifest` as product source, not generated build metadata. Before users67install the app:6869- replace `name`, `short_name`, `description`, `id`, `lang`, and `dir` with product values;70- choose an `id` URL token that will remain stable for the lifetime of the installed app; unlike71 `start_url`, it identifies the app and does not need to be a navigable page;72- keep `scope`, `start_url`, every shortcut URL, and every shortcut icon aligned with the deployed73 base path;74- replace the regular, maskable, Apple touch, and transparent monochrome icon assets while keeping75 their purposes intact;76- keep `orientation: "any"` unless the product genuinely needs a screen-orientation lock; and77- replace or remove the starter `Open tasks` shortcut when replacing the task example.7879After launch, changing the name or start page is routine; changing `id` can make a browser treat the80manifest as a different installed application. Choose it once. If the product ships in multiple81languages, keep the default strings plus `lang` and `dir`, then add language maps such as82`name_localized`, `short_name_localized`, and `description_localized`. Localized values whose text83direction differs from the manifest default should declare their own `dir`.8485Storefront metadata is optional and product-specific. Add lowercase `categories` only when they86truthfully describe the finished app. Add `iarc_rating_id` only after obtaining a real IARC87certification code; never copy a placeholder rating. Experimental capabilities such as file,88protocol, share-target, and launch handlers are deliberately absent from the starter and should be89added only with matching product behavior and tests. Use the90[installed-app recipe catalog](recipes/README.md) for the supported opt-in patterns.9192### Replace or remove install presentation9394The starter manifest includes one labeled `540x720` narrow screenshot, one labeled `1280x720` wide95screenshot, and one **Open tasks** shortcut. They are examples of the real generated app, not96generic promotional art. Before launch:9798- replace both screenshots with current product captures and update each `sizes`, `label`, and99 `form_factor`, or remove the entire `screenshots` member and both files;100- replace the shortcut name, description, route, and icon with one useful product entry point, or101 remove the entire `shortcuts` member;102- keep shortcut routes inside manifest scope and backed by a prerendered route so they cold-start103 offline; and104- keep shortcut icons inside scope with truthful MIME types and dimensions.105106Build validation checks every referenced asset, requires labeled narrow and wide variants when the107`screenshots` member is present, and rejects shortcut routes that were not emitted. Screenshots are108deliberately excluded from the required shell precache; deleting promotional captures cannot break109offline startup.110111The starter does not claim a Web Share Target. If a product opts in, follow the tested112[Web Share recipe](recipes/web-share.md): first add a same-scope, prerendered action route, then add113a manifest member such as:114115```json116{117 "share_target": {118 "action": "./share/",119 "method": "GET",120 "enctype": "application/x-www-form-urlencoded",121 "params": { "title": "title", "text": "text", "url": "url" }122 }123}124```125126Use `parseTextShareTarget()` in the receiving island to ignore unknown parameters, reject duplicate127or oversized values, and accept a shared URL only after parsing it and allow-listing `https:` and128`http:`. Present the result as a draft: receiving a share is not user confirmation to persist it.129Build validation rejects malformed declarations, POST/file shares without a matching worker recipe,130and action routes that were not emitted for offline startup.131132## Deno Deploy133134Create the static application once:135136```sh137deno task deploy:create --org <org> --app <app>138```139140For later releases:141142```sh143deno task deploy144```145146Both tasks build first and deploy `dist/` as the static root.147148## Other static hosts149150Upload the contents of `dist/` to any host that can:151152- serve `index.html` at the application root;153- preserve the manifest and WASM content types;154- serve the application over HTTPS;155- keep the service worker at the intended scope;156- fall back to the appropriate prerendered HTML for application routes.157158Every prerendered route is included in the shell precache. While offline, a direct navigation such159as `/field-notes/settings/` resolves its cached `settings/index.html`; if that route was not160emitted, the worker falls back to the cached application root.161162The app is the only thing that needs hosting: sync is optional infrastructure the user can point at163a managed Jazz app or at [a self-hosted sync node](/node) they run themselves — the deployment does164not change either way.165166### Offline cache policy167168The build's precache manifest contains required shell resources only. Shell assets are fetched169bypassing the HTTP cache (matching the manifest's own `no-store` fetch), so a new revision can never170be populated from stale HTTP-cached responses. If any listed response cannot be fetched,171service-worker installation fails and reports a precache error rather than exposing a worker that172cannot cold-start the application. Product-specific optional resources do not belong in that173manifest.174175Cache names carry the registration scope, and lookups consult only the worker's own caches: apps176served from different base paths on one origin never read, shadow, or delete each other's caches,177and activation prunes only the current scope's previous revisions. Requests carrying a `Range`178header are left to the network — a cached complete response cannot answer a partial request.179180Runtime caching is a separate, best-effort policy. It accepts only successful, public, same-origin181responses inside the worker scope for fonts, images, scripts, styles, and workers. Navigation,182cross-origin requests, partial responses, `private` or `no-store` responses, and `Vary: *` responses183are not added. The cache retains at most 64 entries, moves refreshed URLs to the end, evicts the184oldest inserted overflow, and removes the current scope's previous build revisions during185activation.186187Navigation preload remains disabled: generated routes and their assets are precached, so starting a188parallel network request before the normal cache-first lookup would spend bandwidth on the expected189offline-ready path. Jazz sync, OPFS storage, background sync, and push remain outside the worker.190191### Install and update lifecycle192193The optional `PwaActions` UI uses a browser prompt only while `beforeinstallprompt` is actually194available. iOS receives its Share-menu steps. Other secure browsers with service-worker support get195generic browser-menu guidance that says to use **Install app** or **Add to Home Screen** only if the196browser offers it; an insecure or unsupported context is reported separately.197198When the app returns to the foreground, restores from the back-forward cache, or reconnects, Lofi199asks the active service-worker registration to check for an update. Checks share one in-flight200request, time out, and are rate-limited. Update state moves through `checking`, `installing`,201`ready`, and `applying`; a waiting worker activates only after the user chooses **Update app**. Once202the new worker takes control, every claimed tab reloads — the new worker prunes the old revision's203caches, so a document left on the old HTML and module graph would go stale — not only the tab that204chose the update. The first claim of a previously uncontrolled page does not reload, and a document205reloads at most once, so ordinary worker changes cannot create a reload loop.206207Foreground recovery reconnects managed sync only while the current account has elected sync and the208development inspector has not paused the transport; a configured server alone is not consent to209resume replication.210211A runtime-cache write error is best-effort and leaves the active worker ready. Registration,212required precache, and activation failures remain worker failures; update-check failures leave the213current worker running and retry on a later foreground signal.214215Do not run a server-side Jazz credential in the static host or expose `JAZZ_ADMIN_SECRET` or216`BACKEND_SECRET` as public environment variables.217218## Stable origins matter219220Durable storage and service workers require a secure context outside localhost. WebAuthn credentials221also bind to the hostname. Choose the permanent production hostname before relying on device222credentials, add it to `credentialOrigins`, and avoid redirect or preview URLs that change between223deployments.224225## Release verification226227On the deployed HTTPS URL:2282291. Confirm the device capability panel passes.2302. Add data, reload, and restart the browser.2313. Install the PWA and perform an offline cold start.2324. If sync is configured, opt in with a throwaway account and verify another device can recover it.2335. Inspect the built application for the expected version/source fingerprint.