ugly-app 0.1.923 → 0.1.925

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +216 -223
  2. package/dist/cli/version.d.ts +1 -1
  3. package/dist/cli/version.js +1 -1
  4. package/dist/client/bootstrapApp.d.ts.map +1 -1
  5. package/dist/client/bootstrapApp.js +21 -4
  6. package/dist/client/bootstrapApp.js.map +1 -1
  7. package/dist/client/createSocket.d.ts +24 -0
  8. package/dist/client/createSocket.d.ts.map +1 -1
  9. package/dist/client/createSocket.js +46 -2
  10. package/dist/client/createSocket.js.map +1 -1
  11. package/dist/client/refreshWebPush.d.ts.map +1 -1
  12. package/dist/client/refreshWebPush.js +4 -0
  13. package/dist/client/refreshWebPush.js.map +1 -1
  14. package/dist/client/webPush.d.ts +33 -0
  15. package/dist/client/webPush.d.ts.map +1 -1
  16. package/dist/client/webPush.js +95 -7
  17. package/dist/client/webPush.js.map +1 -1
  18. package/dist/conversation/server/Conversation.d.ts.map +1 -1
  19. package/dist/conversation/server/Conversation.js +9 -0
  20. package/dist/conversation/server/Conversation.js.map +1 -1
  21. package/dist/inspect/host-bundle.js +8 -8
  22. package/dist/markdown/client/MdastViewer.d.ts.map +1 -1
  23. package/dist/markdown/client/MdastViewer.js +63 -2
  24. package/dist/markdown/client/MdastViewer.js.map +1 -1
  25. package/dist/native/wrapper.d.ts +5 -0
  26. package/dist/native/wrapper.d.ts.map +1 -1
  27. package/dist/native/wrapper.js +6 -0
  28. package/dist/native/wrapper.js.map +1 -1
  29. package/dist/server/adapter/workers/createWorkersApp.d.ts +25 -0
  30. package/dist/server/adapter/workers/createWorkersApp.d.ts.map +1 -1
  31. package/dist/server/adapter/workers/createWorkersApp.js +53 -1
  32. package/dist/server/adapter/workers/createWorkersApp.js.map +1 -1
  33. package/dist/server/index.d.ts +4 -0
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/index.js +8 -0
  36. package/dist/server/index.js.map +1 -1
  37. package/dist/server/sqlite/SqliteMigrations.d.ts +73 -0
  38. package/dist/server/sqlite/SqliteMigrations.d.ts.map +1 -0
  39. package/dist/server/sqlite/SqliteMigrations.js +55 -0
  40. package/dist/server/sqlite/SqliteMigrations.js.map +1 -0
  41. package/package.json +1 -1
  42. package/src/cli/version.ts +1 -1
  43. package/src/client/bootstrapApp.tsx +26 -7
  44. package/src/client/createSocket.test.ts +28 -0
  45. package/src/client/createSocket.ts +56 -6
  46. package/src/client/refreshWebPush.ts +3 -0
  47. package/src/client/webPush.test.ts +87 -0
  48. package/src/client/webPush.ts +117 -7
  49. package/src/conversation/server/Conversation.ts +9 -0
  50. package/src/markdown/client/MdastViewer.tsx +69 -1
  51. package/src/native/wrapper.ts +6 -0
  52. package/src/server/adapter/workers/createWorkersApp.ts +81 -1
  53. package/src/server/index.ts +17 -0
  54. package/src/server/sqlite/SqliteMigrations.test.ts +108 -0
  55. package/src/server/sqlite/SqliteMigrations.ts +130 -0
package/README.md CHANGED
@@ -1,30 +1,15 @@
1
1
  # ugly-app
2
2
 
3
- A full-stack TypeScript framework for shipping production web apps. Scaffold
4
- with `npx ugly-app init my-app` and get an opinionated Node + React + Postgres
5
- stack with type-safe RPC over WebSocket and HTTP, real-time doc subscriptions,
6
- built-in auth, AI generation, storage, workers, cron, and a CLI for every
7
- workflow.
3
+ A full-stack TypeScript framework for shipping production web apps.
4
+ Scaffold with `npx ugly-app init my-app` and get an opinionated Node +
5
+ React + Postgres stack with type-safe RPC over WebSocket and HTTP,
6
+ real-time doc subscriptions, built-in auth, AI generation, storage,
7
+ workers, cron, and a CLI for every workflow.
8
8
 
9
9
  ugly-app is designed to run against [ugly.bot](https://ugly.bot), which
10
- provides auth, infra (Postgres, Qdrant, NATS, S3-compatible object storage),
11
- AI provider keys, push, email, and deployment. Your app talks to all of it
12
- through the project's dev tunnel and its `UGLY_BOT_TOKEN`.
13
-
14
- ## What's included
15
-
16
- - **Server** — Express + WebSocket with typed RPC and Zod validation.
17
- - **Client** — React 19 + Vite with typed routing, lazy pages, animated transitions, and a router-owned popup layer.
18
- - **Database** — `TypedDB` over Postgres (JSONB) or Cloudflare D1, with full-text and vector search.
19
- - **Auth** — HttpOnly cookies + JWT. ugly.bot OAuth by default; magic-link / Google in `mode: 'self'`.
20
- - **AI** — Text, JSON, image, embeddings, web search — proxied through ugly.bot.
21
- - **Realtime** — NATS pub/sub and doc change subscriptions (`trackDoc` / `trackDocs`).
22
- - **Storage** — S3-compatible buckets with presigned uploads.
23
- - **Workers & cron** — `setWorkers()` registers named async tasks with optional Zod schemas and cron schedules.
24
- - **Two-adapter deploy** — Same source runs on Node (Express) or Cloudflare Workers (Hono + Durable Objects).
25
- - **CLI** — `ugly-app` commands for dev, build, deploy, migrations, logs, AI, and auth.
26
-
27
- ## Quick start
10
+ provides auth, infra (Postgres, Qdrant, NATS, S3-compatible object
11
+ storage), AI provider keys, push, email, and deployment. Your app talks
12
+ to all of it through the project's dev tunnel and its `UGLY_BOT_TOKEN`.
28
13
 
29
14
  ```bash
30
15
  npx ugly-app init my-app
@@ -32,16 +17,35 @@ cd my-app
32
17
  npm run dev
33
18
  ```
34
19
 
35
- The scaffold gives you a working app at `http://localhost:4321` with todo CRUD,
36
- AI chat, file upload, auth demo, collab editing, and other test pages already
37
- wired up.
20
+ ## Package entry points
21
+
22
+ | Import path | Contents |
23
+ |-------------|----------|
24
+ | `ugly-app` | Server: `createApp`, `TypedDB`, auth, AI clients, NATS, storage, email, push, workers. |
25
+ | `ugly-app/shared` | Cross-tier: `defineRequests`, `defineCollections`, `definePage`, `defineWorkers`, Zod, experiments, time constants. |
26
+ | `ugly-app/client` | React: `bootstrapApp`, `createRouter`, `lazyPage`, `AppProvider`, components, animations, audio, AI helpers. |
27
+ | `ugly-app/server/adapter/workers` | Cloudflare Workers entry (Hono router + Durable Objects + Neon HTTP driver + R2). |
28
+ | `ugly-app/server/ssr` | SSR renderer used by the Workers adapter. |
29
+ | `ugly-app/conversation/{shared,server,client,engine}` | AI chat sessions with persisted history. |
30
+ | `ugly-app/collab/{server,client}` | Yjs-based collaborative editing. |
31
+ | `ugly-app/markdown/{shared,client}` | Markdown rendering + editor. |
32
+ | `ugly-app/search/{shared,server}` | Search primitives. |
33
+ | `ugly-app/agent/{shared,server,client}` | Agent-mode SSE + tool orchestration. |
34
+ | `ugly-app/webrtc`, `ugly-app/webrtc/server` | WebRTC video rooms. |
35
+ | `ugly-app/three/{server,client}` | Three.js scene helpers. |
36
+ | `ugly-app/native`, `ugly-app/native/server` | Native task host + conformance harness. |
37
+ | `ugly-app/inspect`, `ugly-app/inspect/agent`, `ugly-app/inspect/ui` | UX inspection agent + host bundle. |
38
+ | `ugly-app/worker` | Worker queue runtime. |
39
+ | `ugly-app/playwright` | E2E test utilities (`inspectWindow`, `expectClean`, `setDevice`, …). |
40
+ | `ugly-app/testing` | Test-mode helpers. |
41
+ | `ugly-app/vite`, `ugly-app/eslint` | Build-tool plugins. |
38
42
 
39
43
  ---
40
44
 
41
45
  ## Server — `createApp()`
42
46
 
43
- Single server entry point. Returns an `App` that owns Express, the WebSocket
44
- server, the typed DB, and the RPC dispatcher.
47
+ Single server entry point. Returns an `App` that owns Express, the
48
+ WebSocket server, the typed DB, and the RPC dispatcher.
45
49
 
46
50
  ```ts
47
51
  // server/index.ts
@@ -74,7 +78,7 @@ const app = createApp(
74
78
  defaultLang: stringsDef.defaultLang,
75
79
  langs: stringsDef.langs,
76
80
  criticalKeys: stringsDef.criticalKeys,
77
- getTable: (lang) => ({ en, es } as Record<string, unknown>)[lang] ?? en,
81
+ getTable: (lang) => (lang === 'es' ? es : en),
78
82
  });
79
83
  configurator.setWorkers(cronTasks, cronHandlers);
80
84
  configurator.setOnEmail(async (inbound: InboundEmail) => { /* … */ });
@@ -99,10 +103,10 @@ function createApp<
99
103
  ): App<CollectionMap<typeof BUILTIN_DEFS & Defs>, RegistryPages<R>>;
100
104
  ```
101
105
 
102
- Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades
103
- `app.pushSend()` to a per-route typed API. `pages` is optional; apps that only
104
- call `configurator.setPages()` still work — they just get the loose
105
- `PageRegistry` typing on `pushSend`.
106
+ Passing `pages` in the registry (`{ requests, messages, pages }`)
107
+ upgrades `app.pushSend()` to a per-route typed API. `pages` is optional;
108
+ apps that only call `configurator.setPages()` still work — they just get
109
+ the loose `PageRegistry` typing on `pushSend`.
106
110
 
107
111
  ### The returned `App`
108
112
 
@@ -114,21 +118,21 @@ call `configurator.setPages()` still work — they just get the loose
114
118
  | `wss` | The main `WebSocketServer` (path set via `setWsPath`, default `/rpc`). |
115
119
  | `dispatch(name, input, userId)` | Invoke a registered RPC handler programmatically (inherited from `AppRouter`). |
116
120
  | `registerRoutes(fn)` | Mount additional Express routes after creation. |
117
- | `pushSend(input)` | Send a push whose click-through target is a route from this app's `pages`. `page` and `query` are type-checked when the registry carries `pages`; the framework mints a `https://ugly.bot/l/<code>` short link so the click-through is always absolute and dock-app-routable. |
121
+ | `pushSend(input)` | Typed push whose click-through target is a route from this app's `pages`. The framework mints a `https://ugly.bot/l/<code>` short link so the click-through is always absolute and dock-app-routable. |
118
122
 
119
- Framework services start automatically inside `app.start()`: schema drift
120
- check, NATS connection + KV buckets, data-proxy connection, event-counter
121
- flush, TTL cleanup for log tables, console/error capture, and ugly.bot log
122
- forwarding. Postgres, NATS, storage, and AI clients are loaded **lazily** — a
123
- host without `DATABASE_URL` / `NATS_URL` / `R2_BUCKET` doesn't pay their
124
- startup cost.
123
+ Framework services start automatically inside `app.start()`: schema
124
+ drift check, NATS connection + KV buckets, data-proxy connection,
125
+ event-counter flush, TTL cleanup for log tables, console/error capture,
126
+ and ugly.bot log forwarding. Postgres, NATS, storage, and AI clients
127
+ are loaded **lazily** — a host without `DATABASE_URL` / `NATS_URL` /
128
+ `R2_BUCKET` doesn't pay their startup cost.
125
129
 
126
130
  ### `AppConfigurator`
127
131
 
128
132
  Every method is optional; `setPages` is what mounts the SPA.
129
133
 
130
- | Method | Description |
131
- |--------|-------------|
134
+ | Method | Purpose |
135
+ |--------|---------|
132
136
  | `setPages({ pages, renderPage?, clientDistPath? })` | Mount the SPA. Dev runs Vite in middleware mode; prod serves `clientDistPath` (default `dist/client`). Provide `renderPage(routeName, params) => Promise<string>` to SSR any `ssr: true` pages. |
133
137
  | `setUserHelper(helper)` | A `UserHelper<UserBase>` the framework uses to look up / create users on WebSocket auth. Falls back to a minimal shim if unset. |
134
138
  | `setOnUserCreate(handler)` | `(userId, { email?, phone? }, db) => Promise<void>` — called on first login; create the user record. |
@@ -138,11 +142,11 @@ Every method is optional; `setPages` is what mounts the SPA.
138
142
  | `setWsPath(path)` | Override the WebSocket path (default `/rpc`). |
139
143
  | `setOnWsAuth(handler)` | `(ws, userId, req) => void` — fires after a socket session authenticates. |
140
144
  | `setOnAfterStart(handler)` | `(db) => Promise<void>` — called once after data-proxy + NATS are ready. |
141
- | `setOnMinuteTick(fn)` / `setOnHourlyTick(fn)` | Framework-managed periodic callbacks. Fire only when `CLOCK_ENABLED=true`. |
145
+ | `setOnMinuteTick(fn)` / `setOnHourlyTick(fn)` | Framework-managed periodic callbacks. Fire only when `CLOCK_ENABLED=true`. The hourly handler receives `(now, currentHour)`. |
142
146
  | `setHealthHandler(fn)` | Override the default `GET /health`. |
143
147
  | `setExperiments(experiments)` | Register `Experiment` definitions for `initSession` / `captureEvent` bucketing. |
144
148
  | `setIsAdmin(fn)` | `(userId, db) => boolean \| Promise<boolean>` — gate for admin-only framework requests. Defaults to matching `MAINTAIN_BOT_USER_ID`. |
145
- | `setZeroTestUserBalance(fn)` | `(userId) => Promise<void>` — how to zero a synthetic test user's balance so the out-of-credit path can be exercised. Only apps that own the balance ledger (i.e. ugly.bot itself) should register this. |
149
+ | `setZeroTestUserBalance(fn)` | How to zero a synthetic test user's balance so the out-of-credit path can be exercised. Only apps that own the balance ledger (i.e. ugly.bot itself) should register this. |
146
150
  | `setOnEmail(handler)` | Handle inbound emails routed to `{domain}@ugly.bot` (delivered as internal HTTP). |
147
151
  | `setCronTasks(tasks, handlers)` | **Deprecated.** Legacy cron-only registry — prefer `setWorkers()`. |
148
152
  | `setWorkers(workers, handlers)` | Register named async tasks with optional Zod input schema and cron schedule. Powers `GET /_workers/manifest`, `POST /_workers/run`, and the cron orchestrator. Scheduled workers are mirrored into the cron registry automatically. |
@@ -152,8 +156,8 @@ Every method is optional; `setPages` is what mounts the SPA.
152
156
 
153
157
  ### Handler signatures
154
158
 
155
- Handlers are plain async functions — no context object. Access state via
156
- captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, etc.).
159
+ Handlers are plain async functions — no context object. Access state
160
+ via captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, …).
157
161
 
158
162
  ```ts
159
163
  // req() — public, userId may be null
@@ -165,9 +169,9 @@ getMe: async (userId: string, input) => { /* … */ }
165
169
 
166
170
  ### Built-in framework requests
167
171
 
168
- `createApp` registers several framework handlers reachable from any client via
169
- the normal RPC pipeline. App-provided handlers with the same name override the
170
- framework's defaults.
172
+ `createApp` registers several framework handlers reachable from any
173
+ client via the normal RPC pipeline. App-provided handlers with the same
174
+ name override the framework's defaults.
171
175
 
172
176
  | Name | Purpose |
173
177
  |------|---------|
@@ -176,19 +180,19 @@ framework's defaults.
176
180
  | `textGen` / `imageGen` | AI proxies — server-validated, billed through ugly.bot. |
177
181
  | `kagiSearch` / `kagiSummarize` / `kagiEnrichWeb` / `kagiEnrichNews` | Web search via ugly.bot. |
178
182
  | `uploadUrl` | Issues a presigned PUT for the `temp` bucket. |
179
- | `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata (see the "sharing links" rule in `CLAUDE.md`). |
180
- | `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` / `perfSnapshotCaptureNoAuth` | Same-origin, public endpoints used by browser telemetry to write into the project's Postgres. |
183
+ | `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata. |
184
+ | `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` / `perfSnapshotCaptureNoAuth` | Same-origin, public endpoints used by browser telemetry. |
181
185
  | `submitFeedbackBot` / `feedbackReportResolve` | Bot-persona feedback submission; admin resolve/decline. |
182
186
  | `adminGetPerfLogs` | Admin-only perf telemetry read. |
183
- | `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()` (or `MAINTAIN_BOT_USER_ID`). |
187
+ | `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()`. |
184
188
  | `projectPlanList` / `projectPlanCreate` / `projectPlanUpdate` / `projectPlanDelete` | Project plan CRUD used by Studio. |
185
189
 
186
190
  ---
187
191
 
188
192
  ## Shared API definitions
189
193
 
190
- `shared/` is consumed by both server and client. Keep all Zod schemas, types,
191
- collections, and route declarations here.
194
+ `shared/` is consumed by both server and client. Keep all Zod schemas,
195
+ types, collections, and route declarations here.
192
196
 
193
197
  ### Requests (`shared/api.ts`)
194
198
 
@@ -218,8 +222,8 @@ export const requests = defineRequests({
218
222
  ```
219
223
 
220
224
  Every request is reachable as **both** `socket.request(name, input)`
221
- (WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported from
222
- Zod for convenience.
225
+ (WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported
226
+ from Zod for convenience.
223
227
 
224
228
  ### Collections (`shared/collections.ts`)
225
229
 
@@ -238,7 +242,7 @@ export const collections = defineCollections({
238
242
  todo: {
239
243
  schema: TodoSchema,
240
244
  meta: {
241
- db: neon, // REQUIRED — `neon` (Postgres) or `d1` (SQLite / Cloudflare D1)
245
+ db: neon, // REQUIRED — `neon` (Postgres) or `d1` (SQLite / D1)
242
246
  cache: true, // or { ttlMs: 60_000 }
243
247
  trackable: true,
244
248
  public: false,
@@ -249,7 +253,7 @@ export const collections = defineCollections({
249
253
  ```
250
254
 
251
255
  **`CollectionMeta`:**
252
- - `db` — **required.** `neon` (Postgres) or `d1` (Cloudflare D1 / SQLite). Both handles are exported from `ugly-app/shared`. Omitting it is a compile error and runtime throw. A `d1` collection may also declare `search` (SQLite FTS5) and/or `vector` (Cloudflare Vectorize).
256
+ - `db` — **required.** `neon` (Postgres) or `d1` (Cloudflare D1 / SQLite). Both handles are exported from `ugly-app/shared`. Omitting it is a compile error and runtime throw.
253
257
  - `cache` — `true` / `false` / `{ ttlMs }`. When enabled, `getDoc` / `getByIds` read from an LRU cache; writes invalidate it.
254
258
  - `trackable` — enables real-time `trackDoc` / `trackDocs` via change-stream NATS notifications.
255
259
  - `public` — allow unauthenticated client reads.
@@ -260,21 +264,22 @@ export const collections = defineCollections({
260
264
  - `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
261
265
  - `vector?: { dimensions, metric?, filterable? }` — ANN index. Vector supplied out-of-band at write time (`setDoc(c, doc, { vec })`) — never stored in the doc JSON. Query with `getDocs(c, filter, { near })`.
262
266
 
263
- All documents extend `DBObject`: `{ _id, version, created, updated }`. Use
264
- `dbDefaults()` to stamp `version` / `created` / `updated` on inserts.
265
- **Always generate `_id` with `nanoid()`** — never `crypto.randomUUID()`,
266
- `Date.now()`, or `Math.random()` (see `CLAUDE.md`).
267
+ All documents extend `DBObject`: `{ _id, version, created, updated }`.
268
+ Use `dbDefaults()` to stamp `version` / `created` / `updated` on
269
+ inserts. **Always generate `_id` with `nanoid()`** — never
270
+ `crypto.randomUUID()`, `Date.now()`, or `Math.random()` (see
271
+ `CLAUDE.md`).
267
272
 
268
- After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`.
269
- The app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
270
- only as a last resort).
273
+ After schema changes, run `npm run db:schema-gen` then `npm run
274
+ db:migrate`. The app refuses to start when drift is detected (set
275
+ `SCHEMA_CHECK_SKIP=true` only as a last resort).
271
276
 
272
277
  ---
273
278
 
274
279
  ## Routing
275
280
 
276
- Route definitions live in `shared/pages.ts`; the client-side router is created
277
- by `createRouter({ pages, allPages?, ssrPages? })`.
281
+ Route definitions live in `shared/pages.ts`; the client-side router is
282
+ created by `createRouter({ pages, allPages?, ssrPages? })`.
278
283
 
279
284
  ### `definePage` / `definePages` (`shared/pages.ts`)
280
285
 
@@ -282,17 +287,17 @@ by `createRouter({ pages, allPages?, ssrPages? })`.
282
287
  import { definePage, definePages } from 'ugly-app/shared';
283
288
 
284
289
  export const pages = definePages({
285
- '': definePage<{}>({ auth: false }), // /
286
- 'user/:userId': definePage<{ userId: string }>(), // /user/abc
290
+ '': definePage<{}>({ auth: false }), // /
291
+ 'user/:userId': definePage<{ userId: string }>(), // /user/abc
287
292
  'search': definePage<{ q?: string }>({ auth: false, cacheQuery: ['q'] }),
288
293
  'blog/*slug': definePage<{ slug: string }>({ ssr: true, auth: false }),
289
294
  });
290
295
  export type AppPages = typeof pages;
291
296
  ```
292
297
 
293
- `definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object
294
- carrying `PageMeta` plus a phantom `_params` used only for TypeScript
295
- inference.
298
+ `definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime
299
+ object carrying `PageMeta` plus a phantom `_params` used only for
300
+ TypeScript inference.
296
301
 
297
302
  **Options** (all optional):
298
303
 
@@ -302,11 +307,11 @@ inference.
302
307
  - `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is effectively 1 year (`DEFAULT_SSR_CACHE_TIMEOUT`) because the `buildId` is part of the cache key. Override only for pages that drift between deploys.
303
308
 
304
309
  Path syntax: `:param` matches a single path segment; `*param` is greedy
305
- (captures slashes). Query-string params are declared in `Params` but never
306
- appear in the path template.
310
+ (captures slashes). Query-string params are declared in `Params` but
311
+ never appear in the path template.
307
312
 
308
- `definePages<T>(p)` is a pass-through identity — use it to give the registry a
309
- name.
313
+ `definePages<T>(p)` is a pass-through identity — use it to give the
314
+ registry a name.
310
315
 
311
316
  ### `createRouter()` (`ugly-app/client`)
312
317
 
@@ -329,21 +334,21 @@ export const {
329
334
  - `allPages?` — the lazy route → element map (`PageMap<Pages>`). Prefer registering via `setAllPages()` from the browser entry when the app also server-renders — the Worker bundle has no code splitting, so a static import inlines every page (and its deps) into `worker.js`.
330
335
  - `ssrPages?` — `Partial<{ [K]: ComponentType<Params> }>` of statically-imported components for `ssr: true` routes. Used by both the Worker's `renderToString` and the client's first hydration render so the initial paint resolves synchronously and never mismatches the server output.
331
336
 
332
- **`createRouter` returns:**
337
+ **Returns:**
333
338
 
334
339
  - **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated?`, `initialUrl?` (`{ pathname, search }`, required for SSR + hydration). Manages route state, browser history, and the popup layer. Runs the per-route auth check synchronously — apps cannot override the login fallback.
335
340
  - **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing?`, `transitionComponent?` (a `React.ComponentType<RouterTransitionProps>` that replaces the default `ViewFlipper`), `renderPage?(state) => ReactElement` (sync alternative to `allPages` loaders, called with `RouterStateRaw`).
336
- - **`useRouter()`** — returns the `RouterContextValue<Pages>` (typed navigation + popup API). Throws if used outside `<RouterProvider>`.
341
+ - **`useRouter()`** — returns `RouterContextValue<Pages>` (typed navigation + popup API). Throws if used outside `<RouterProvider>`.
337
342
  - **`Link`** — typed SPA link bound to this router. Renders a real `<a>` (right-click / cmd-click / SEO keep working) but intercepts a plain left-click and routes through `push` / `replace`. Props: `to`, `params` (both type-checked against `pages`), `replace?`, `children`, plus common anchor attrs (`className`, `style`, `target`, `aria-label`, `data-id`, `onClick`).
338
343
  - **`setAllPages(map)`** — register the lazy `PageMap<Pages>` after construction (typically from the browser entry, so the Worker's import graph stays free of every lazy page).
339
344
 
340
- A standalone `Link` is also exported from `ugly-app/client` for code that
341
- can't easily reach the typed one; it accepts an optional `router` prop and
342
- falls back to `<RouterProvider>` context.
345
+ A standalone `Link` is also exported from `ugly-app/client` for code
346
+ that can't easily reach the typed one; it accepts an optional `router`
347
+ prop and falls back to `<RouterProvider>` context.
343
348
 
344
- **Never use a bare `<a href="/route">`** for internal navigation — it triggers
345
- a full document reload (white flash + repaint). See the "client navigation"
346
- rule in `CLAUDE.md`.
349
+ **Never use a bare `<a href="/route">`** for internal navigation — it
350
+ triggers a full document reload (white flash + repaint). See the "client
351
+ navigation" rule in `CLAUDE.md`.
347
352
 
348
353
  ### Page map — `lazyPage` / `lazyPageLoader`
349
354
 
@@ -364,9 +369,10 @@ export const allPages = {
364
369
  - **`lazyPageLoader(factory)`** — lazy-imports an async loader `(params) => Promise<ReactElement>`. Use when a route needs data fetching before render. The loader file is the chunk boundary, so it can statically import its page component.
365
370
 
366
371
  Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch
367
- dynamically imported module"): on the first failure they trigger a single
368
- `window.location.reload()` (guarded by `sessionStorage` so a genuinely broken
369
- chunk can't loop) to fetch the fresh `index.html` with new chunk hashes.
372
+ dynamically imported module"): on the first failure they trigger a
373
+ single `window.location.reload()` (guarded by `sessionStorage` so a
374
+ genuinely broken chunk can't loop) to fetch the fresh `index.html` with
375
+ new chunk hashes.
370
376
 
371
377
  ```ts
372
378
  // pages/SlowPageLoader.tsx
@@ -393,18 +399,18 @@ const {
393
399
 
394
400
  push('user/:userId', { userId: '123' }); // → /user/123
395
401
  replace('search', { q: 'hello' }); // → /search?q=hello
396
- back(); // browser history back
402
+ back(); // browser history back
397
403
  ```
398
404
 
399
- Route names and params are fully typed against `pages`. `push` / `replace`
400
- no-op with a `console.error` when `buildUrl()` produces a URL that doesn't
401
- match a registered route.
405
+ Route names and params are fully typed against `pages`. `push` /
406
+ `replace` no-op with a `console.error` when `buildUrl()` produces a URL
407
+ that doesn't match a registered route.
402
408
 
403
409
  ### Popups — `openPopup()`
404
410
 
405
- `useRouter().openPopup()` is the canonical modal / sheet / menu API. The
406
- router owns the popup layer, drives a spring animation, and stacks popups
407
- z-index-correctly above every page.
411
+ `useRouter().openPopup()` is the canonical modal / sheet / menu API.
412
+ The router owns the popup layer, drives a spring animation, and stacks
413
+ popups z-index-correctly above every page.
408
414
 
409
415
  ```tsx
410
416
  const { openPopup } = useRouter();
@@ -416,7 +422,7 @@ const handle = openPopup(<MyContent />, {
416
422
  containerStyle: { /* CSS for the content wrapper */ },
417
423
  backgroundStyle: { /* CSS for the backdrop */ },
418
424
  animConfig: { duration: 300, easing: myEasingFn },
419
- renderLayer: (props) => <CustomLayer {...props} />, // fully replace the layer renderer
425
+ renderLayer: (props) => <CustomLayer {...props} />, // fully replace the layer
420
426
  });
421
427
 
422
428
  handle.hide(); // dismiss programmatically — same as router.closePopup(handle.id)
@@ -429,20 +435,20 @@ handle.hide(); // dismiss programmatically — same as router.closePopup(handle.
429
435
  - **`contextMenu`** — same as transient, intended for menus and pickers.
430
436
 
431
437
  `renderLayer` receives `{ content, spring, hide }`: `spring` is a
432
- `createAnimatedValue()` result driving 0 → 1; `hide` closes the popup. The
433
- default layer animates open with `easeOut` (250 ms) and closed with `easeIn`
434
- (200 ms). Popups render as siblings of the router's children (managed inside
435
- `RouterProvider`), so they stack above every page.
438
+ `createAnimatedValue()` result driving 0 → 1; `hide` closes the popup.
439
+ The default layer animates open with `easeOut` (250 ms) and closed with
440
+ `easeIn` (200 ms). Popups render as siblings of the router's children
441
+ (managed inside `RouterProvider`), so they stack above every page.
436
442
 
437
443
  ### Scroll containers
438
444
 
439
445
  `html, body, #root` are `overflow: hidden` — the document itself never
440
- scrolls, so a page that just renders tall content is clipped. For pages that
441
- own their scrolling (or render outside the normal authed chrome — e.g. a
442
- public page special-cased before the auth gate), wrap the content in
443
- `SimpleScrollView` (exported from `ugly-app/client`). For long / virtualized
444
- lists with scroll-position persistence, use the richer `ScrollView`. See the
445
- "page scrolling" rule in `CLAUDE.md`.
446
+ scrolls, so a page that just renders tall content is clipped. For pages
447
+ that own their scrolling (or render outside the normal authed chrome —
448
+ e.g. a public page special-cased before the auth gate), wrap the
449
+ content in `SimpleScrollView` (exported from `ugly-app/client`). For
450
+ long / virtualized lists with scroll-position persistence, use the
451
+ richer `ScrollView`. See the "page scrolling" rule in `CLAUDE.md`.
446
452
 
447
453
  ---
448
454
 
@@ -473,7 +479,7 @@ bootstrapApp({
473
479
  | `messages?` | Your `MessageRegistry` (merged with framework messages). |
474
480
  | `RouterProvider` | The `RouterProvider` component returned from `createRouter()`. |
475
481
  | `render` | Callback returning the app's UI tree (typically `<RouterView />`). |
476
- | `root?` | Root element / selector (default `'#root'`). |
482
+ | `root?` | Root element or CSS selector (default `'#root'`). |
477
483
  | `fallback?` | UI for unmatched routes (default: tiny "404"). |
478
484
  | `socketUrl?` | Override the WebSocket path (default `/rpc`). |
479
485
  | `strings?` | Localization config — when present, wraps the tree with `<StringsProvider>`. |
@@ -482,54 +488,57 @@ bootstrapApp({
482
488
  | `silentSso?` | Apex-domain apps that use ugly.bot as their auth authority but don't share its cookie. When `true`, a logged-out boot attempts a top-level SSO redirect (once per tab) to adopt an existing ugly.bot session. Leave unset for `*.ugly.bot` subdomains and Mode B apps. |
483
489
  | `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
484
490
 
485
- `bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after
486
- cookie verification). If absent, it renders unauthenticated immediately — the
487
- router's per-route auth guard surfaces `<AuthRoot>` only when the user opens a
488
- protected route. If the token is present, it connects the socket, mounts
489
- `<AppProvider>`, and renders.
491
+ `bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server
492
+ after cookie verification). If absent, it renders unauthenticated
493
+ immediately — the router's per-route auth guard surfaces `<AuthRoot>`
494
+ only when the user opens a protected route. If the token is present, it
495
+ connects the socket, mounts `<AppProvider>`, and renders.
490
496
 
491
- After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`:
492
- if it returns a fresh code, the cookie is refreshed via `POST /auth/verify`;
493
- if the returned account differs from the current session, the page reloads
494
- onto the live account (guarded once per from→to pair to prevent loops). Any
495
- `?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from ugly.bot)
496
- is redeemed at the top of bootstrap before anything renders.
497
+ After render, a hidden iframe silently calls
498
+ `${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie
499
+ is refreshed via `POST /auth/verify`; if the returned account differs
500
+ from the current session, the page reloads onto the live account
501
+ (guarded once per from→to pair to prevent loops). Any
502
+ `?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from
503
+ ugly.bot) is redeemed at the top of bootstrap before anything renders.
497
504
 
498
- If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders
499
- `<MagicLinkCallback>` instead of the regular app shell.
505
+ If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it
506
+ renders `<MagicLinkCallback>` instead of the regular app shell.
500
507
 
501
508
  ### `AppProvider` & `useApp()`
502
509
 
503
- `bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use
504
- `useApp()` inside any page to access the active user, socket, and app-scoped
505
- services.
510
+ `bootstrapApp` mounts `<AppProvider>` automatically after socket
511
+ connect. Use `useApp()` inside any page to access the active user,
512
+ socket, and app-scoped services.
506
513
 
507
514
  ```ts
508
515
  const {
509
516
  userId, // current user id (string)
510
517
  user, // UserBase doc
511
518
  socket, // AppSocket — typed RPC client
512
- uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS, etc.
513
- showPopup, // AppProvider-owned popup layer (see note below) — returns popup id
519
+ uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS
520
+ showPopup, // flat overlay layer (see note) — returns popup id
514
521
  hidePopup,
515
522
  hideAllPopups,
516
- runAsync, // (label, async () => {…}, options?) — shows loading overlay while pending
523
+ runAsync, // (label, async () => {…}, options?) — shows loading overlay
517
524
  splashDone, // (step: string) — mark a splash-screen step complete
518
525
  localizer, // (key, params?) => string — alias for useLocalizer()
519
526
  } = useApp();
520
527
  ```
521
528
 
522
- `useApp<TAsyncOptions>()` is generic in the async-options type so apps can
523
- pass a custom option through to a custom `loadingOverlay` element. `AppProvider`
524
- `cloneElement`s the overlay with `{ label, asyncOptions }` while `runAsync` is
525
- pending. `useAppOptional()` returns `null` outside the provider; `useLocalizer()`
526
- returns a localizer that prefers `<StringsProvider>` data, falls back to the
527
- `AppProvider` `localizer` prop, and finally to identity.
529
+ `useApp<TAsyncOptions>()` is generic in the async-options type so apps
530
+ can pass a custom option through to a custom `loadingOverlay` element.
531
+ `AppProvider` `cloneElement`s the overlay with `{ label, asyncOptions
532
+ }` while `runAsync` is pending. `useAppOptional()` returns `null`
533
+ outside the provider; `useLocalizer()` returns a localizer that prefers
534
+ `<StringsProvider>` data, falls back to the `AppProvider` `localizer`
535
+ prop, and finally to identity.
528
536
 
529
537
  Two popup APIs coexist: `useRouter().openPopup()` (spring-animated,
530
- dismissible, mode-aware — the default choice) and `useApp().showPopup()` (a
531
- flat overlay layer returning a string id, for content that must sit *outside*
532
- the router transition). Prefer the router's.
538
+ dismissible, mode-aware — **the default choice**) and
539
+ `useApp().showPopup()` (a flat overlay layer returning a string id, for
540
+ content that must sit *outside* the router transition). Prefer the
541
+ router's.
533
542
 
534
543
  ### Direct socket access
535
544
 
@@ -556,13 +565,14 @@ from `ugly-app/client`.
556
565
  ugly-app uses HttpOnly cookies and server-side JWT injection — no
557
566
  `localStorage`, no client-side token handling.
558
567
 
559
- Two modes, picked from the `auth` block in the project's `.uglyapp` config:
568
+ Two modes, picked from the `auth` block in the project's `.uglyapp`
569
+ config:
560
570
 
561
571
  - **Mode A — `mode: 'uglybot'`** (default when no `auth` block is present). Auth is delegated to ugly.bot OAuth. AI / email / push proxies bill the end user's ugly.bot credits.
562
572
  - **Mode B — `mode: 'self'`**. The app issues its own sessions via magic-link email and/or Google OAuth. AI / email / push proxies bill the developer's ugly.bot account using the project's `AI_PROXY_TOKEN`.
563
573
 
564
- `window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code
565
- (incl. `<AuthRoot>`) can branch correctly.
574
+ `window.__UGLY_APP_AUTH_MODE__` is injected into every page so client
575
+ code (incl. `<AuthRoot>`) can branch correctly.
566
576
 
567
577
  ### Mode A — ugly.bot OAuth (default)
568
578
 
@@ -574,9 +584,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
574
584
 
575
585
  ### Mode B — self-issued sessions
576
586
 
577
- When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link
578
- provider as primary; if `providers.google.clientId` is configured it adds
579
- Google as an extra provider on the same router.
587
+ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the
588
+ magic-link provider as primary; if `providers.google.clientId` is
589
+ configured it adds Google as an extra provider on the same router.
580
590
 
581
591
  - `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
582
592
  - Background ugly.bot silent SSO is a no-op.
@@ -586,9 +596,9 @@ Google as an extra provider on the same router.
586
596
 
587
597
  ### Token-in-URL embed
588
598
 
589
- Any GET request with `?token=<JWT>` will, if the token verifies, set the
590
- cookie and 302-redirect to the same URL without the token parameter — useful
591
- for embedding any page in an iframe.
599
+ Any GET request with `?token=<JWT>` will, if the token verifies, set
600
+ the cookie and 302-redirect to the same URL without the token
601
+ parameter — useful for embedding any page in an iframe.
592
602
 
593
603
  ### Built-in auth routes
594
604
 
@@ -622,8 +632,8 @@ configurator.setAuth({
622
632
  ## Database — `TypedDB`
623
633
 
624
634
  Access via `app.db`. All methods accept a `CollectionDef` (from
625
- `defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain
626
- collection name string.
635
+ `defineCollections()`) or, on the `raw*` / `getQuery*` variants, a
636
+ plain collection name string.
627
637
 
628
638
  ### Writing
629
639
 
@@ -648,10 +658,10 @@ await db.batch(async () => {
648
658
  });
649
659
  ```
650
660
 
651
- Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`.
652
- All keys are dot-notation, fully typed against the collection's schema.
653
- Partial updates go through optimistic concurrency to prevent lost updates on
654
- concurrent writers.
661
+ Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`,
662
+ `$set`. All keys are dot-notation, fully typed against the collection's
663
+ schema. Partial updates go through optimistic concurrency to prevent
664
+ lost updates on concurrent writers.
655
665
 
656
666
  ### Reading
657
667
 
@@ -685,18 +695,18 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
685
695
  await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
686
696
  ```
687
697
 
688
- Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection
689
- `onDelete` callbacks.
698
+ Pass `deleteHandlers` as the 5th argument to `createApp` to run
699
+ per-collection `onDelete` callbacks.
690
700
 
691
701
  ### Search
692
702
 
693
703
  ```ts
694
- // Full-text search — requires `search: { fields, language? }` on the collection
704
+ // Full-text — requires `search: { fields, language? }` on the collection
695
705
  const hits = await db.searchDocs(collections.note, 'react hooks', { limit: 10 });
696
706
 
697
- // Vector search — requires `vector: { dimensions }` on the collection; vector supplied at write time
707
+ // Vector — requires `vector: { dimensions }` on the collection; vector supplied at write time
698
708
  const similar = await db.vectorSearch(collections.note, embeddingVector, { limit: 10 });
699
- const vecs = await db.getVecs(collections.note, ids); // pull raw stored vectors
709
+ const vecs = await db.getVecs(collections.note, ids);
700
710
  ```
701
711
 
702
712
  ### Caching
@@ -733,11 +743,12 @@ Imports available from `ugly-app`:
733
743
 
734
744
  ---
735
745
 
736
- ## AI
746
+ ## AI providers
737
747
 
738
- AI calls are proxied through ugly.bot — your app never holds a provider key.
739
- Pass `UGLY_BOT_TOKEN` in the environment and the framework handles routing,
740
- balance tracking, retries, and per-user billing.
748
+ AI calls are proxied through ugly.bot — your app never holds a
749
+ provider key. Pass `UGLY_BOT_TOKEN` in the environment and the
750
+ framework handles routing, balance tracking, retries, and per-user
751
+ billing.
741
752
 
742
753
  ### Server-side text generation
743
754
 
@@ -759,9 +770,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
759
770
  });
760
771
  ```
761
772
 
762
- Available models are exposed via `textGenModels` / `textGenModelData` from
763
- `ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq,
764
- Fireworks, Kimi, and Kie families.
773
+ Available models are exposed via `textGenModels` / `textGenModelData`
774
+ from `ugly-app` — the platform supports Claude, GPT, Gemini, Together,
775
+ Groq, Fireworks, Kimi, and Kie families.
765
776
 
766
777
  ### Server-side image generation
767
778
 
@@ -772,14 +783,14 @@ const imageGen = createImageGen(userId);
772
783
  const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
773
784
  ```
774
785
 
775
- `imageGenModels` / `imageGenModelData` enumerate available models (Together
776
- FLUX, FAL, Google Imagen, Wavespeed, Kie Kolors).
786
+ `imageGenModels` / `imageGenModelData` enumerate available models
787
+ (Together FLUX, FAL, Google Imagen, Wavespeed, Kie Kolors).
777
788
 
778
789
  ### Embeddings
779
790
 
780
791
  ```ts
781
792
  import { createEmbeddingClient, cosineSimilarity } from 'ugly-app';
782
- const embeddings = createEmbeddingClient(); // optional providerName arg
793
+ const embeddings = createEmbeddingClient();
783
794
  const vector = await embeddings.embed('hello world');
784
795
  const sim = cosineSimilarity(vectorA, vectorB);
785
796
  ```
@@ -798,8 +809,8 @@ await search.enrichNews({ query: 'topic' });
798
809
 
799
810
  ### Client-side AI calls
800
811
 
801
- Calls from React components go through the framework RPC pipeline — no token
802
- plumbing in the browser:
812
+ Calls from React components go through the framework RPC pipeline — no
813
+ token plumbing in the browser:
803
814
 
804
815
  ```ts
805
816
  import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
@@ -811,8 +822,9 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
811
822
 
812
823
  ### STT / TTS
813
824
 
814
- Speech goes **directly** from the browser to ugly.bot — never proxied through
815
- your app server (see the "STT/TTS routes to ugly.bot" rule in `CLAUDE.md`).
825
+ Speech goes **directly** from the browser to ugly.bot — never proxied
826
+ through your app server (see the "STT/TTS routes to ugly.bot" rule in
827
+ `CLAUDE.md`).
816
828
 
817
829
  ```ts
818
830
  import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
@@ -846,15 +858,17 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
846
858
  await storage.delete('public', destKey);
847
859
  ```
848
860
 
849
- Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL
850
- via the built-in `uploadUrl` framework request and streams the upload. In dev,
851
- uploads go through a same-origin `/_s3` proxy to avoid CORS with local MinIO.
861
+ Client-side, use `socket.uploadFile(file, key)` — it requests a
862
+ presigned URL via the built-in `uploadUrl` framework request and
863
+ streams the upload. In dev, uploads go through a same-origin `/_s3`
864
+ proxy to avoid CORS with local MinIO.
852
865
 
853
- `STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
854
- isolation.
866
+ `STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for
867
+ per-environment isolation.
855
868
 
856
- On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs
857
- are not exposed (browser uploads must go through a Worker endpoint).
869
+ On Cloudflare Workers, storage uses the R2 binding directly; presigned
870
+ PUTs are not exposed (browser uploads must go through a Worker
871
+ endpoint).
858
872
 
859
873
  ---
860
874
 
@@ -886,15 +900,16 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
886
900
  configurator.setWorkers(cronTasks, cronHandlers);
887
901
  ```
888
902
 
889
- Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
890
- `description`. Workers without a schedule are still invocable via
891
- `POST /_workers/run` (auth: localhost in dev,
892
- `Authorization: Bearer $CRON_SECRET` in prod). Scheduled workers also appear
893
- in `/_cron/manifest` for the deploy orchestrator.
903
+ Each worker can have `inputSchema`, `outputSchema`, `schedule`,
904
+ `timeout`, `description`. Workers without a schedule are still
905
+ invocable via `POST /_workers/run` (auth: localhost in dev,
906
+ `Authorization: Bearer $CRON_SECRET` in prod). Scheduled workers also
907
+ appear in `/_cron/manifest` for the deploy orchestrator.
894
908
 
895
- On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
896
- Triggers and durable queueing goes through Cloudflare Queues. On the Node
897
- adapter, `enqueueWorker` runs the handler inline (in-process queues only).
909
+ On the Workers adapter, scheduled workers dispatch through Cloudflare
910
+ Cron Triggers and durable queueing goes through Cloudflare Queues. On
911
+ the Node adapter, `enqueueWorker` runs the handler inline (in-process
912
+ queues only).
898
913
 
899
914
  ---
900
915
 
@@ -909,9 +924,9 @@ configurator.setStrings({
909
924
  });
910
925
  ```
911
926
 
912
- The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
913
- `window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` /
914
- `useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
927
+ The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`,
928
+ and `window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()`
929
+ / `useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
915
930
 
916
931
  ---
917
932
 
@@ -936,9 +951,10 @@ export const experiments: Experiment[] = [
936
951
  configurator.setExperiments(experiments);
937
952
  ```
938
953
 
939
- Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for
940
- unauthenticated users). The framework's `initSession` / `captureEvent`
941
- requests automatically tag events with the user's branch assignments.
954
+ Bucketing is deterministic: `hash(experimentId + userId)` (or
955
+ `sessionId` for unauthenticated users). The framework's `initSession`
956
+ / `captureEvent` requests automatically tag events with the user's
957
+ branch assignments.
942
958
 
943
959
  ---
944
960
 
@@ -962,42 +978,19 @@ requests automatically tag events with the user's branch assignments.
962
978
 
963
979
  ---
964
980
 
965
- ## Package entry points
966
-
967
- | Import path | Description |
968
- |-------------|-------------|
969
- | `ugly-app` | Server: `createApp`, `TypedDB`, auth, AI clients, NATS, storage, email, push, workers. |
970
- | `ugly-app/shared` | Cross-tier: `defineRequests`, `defineCollections`, `definePage`, `defineWorkers`, Zod, experiments, time constants. |
971
- | `ugly-app/client` | React: `bootstrapApp`, `createRouter`, `lazyPage`, `AppProvider`, components, animations, audio, AI helpers. |
972
- | `ugly-app/server/adapter/workers` | Cloudflare Workers entry (Hono router + Durable Objects + Neon HTTP driver + R2). |
973
- | `ugly-app/server/ssr` | SSR renderer used by the Workers adapter. |
974
- | `ugly-app/conversation/{shared,server,client,engine}` | AI chat sessions with persisted history. |
975
- | `ugly-app/collab/{server,client}` | Yjs-based collaborative editing. |
976
- | `ugly-app/markdown/{shared,client}` | Markdown rendering + editor. |
977
- | `ugly-app/search/{shared,server}` | Search primitives. |
978
- | `ugly-app/agent/{shared,server,client}` | Agent-mode SSE + tool orchestration. |
979
- | `ugly-app/webrtc`, `ugly-app/webrtc/server` | WebRTC video rooms. |
980
- | `ugly-app/three/{server,client}` | Three.js scene helpers. |
981
- | `ugly-app/native`, `ugly-app/native/server` | Native task host + conformance harness. |
982
- | `ugly-app/inspect`, `ugly-app/inspect/agent`, `ugly-app/inspect/ui` | UX inspection agent + host bundle. |
983
- | `ugly-app/worker` | Worker queue runtime. |
984
- | `ugly-app/playwright` | E2E test utilities (`inspectWindow`, `expectClean`, `setDevice`, …). |
985
- | `ugly-app/testing` | Test-mode helpers. |
986
- | `ugly-app/vite`, `ugly-app/eslint` | Build-tool plugins. |
987
-
988
- ---
989
-
990
981
  ## Two-adapter architecture
991
982
 
992
- The same developer source compiles for two runtimes via `src/server/adapter/`:
983
+ The same developer source compiles for two runtimes via
984
+ `src/server/adapter/`:
993
985
 
994
986
  - **Adapter A — Node + TCP** (`src/server/adapter/node/`): default for `npm run dev` and any non-Workers deploy. Wraps `pg.Pool`, `nats.js`, AWS S3 SDK.
995
987
  - **Adapter B — Cloudflare Workers** (`src/server/adapter/workers/`): used when the Studio publish flow deploys to Cloudflare. Hono router + Durable Objects (one `CollectionDO` per project-collection, plus a `SessionDO` per user WS) + `@neondatabase/serverless` HTTP driver + R2 binding + Cloudflare Cron Triggers + Cloudflare Queues.
996
988
 
997
- Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`,
998
- `setCronTasks`, `setWorkers`) don't change between adapters. Local Workers dev
999
- boots via `npm run dev:workers`, which starts a small Node HTTP proxy speaking
1000
- Neon's wire format so the Worker can talk to a real Postgres.
989
+ Developer-facing APIs (`createTypedDB`, `subscribeDoc`,
990
+ `createStorageClient`, `setCronTasks`, `setWorkers`) don't change
991
+ between adapters. Local Workers dev boots via `npm run dev:workers`,
992
+ which starts a small Node HTTP proxy speaking Neon's wire format so
993
+ the Worker can talk to a real Postgres.
1001
994
 
1002
995
  ---
1003
996
 
@@ -1052,8 +1045,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via
1052
1045
  | `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
1053
1046
  | `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
1054
1047
 
1055
- Inside a scaffolded project, the same commands are available via `npm run …`
1056
- scripts — see `templates/CLAUDE.md`.
1048
+ Inside a scaffolded project, the same commands are available via `npm
1049
+ run …` scripts — see `templates/CLAUDE.md`.
1057
1050
 
1058
1051
  ---
1059
1052
 
@@ -1074,5 +1067,5 @@ The framework refuses to start when drift is detected (set
1074
1067
  ## Tech stack
1075
1068
 
1076
1069
  Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) ·
1077
- Qdrant · NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers +
1078
- Durable Objects (Adapter B) · ugly.bot platform
1070
+ Qdrant · NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare
1071
+ Workers + Durable Objects (Adapter B) · ugly.bot platform