@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
PACKAGE_NAME — agent notes
Scaffolded from @kentcdodds/package-app-kit/starter-tanstack. Human Intent lives in README.
- Handoff: open private hosted apps with Open Package App from kody.codes; re-open from the package page when you need a fresh session or service worker.
After scaffolding
After create-package-app-tanstack lands this tree (or you copy starter-tanstack/), work through this checklist before treating the app as product-ready:
- List mark (TanStack default included) — This scaffold ships the square TanStack koala mark at
.kody/icon.png/.kody/icon.svg(SVG wraps the PNG for text-onlypackageSave). Keep it unless the product needs its own brand — then replace with a distinctive square mark (do not reuse the kit demo race-driver). Prefer PNG on the Artifacts git lane. This is package / repo / community list identity. - PWA + in-app chrome marks (TanStack default included) — Favicon / header / PWA icons already default to the same TanStack koala via
src/icon-png-bytes.tsand/icons/icon-192.png+/icons/icon-512.pngroutes (git lane also has matchingicons/*.png). Replace those bytes/files only when product-branding. Serve from app routes and point favicon / header<img>atroutes.…href(). For a hero or alternate bust, add the same kind of route (for example/brand/logo-full.png). Keep list identity in.kody/; keeppublic/for CSS,sw.js, and other asset-host files. - Fill product identity — Set
__APP_TITLE__/ title, description, andpackageName(and related placeholders) to the real product names. - Keep local version writers — Use this package's
src/version.ts+src/record-version.tsagainst this app'spackageStorage. Call this package's./record-version(kody:__PACKAGE_NAME__/record-version), not the kit's export. - Record each publish — After every successful publish, call this package's
./record-versionwith the published SHA (and message /committedAtwhen available). - TanStack recipe (#2312) — Confirm root
tsconfig.jsonhas"jsx": "react-jsx"+"jsxImportSource": "react";app/router.tsremounts the Request andexport default { fetch };app/routeTree.tsxbuilds@tanstack/react-routerroutes; SSR usesreact-dom/server.edgerenderToReadableStreaminapp/ssr.tsx(no TanStack Start / Vite); clientapp/assets/entry.tscallshydrateRoot+RouterProvider. - Match manifest colors to the mark — Set
themeColor/backgroundColorinbuildWebManifest(seeapp/controllers/manifest.ts) and the documenttheme-colormeta so they harmonize with the mark. - Optional UI icons — Add Lucide glyphs via
node scripts/add-lucide-icon.mjs <names>intoapp/icons/when the UI needs icon-only or labeled controls.
Then smoke with packageAppFetch (paths in Smoke below).
Kody CLI workflow
This package ships npm scripts around the Kody CLI (@kodycodes/cli, pinned in devDependencies, so run npm install once). Each script is a thin wrapper; the raw command is listed so agents without npm (or with only MCP) can follow along.
| Step | Script | Raw command |
|---|---|---|
| Sign in + git access (humans) | npm run setup | kody login && kody auth setup-git |
| Pull | — | git clone https://kody.codes/__PACKAGE_NAME__.git then git pull |
| Run locally (mock) | npm run dev | kody dev --target mock |
| Run against real data | npm run dev:prod | kody dev --target prod |
| Fetch one path | npm run fetch -- /health | kody dev fetch /health |
| Run a job once | npm run job -- <name> | kody dev job <name> |
| Send a webhook | npm run webhook -- <name> --body fixtures/webhook.example.json | kody dev webhook <name> --body <file> |
| Emit an event | npm run emit -- <event> --body fixtures/event.example.json | kody dev emit <event> --body <file> |
| Bundle + publish checks | npm run check | kody dev check |
| Types | npm run typecheck | tsc --noEmit |
| Tests | npm test | node --test (smoke tests call kody dev fetch on mock) |
| Push | — | git push |
| Publish | npm run publish:kody (add -- <package_id> if your CLI rejects the name) | tests, node scripts/prepublish-check.mjs, then kody package publish <package_id or __PACKAGE_NAME__> |
Loop: pull, edit, npm run dev (or npm run fetch -- <path>), npm test, commit, git push, npm run publish:kody. Add --dry-run to job / webhook / emit to see what would run without side effects. Mock is the default target: scratch storage and fake credentials, so unpublished code never touches real data. Use dev:prod only when you mean to.
Signing in (explicit token lifetimes)
- Humans:
npm run setup(browserkody login, thenkody auth setup-git, which defaults to--lifetime long). - Agents on a task: get a one-shot
kody_bc_…code from MCPcliCredentialBootstrap, thennpx kody auth bootstrap --code <code> --lifetime short(1h idle / 24h max). Only use--lifetime long(14d idle / 3mo max) for a long-running agent the owner set up on purpose. Thennpx kody auth setup-git --lifetime shortfor git access. - Always pass
--lifetimeexplicitly. Never print, log, commit, or paste tokens. Do not callkody api cliCredentialBootstrapRedeem(it prints the token).kody status/kody whoamiconfirm sign-in without showing it. - Package code reads Kody credentials through the runtime, never from files in this repo.
scripts/prepublish-check.mjsrefuses to publish if a tracked file looks like it contains one.
Without a shell (MCP-only agents)
Scripts are optional. The same loop works through Kody MCP tools: edit files with packageSave (or repo sessions), publish with packagePublishExternalPush, and smoke with packageAppFetch (see Smoke below). Before publishing, re-read After scaffolding and confirm README/AGENTS still describe the app.
TanStack recipe (framework-agnostic host)
"kody": {
"app": {
"entry": "./app/router.ts",
"client": "./app/assets/entry.ts",
"assets": "./public"
}
}- tsconfig — root
tsconfig.jsonwith"jsx": "react-jsx"and"jsxImportSource": "react". - Router
app/router.ts— remount the Request forappBasePath, thenexport default { fetch(request) { … } }dispatching API routes +renderAppSSR. - Route tree
app/routeTree.tsx—@tanstack/react-routercreateRootRouteWithContext+ page routes;basepathmatches the mount. - SSR
app/ssr.tsx—createMemoryHistory+renderToReadableStream(<RouterProvider />)(Workers-safe; not TanStack Start). - Routes helpers
app/routes.ts—createRoutes(appBasePath)for hrefs (nokody:runtimein this module). - Client
app/assets/entry.ts—hydrateRoot(document, <RouterProvider />)+ SW register. - Assets
public/—styles.css,sw.js(reads__version.json; no content hash in source).
Imports — React + TanStack Router (no Vite / Start)
import {
createRouter,
createRootRouteWithContext,
createRoute,
createMemoryHistory,
createBrowserHistory,
RouterProvider,
Link,
} from '@tanstack/react-router'
import { renderToReadableStream } from 'react-dom/server.edge'
import { hydrateRoot } from 'react-dom/client'
import { packageContext, packageStorage } from 'kody:runtime'Forbid:
- TanStack Start / Vite / Pitlane tooling inside the Worker publish
remix/componentJSX pragmas (this starter is standard React)- Importing
kody:/kody:@…from the browser client graph
Kit (Worker / SSR only)
import { formatAboutPageData } from 'kody:@kentcdodds/package-app-kit/about'
import { buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'
import { starterManifestIcons, iconPngResponse } from '../src/icon-png-bytes.ts'Version metadata: use this package's local ./record-version (src/record-version.ts → this app's packageStorage). Do not re-export or call kody:@kentcdodds/package-app-kit/record-version from execute — that writes the kit's storage and fails provenance. About / /api/version import readRecordedVersion relatively from ../../src/record-version.ts.
Browser islands must not import kody: / kody:@….
List / identity mark
Put the package list mark at .kody/icon.png (also .svg / .webp / .jpg / .jpeg). This scaffold includes the TanStack koala mark (.kody/icon.png + .kody/icon.svg base64 wrapper) — keep it unless replacing with the product's official logo (prefer PNG on the git lane). Do not add root community-icon.*. Do not move PWA icons (icons/icon-192.png, routes served by src/icon-png-bytes.ts) into .kody/.
PWA / SW
Register ${assetBasePath}/sw.js with { scope: ${appBasePath}/ }. Precache via GET …/_assets/__version.json → clientModuleUrl. Icons via starter src/icon-png-bytes.ts routes (192 + 512; TanStack koala default). Manifest crossorigin="use-credentials".
Optional L3 — Realtime
Package apps can add live sync with kit ./realtime + ./realtime-client (see kit AGENTS L3 — Realtime). Typical recipe: export handleRealtimeEvent from app/router.ts, broadcast after durable writes via KodyRuntime.realtime, connect from a browser island with connectPackageRealtime. Do not wire WebSocket into every scaffold by default — adopt when the product needs multi-session notify (Notes-style).
Smoke
import { kody } from 'kody:runtime'
export default async function main() {
const paths = ['/', '/notes', '/about', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png', '/health']
const out = []
for (const path of paths) {
const res = await kody.packageAppFetch({ kody_id: '__PACKAGE_ID__', path })
out.push({ path, status: res.status, type: res.headers?.['content-type'] || res.headers?.['Content-Type'] })
}
return out
}Prefer zero content layout shift (CLS)
Keep document flow stable. Update-available, toasts, and other post-hydration chrome use fixed overlays (see UpdateBanner / .pak-update-toast). Do not inject in-flow banners that push content down. Reserve height for async UI; hide install chrome in display-mode: standalone.
Tooltips (data-tip)
Tip when the tip adds information: icon-only install in the header, truncated SHA, ISO behind local time. Labeled nav and action buttons rely on their visible words. Color swatches use visible text labels.
Install header icon
Place InstallCta in the header brand row to the right of the title. Keep a reserved same-size slot (pak-install-slot) so BIP reveal does not shift layout. No large in-flow Install button.
Optimistic UI + press feedback
Immediate data-pending / aria-busy on press. Use app/ui/busy.ts (createBusyGate) for spin-delay timing: delay busy labels ~200ms, keep them ≥400ms once shown, so fast ops do not flash "Loading…".
Lucide icons
# From the kit repo while editing the starter twin, or copy script into the app:
node scripts/add-lucide-icon.mjs download trash-2 plusImport generated React SVG components from app/icons/{name}.tsx. Icon-only controls get data-tip + aria-label; labeled buttons keep visible text.
packageStorage migrations
import { packageStorage } from 'kody:runtime'
import {
createMigrationRunner,
} from 'kody:@kentcdodds/package-app-kit/storage-migrations'
const ensureSchema = createMigrationRunner({
storage: packageStorage(),
versionKey: '__PACKAGE_ID__-schema-version',
migrations: [/* { version, name, up } */],
})
await ensureSchema()See app/data/notes.ts for a copy-paste example.