clone

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

main
lofi / website / src / pages / index.tsx
index.tsx 20.5K
1import type { ReactNode } from "react";2import { useState } from "react";3import Link from "@docusaurus/Link";4import useDocusaurusContext from "@docusaurus/useDocusaurusContext";5import useBaseUrl from "@docusaurus/useBaseUrl";6import Layout from "@theme/Layout";7import SyncDemo from "../components/SyncDemo";8import Leaf from "../components/Leaf";910import "../css/landing.css";1112const CREATE_CMD = "deno run -A jsr:@nzip/lofi/create my-app";1314function CopyCmd(): ReactNode {15 const [label, setLabel] = useState("Copy");16 const copy = async () => {17 try {18 await navigator.clipboard.writeText(CREATE_CMD);19 setLabel("Copied");20 setTimeout(() => setLabel("Copy"), 1600);21 } catch {22 setLabel("Ctrl+C");23 }24 };25 return (26 <div className="cmd">27 <code>28 <b>$&nbsp;</b>29 {CREATE_CMD}30 </code>31 <button type="button" onClick={copy}>{label}</button>32 </div>33 );34}3536const FEATURES: Array<{ title: string; body: ReactNode }> = [37 {38 title: "Local-first data",39 body:40 "The UI reads from durable local storage instead of waiting on the network. Storage failures are surfaced, never silently swapped for memory.",41 },42 {43 title: "Identity from first launch",44 body:45 "Each user begins with a private on-device account, even when the app has no sync service configured.",46 },47 {48 title: "Optional sync and recovery",49 body: (50 <>51 Users make that same account portable when you connect managed Jazz52 sync: a passkey where available, a recovery phrase as the portable53 fallback. Or they sync to <Link to="/node">a node they run</Link> — the54 app needs zero changes either way.55 </>56 ),57 },58 {59 title: "Server-blind fields",60 body: (61 <>62 Columns are sealed on the client before they sync —{" "}63 <code className="inline-mono">s.privateTable</code>{" "}64 encrypts by default, and shared sealed columns extend that to groups.65 The keys stay with users; the server relays what it cannot read.66 </>67 ),68 },69 {70 title: "Verbs with effects",71 body: (72 <>73 Tables are the nouns; mutations declared with{" "}74 <code className="inline-mono">s.mutation</code>{" "}75 are the verbs. Their sync-boundary effects run from a durable journal76 on the device that performed the intent — once, even across restarts.77 </>78 ),79 },80 {81 title: "Narrow collaboration templates",82 body: (83 <>84 Private resources, direct shares, and fixed-role groups sit behind a85 small <code className="inline-mono">@nzip/lofi/access</code>{" "}86 API. Raw Jazz permissions stay available as an escape hatch.87 </>88 ),89 },90 {91 title: "A shell that defends itself",92 body:93 "Generated apps ship a strict Content-Security-Policy, detect a forked storage container before it splits the account, and show honest progress on the one first load that needs the network.",94 },95 {96 title: "Local-first test helpers",97 body:98 "Playwright-backed fixtures cover offline writes, multiple clients, convergence, and readiness — without hand-timed sleeps.",99 },100 {101 title: "Explicit mobile support",102 body:103 "Unsupported browsers get a clear explanation, not a half-working app that risks the user's data.",104 },105];106107export default function Home(): ReactNode {108 const { siteConfig } = useDocusaurusContext();109 const version = siteConfig.customFields?.lofiVersion as string;110 const heroArt = useBaseUrl("/img/lofi-hero-art.webp");111112 return (113 <Layout114 title="lofi"115 description="lofi generates installable Deno web apps that open immediately, keep working offline, and sync when users choose — with fields the server can't read. Astro shell, Preact islands, Jazz 2 CRDT sync."116 >117 <main className="landing">118 {/* ===================== HERO: MARK + CLAIM ===================== */}119 <div className="wrap">120 <header>121 <div className="hero-top">122 <h1 className="nmark" aria-label="lofi">123 <span className="lo">lo</span>124 <span className="fi">fi</span>125 </h1>126 <div className="hero-side">127 <p className="eyebrow">128 <span>local-first meta-framework</span>129 <span className="pill">v{version} · alpha · MIT</span>130 </p>131 </div>132 </div>133 <div className="hero-main">134 <div>135 <p className="hero-claim">136 Installable web apps where the network is optional and the137 user holds the keys.138 </p>139 <CopyCmd />140 <p className="cmd-note">141 Requires Deno 2.9+. Astro + Preact + Jazz 2, generated.142 </p>143 <div className="cta-row">144 <Link className="btn btn--primary" to="/docs/getting-started">145 Read the guide146 </Link>147 <Link className="btn" href="https://demo.lofi.host">148 live demo149 </Link>150 <Link151 className="btn"152 href="https://github.com/FelineStateMachine/lofi"153 >154 source155 </Link>156 </div>157 </div>158 <div className="hero-art">159 <img160 src={heroArt}161 width="900"162 height="900"163 fetchPriority="high"164 alt="A phone containing a growing plant. Its roots run below the soil line inside the device; a dotted line arcs to a second, smaller phone."165 />166 </div>167 </div>168 </header>169 </div>170171 {/* ===================== LIFECYCLE ===================== */}172 <div className="wrap">173 <section className="band band--rule">174 <div className="band-head">175 <h2>Open now. Back up later.</h2>176 </div>177 <div className="life">178 <div className="life-step">179 <span className="life-tag">First launch</span>180 <h3>A private account, on the device</h3>181 <p>182 Every user gets an on-device account at first open, even when183 the app has no sync service configured at all. Nothing to sign184 up or waiting.185 </p>186 </div>187 <div className="life-step">188 <span className="life-tag">Every launch after</span>189 <h3>Reads from local storage, not the network</h3>190 <p>191 The UI reads durable local data instead of waiting on a round192 trip. When storage fails, lofi surfaces it rather than quietly193 falling back to memory and risking the data.194 </p>195 </div>196 <div className="life-step">197 <span className="life-tag">If the user wants it</span>198 <h3>The same account, made portable</h3>199 <p>200 Connect managed Jazz sync and a user can back up the account201 they already have — recovering it with a passkey where202 supported, or a 24-word phrase. Data made offline comes along,203 and a fresh device can connect and recover in either order.204 </p>205 <p className="life-cmd">206 <code className="inline-mono">deno task jazz:provision</code>207 <br />208 <span>209 or scaffold with <code className="inline-mono">--sync</code>210 {" "}211 from the start.212 </span>213 </p>214 </div>215 </div>216 </section>217 </div>218219 {/* ===================== DEMO ===================== */}220 <div className="wrap">221 <section className="band band--rule">222 <div className="band-head">223 <p className="eyebrow">at the core of the idea</p>224 <h2>Pull the cable.</h2>225 <p className="lede band-lede">226 This is the starter task app the generator gives you. Turn the227 network off, keep typing on both devices, then turn it back on.228 Edits from both sides survive and every replica converges on229 the same result: same-field collisions resolve last-writer-wins,230 everything else merges cleanly. That is what the CRDT buys you.231 A reworked build of this starter runs installable at{" "}232 <Link href="https://demo.lofi.host">demo.lofi.host</Link>,233 deployed from the latest release.234 </p>235 </div>236 <SyncDemo />237 </section>238 </div>239240 {/* ===================== FEATURES ===================== */}241 <div className="wrap">242 <section className="band band--rule">243 <div className="band-head">244 <h2>The parts you would otherwise write yourself.</h2>245 </div>246 <div className="grid-3">247 {FEATURES.map((feature) => (248 <div className="cell" key={feature.title}>249 <Leaf />250 <h3>{feature.title}</h3>251 <p>{feature.body}</p>252 </div>253 ))}254 </div>255 </section>256 </div>257258 {/* ===================== ANATOMY ===================== */}259 <div className="wrap">260 <section className="band band--rule">261 <div className="band-head">262 <h2>263 You own the product.264 <br />265 lofi owns the plumbing.266 </h2>267 </div>268 <div className="anat">269 <pre270 className="tree"271 aria-label="Generated project layout, coloured by ownership"272 ><code>{"my-app/\n├── "}<span className="yours">deno.json</span>{" "}<span className="note"># tasks + one pinned version</span>{"\n├── "}<span className="yours">public/</span>{" "}<span className="note"># manifest, product icons</span>{"\n├── "}<span className="yours">src/</span>{"\n│ ├── "}<span className="yours"><b>app.ts</b></span>{" "}<span className="note"># storage, sync, passkey config</span>{"\n│ ├── "}<span className="yours"><b>schema.ts</b></span>{" "}<span className="note"># persisted data model</span>{"\n│ ├── "}<span className="yours"><b>permissions.ts</b></span>{" "}<span className="note"># private, shared, or group</span>{"\n│ ├── "}<span className="yours">islands/</span>{" "}<span className="note"># interactive Preact UI</span>{"\n│ ├── "}<span className="yours">layouts/</span>{" "}<span className="note"># document shell</span>{"\n│ ├── "}<span className="yours">pages/</span>{" "}<span className="note"># Astro routes</span>{"\n│ └── "}<span className="yours">styles/</span>{" "}<span className="note"># product styling</span>{"\n├── "}<span className="yours">tests/</span>{" "}<span className="note"># local-first journeys</span>{"\n├── "}<span className="note">.lofi/</span>{" "}<span className="note"># generated tooling (ignored)</span>{"\n└── "}<span className="note">dist/</span>{" "}<span className="note"># production PWA (ignored)</span>{"\n\n"}<span className="theirs">@nzip/lofi</span>{" "}<span className="note"># storage, identity, sync, PWA</span></code></pre>273 <div>274 <p>275 A generated project keeps your source separate from the276 versioned framework package. Product work stays in the files277 above; framework behaviour is imported, never copied into your278 tree. Upgrading the package updates the runtime without a279 migration through your own source.280 </p>281 <ul className="legend">282 <li>283 <span className="swatch swatch--yours" />284 <span>285 <b>Yours.</b>{" "}286 Schema, permissions, config, routes, islands, styles,287 tests.288 </span>289 </li>290 <li>291 <span className="swatch swatch--theirs" />292 <span>293 <b>lofi's.</b>{" "}294 Storage, identity, sync, lifecycle, and PWA plumbing —295 imported from one pinned version.296 </span>297 </li>298 <li>299 <span className="swatch swatch--gen" />300 <span>301 <b>Generated.</b>{" "}302 Regenerated on demand and git-ignored. Never edit by hand.303 </span>304 </li>305 </ul>306 <p className="anat-more">307 <Link to="/docs/reference/project-layout">308 See the exact generated-project map →309 </Link>310 </p>311 </div>312 </div>313 </section>314 </div>315316 {/* ===================== STACK + LIMITS ===================== */}317 <div className="wrap" id="stack">318 <section className="band band--rule">319 <div className="two-up">320 <div>321 <div className="band-head band-head--tight">322 <p className="eyebrow">stack and version policy</p>323 <h2>Pinned on purpose.</h2>324 </div>325 <p className="dim intro-note">326 The data layer is an alpha, so every upgrade is reviewed and327 validated before it reaches a generated project.328 </p>329 <table className="tbl">330 <thead>331 <tr>332 <th>Layer</th>333 <th>Choice</th>334 <th>Pinned at</th>335 </tr>336 </thead>337 <tbody>338 <tr>339 <td>Data / sync</td>340 <td>Jazz 2, CRDTs, OPFS</td>341 <td className="pin">2.0.0-alpha.53</td>342 </tr>343 <tr>344 <td>UI runtime</td>345 <td>Preact islands, thin adapter</td>346 <td className="pin">Preact 10 / Astro 7</td>347 </tr>348 <tr>349 <td>Shell</td>350 <td>Prerendered Astro, static host</td>351 <td className="pin">Astro 7</td>352 </tr>353 <tr>354 <td>Toolchain</td>355 <td>Deno tasks, npm compat</td>356 <td className="pin">Deno 2.9</td>357 </tr>358 </tbody>359 </table>360 </div>361 <div>362 <div className="band-head band-head--tight">363 <h2>What it isn't, yet.</h2>364 </div>365 <p className="dim intro-note">366 Built for mobile web apps where offline is a requirement, not367 a best-effort enhancement. Before you commit:368 </p>369 <ul className="limits">370 <li>371 <b>It is an early alpha release.</b>{" "}372 Expect the surface to move, and do not yet trust it with373 data you cannot afford to lose.374 </li>375 <li>376 <b>The data layer is Jazz 2 alpha</b>, deliberately pinned377 to a reviewed version. Known engine defects at the current378 pin are documented in decision records and pinned by379 automated canaries that fail loudly when a version bump380 changes the behavior.381 </li>382 <li>383 <b>Sealing has limits.</b>{" "}384 Private and shared columns are encrypted on the client385 before they sync, but structure, timing, and presence stay386 visible, and sealing protects future content, not merged387 history. <Link to="/docs/threat-model">The threat model</Link>{" "}388 states exactly what the server can and cannot see.389 </li>390 <li>391 <b>Browser floors are real.</b>{" "}392 Android Chrome 148+ and iOS Safari 16.4+.393 </li>394 <li>395 <b>Recovery is user-controlled.</b>{" "}396 lofi keeps no recoverable account material on the server, so397 the phrase has to be kept safe.398 </li>399 </ul>400 </div>401 </div>402 </section>403 </div>404405 {/* ===================== COMMANDS ===================== */}406 <div className="wrap">407 <section className="band band--rule">408 <div className="band-head">409 <h2>Every project, a unified toolset.</h2>410 <p className="dim intro-note">411 Every generated project exposes these as{" "}412 <code className="inline-mono">deno task &lt;name&gt;</code>.413 </p>414 </div>415 <div className="two-up">416 <table className="tbl">417 <caption>Everyday development</caption>418 <tbody>419 <tr>420 <td>421 <code>dev</code>422 </td>423 <td>Runs the Astro dev server and prints runtime state.</td>424 </tr>425 <tr>426 <td>427 <code>doctor</code>428 </td>429 <td>430 Checks readiness without printing config or secrets.431 </td>432 </tr>433 <tr>434 <td>435 <code>test</code>436 </td>437 <td>Runs the deterministic local-first suite.</td>438 </tr>439 <tr>440 <td>441 <code>build</code>442 </td>443 <td>444 Creates a static production build in <code>dist/</code>.445 </td>446 </tr>447 <tr>448 <td>449 <code>preview</code>450 </td>451 <td>Serves the production build locally.</td>452 </tr>453 </tbody>454 </table>455 <table className="tbl">456 <caption>Sync, schema, deployment</caption>457 <tbody>458 <tr>459 <td>460 <code>jazz:provision</code>461 </td>462 <td>463 Creates a managed Jazz app and configures{" "}464 <code>.env</code>.465 </td>466 </tr>467 <tr>468 <td>469 <code>schema:validate</code>470 <br />471 <code>schema:deploy</code>472 </td>473 <td>Validates and publishes the Jazz schema.</td>474 </tr>475 <tr>476 <td>477 <code>migrations:create</code>478 <br />479 <code>migrations:push</code>480 </td>481 <td>Authors and pushes schema migrations.</td>482 </tr>483 <tr>484 <td>485 <code>deploy</code>486 <br />487 <code>deploy:create</code>488 </td>489 <td>Hosts the static build on Deno Deploy.</td>490 </tr>491 </tbody>492 </table>493 </div>494 </section>495 </div>496 </main>497 </Layout>498 );499}