ugly-app 0.1.956 → 0.1.958
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.
- package/README.md +200 -207
- package/dist/character/client/CharacterCanvas.d.ts.map +1 -1
- package/dist/character/client/CharacterCanvas.js.map +1 -1
- package/dist/character/client/index.d.ts +1 -1
- package/dist/character/client/index.d.ts.map +1 -1
- package/dist/character/client/index.js +1 -1
- package/dist/character/client/index.js.map +1 -1
- package/dist/character/client/lib/charGarment.d.ts.map +1 -1
- package/dist/character/client/lib/charGarment.js.map +1 -1
- package/dist/character/client/lib/characterScene.d.ts.map +1 -1
- package/dist/character/client/lib/characterScene.js +3 -1
- package/dist/character/client/lib/characterScene.js.map +1 -1
- package/dist/character/client/lib/fbxClips.d.ts.map +1 -1
- package/dist/character/client/lib/fbxClips.js +107 -44
- package/dist/character/client/lib/fbxClips.js.map +1 -1
- package/dist/character/client/lib/voiceCatalog.d.ts.map +1 -1
- package/dist/character/client/lib/voiceCatalog.js.map +1 -1
- package/dist/character/shared/lib/skinOverlap.d.ts.map +1 -1
- package/dist/character/shared/lib/skinOverlap.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/audio/AudioPlayer.d.ts +1 -0
- package/dist/client/audio/AudioPlayer.d.ts.map +1 -1
- package/dist/client/audio/AudioPlayer.js +13 -5
- package/dist/client/audio/AudioPlayer.js.map +1 -1
- package/dist/client/audio/AudioRecorder.d.ts +1 -0
- package/dist/client/audio/AudioRecorder.d.ts.map +1 -1
- package/dist/client/audio/AudioRecorder.js +8 -2
- package/dist/client/audio/AudioRecorder.js.map +1 -1
- package/dist/client/audio/audioPlayer.worklet.js +30 -3
- package/dist/client/audio/audioPlayer.worklet.js.map +1 -1
- package/dist/client/audio/audioRecorder.worklet.js +7 -3
- package/dist/client/audio/audioRecorder.worklet.js.map +1 -1
- package/dist/client/audio/workletLoader.d.ts +19 -0
- package/dist/client/audio/workletLoader.d.ts.map +1 -0
- package/dist/client/audio/workletLoader.js +58 -0
- package/dist/client/audio/workletLoader.js.map +1 -0
- package/dist/client/callAI.d.ts.map +1 -1
- package/dist/client/callAI.js.map +1 -1
- package/dist/server/App.d.ts.map +1 -1
- package/dist/server/App.js +7 -1
- package/dist/server/App.js.map +1 -1
- package/dist/server/adapter/workers/wrapWithAssets.d.ts.map +1 -1
- package/dist/server/adapter/workers/wrapWithAssets.js +9 -0
- package/dist/server/adapter/workers/wrapWithAssets.js.map +1 -1
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +1 -0
- package/dist/vite/index.js.map +1 -1
- package/package.json +2 -2
- package/src/character/client/lib/characterScene.ts +3 -1
- package/src/character/client/lib/fbxClips.ts +109 -45
- package/src/cli/version.ts +1 -1
- package/src/client/audio/AudioPlayer.ts +25 -5
- package/src/client/audio/AudioRecorder.ts +20 -2
- package/src/client/audio/audioPlayer.worklet.ts +33 -6
- package/src/client/audio/audioRecorder.worklet.ts +11 -7
- package/src/client/audio/workletLoader.test.ts +122 -0
- package/src/client/audio/workletLoader.ts +66 -0
- package/src/client/audio/workletSync.test.ts +43 -0
- package/src/server/App.ts +7 -1
- package/src/server/adapter/workers/wrapWithAssets.ts +9 -0
- package/src/vite/audioWorkletsPlugin.test.ts +45 -0
- package/src/vite/index.ts +6 -0
- package/templates/vite.config.ts +11 -2
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# ugly-app
|
|
2
2
|
|
|
3
|
-
A full-stack TypeScript framework for shipping production web apps.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
13
|
|
|
14
14
|
```bash
|
|
15
15
|
npx ugly-app init my-app
|
|
@@ -21,31 +21,25 @@ npm run dev
|
|
|
21
21
|
|
|
22
22
|
| Import path | Contents |
|
|
23
23
|
|-------------|----------|
|
|
24
|
-
| `ugly-app` | Server: `createApp`, `TypedDB`, auth, AI clients,
|
|
25
|
-
| `ugly-app/shared` | Cross-tier: `defineRequests`, `defineCollections`, `definePage`, `defineWorkers`, Zod, experiments
|
|
24
|
+
| `ugly-app` | Server: `createApp`, `TypedDB`, auth, AI clients, storage, NATS, workers. |
|
|
25
|
+
| `ugly-app/shared` | Cross-tier: `defineRequests`, `defineCollections`, `definePage`, `defineWorkers`, Zod, experiments. |
|
|
26
26
|
| `ugly-app/client` | React: `bootstrapApp`, `createRouter`, `lazyPage`, `AppProvider`, components, animations, audio, AI helpers. |
|
|
27
27
|
| `ugly-app/server/adapter/workers` | Cloudflare Workers entry (Hono router + Durable Objects + Neon HTTP driver + R2). |
|
|
28
28
|
| `ugly-app/server/ssr` | SSR renderer used by the Workers adapter. |
|
|
29
|
-
| `ugly-app/
|
|
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`, …). |
|
|
29
|
+
| `ugly-app/playwright` | E2E test utilities. |
|
|
40
30
|
| `ugly-app/testing` | Test-mode helpers. |
|
|
41
31
|
| `ugly-app/vite`, `ugly-app/eslint` | Build-tool plugins. |
|
|
42
32
|
|
|
33
|
+
Additional per-feature bundles: `conversation`, `character`, `collab`,
|
|
34
|
+
`markdown`, `search`, `agent`, `webrtc`, `three`, `native`, `inspect`,
|
|
35
|
+
`worker` — each with its own `shared` / `server` / `client` split.
|
|
36
|
+
|
|
43
37
|
---
|
|
44
38
|
|
|
45
39
|
## Server — `createApp()`
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
`createApp` is the single server entry. It owns Express, the WebSocket
|
|
42
|
+
server, the typed DB, and the RPC dispatcher.
|
|
49
43
|
|
|
50
44
|
```ts
|
|
51
45
|
// server/index.ts
|
|
@@ -103,29 +97,31 @@ function createApp<
|
|
|
103
97
|
): App<CollectionMap<typeof BUILTIN_DEFS & Defs>, RegistryPages<R>>;
|
|
104
98
|
```
|
|
105
99
|
|
|
106
|
-
Passing `pages` in the registry (`{ requests, messages, pages }`)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
100
|
+
Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades
|
|
101
|
+
`app.pushSend()` to a per-route typed API. `pages` is optional; apps that
|
|
102
|
+
only call `configurator.setPages()` still work — they just get the loose
|
|
103
|
+
`PageRegistry` typing on `pushSend`.
|
|
110
104
|
|
|
111
105
|
### The returned `App`
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
`App` extends `AppRouter`, so `dispatch(name, input, userId)` is available
|
|
108
|
+
for in-process invocation of any registered handler.
|
|
109
|
+
|
|
110
|
+
| Member | Description |
|
|
111
|
+
|--------|-------------|
|
|
115
112
|
| `start(port?)` | Start the server (default 3000; templates use 4321). |
|
|
116
|
-
| `db` |
|
|
113
|
+
| `db` | `TypedDB<Map>` — map inferred from your collections + framework built-ins. |
|
|
117
114
|
| `httpServer` | The underlying Node `http.Server`. |
|
|
118
115
|
| `wss` | The main `WebSocketServer` (path set via `setWsPath`, default `/rpc`). |
|
|
119
|
-
| `dispatch(name, input, userId)` |
|
|
116
|
+
| `dispatch(name, input, userId)` | `Promise<unknown>` — invoke any registered RPC handler programmatically. |
|
|
120
117
|
| `registerRoutes(fn)` | Mount additional Express routes after creation. |
|
|
121
|
-
| `pushSend(input)` | Typed push whose click-through target is a route from this app's `pages`.
|
|
118
|
+
| `pushSend(input)` | Typed push whose click-through target is a route from this app's `pages`. Mints a `https://ugly.bot/l/<code>` short link so the URL is always absolute and dock-app-routable. |
|
|
122
119
|
|
|
123
|
-
Framework services start
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
`R2_BUCKET` doesn't pay their startup cost.
|
|
120
|
+
Framework services start inside `app.start()`: schema drift check, NATS
|
|
121
|
+
connection + KV buckets, data-proxy connection, event-counter flush, TTL
|
|
122
|
+
cleanup for log tables, console/error capture, and ugly.bot log forwarding.
|
|
123
|
+
Postgres, NATS, storage, and AI clients load **lazily** — a host without
|
|
124
|
+
`DATABASE_URL` / `NATS_URL` / `R2_BUCKET` doesn't pay their startup cost.
|
|
129
125
|
|
|
130
126
|
### `AppConfigurator`
|
|
131
127
|
|
|
@@ -156,8 +152,8 @@ Every method is optional; `setPages` is what mounts the SPA.
|
|
|
156
152
|
|
|
157
153
|
### Handler signatures
|
|
158
154
|
|
|
159
|
-
Handlers are plain async functions — no context object. Access state
|
|
160
|
-
|
|
155
|
+
Handlers are plain async functions — no context object. Access state via
|
|
156
|
+
captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, …).
|
|
161
157
|
|
|
162
158
|
```ts
|
|
163
159
|
// req() — public, userId may be null
|
|
@@ -169,23 +165,27 @@ getMe: async (userId: string, input) => { /* … */ }
|
|
|
169
165
|
|
|
170
166
|
### Built-in framework requests
|
|
171
167
|
|
|
172
|
-
`createApp` registers several framework handlers reachable from any
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
`createApp` registers several framework handlers reachable from any client
|
|
169
|
+
via the normal RPC pipeline. App-provided handlers with the same name
|
|
170
|
+
override the framework's defaults. Rate-limited handlers are marked below.
|
|
175
171
|
|
|
176
|
-
| Name |
|
|
177
|
-
|
|
172
|
+
| Name | Notes |
|
|
173
|
+
|------|-------|
|
|
178
174
|
| `userGet` | Returns `{ userId, name, avatarUri }` for the given user (or caller). |
|
|
179
175
|
| `initSession` / `captureEvent` | Session + event logging tagged with experiment branches (public — no auth). |
|
|
180
|
-
| `textGen`
|
|
181
|
-
| `
|
|
176
|
+
| `textGen` | AI text proxy — server-validated, billed through ugly.bot. Rate-limited 20/60s. |
|
|
177
|
+
| `imageGen` | AI image proxy. Rate-limited 10/60s. |
|
|
178
|
+
| `kagiSearch` / `kagiEnrichWeb` / `kagiEnrichNews` | Web search via ugly.bot. Rate-limited 20/60s. |
|
|
179
|
+
| `kagiSummarize` | URL / text summary. Rate-limited 10/60s. |
|
|
182
180
|
| `uploadUrl` | Issues a presigned PUT for the `temp` bucket. |
|
|
183
|
-
| `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata. |
|
|
184
|
-
| `feedbackReportCreateNoAuth`
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
181
|
+
| `shareLink` | Mint a `https://ugly.bot/l/<code>` short link with OG metadata. Rate-limited 60/60s. |
|
|
182
|
+
| `feedbackReportCreateNoAuth` | Public, same-origin feedback endpoint used by browser telemetry. Rate-limited 10/60s. |
|
|
183
|
+
| `errorLogCaptureNoAuth` | Public error-log capture. |
|
|
184
|
+
| `perfSnapshotCaptureNoAuth` | Public perf snapshot capture. Rate-limited 60/60s. |
|
|
187
185
|
| `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()`. |
|
|
188
|
-
|
|
186
|
+
|
|
187
|
+
Add per-endpoint rate limits on your own handlers with
|
|
188
|
+
`rateLimit: { max, window }` on the request definition.
|
|
189
189
|
|
|
190
190
|
---
|
|
191
191
|
|
|
@@ -222,8 +222,8 @@ export const requests = defineRequests({
|
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
Every request is reachable as **both** `socket.request(name, input)`
|
|
225
|
-
(WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported
|
|
226
|
-
|
|
225
|
+
(WebSocket) and `POST /api/:name { input }` (HTTP). `z` is re-exported from
|
|
226
|
+
Zod for convenience.
|
|
227
227
|
|
|
228
228
|
### Collections (`shared/collections.ts`)
|
|
229
229
|
|
|
@@ -259,20 +259,19 @@ export const collections = defineCollections({
|
|
|
259
259
|
- `public` — allow unauthenticated client reads.
|
|
260
260
|
- `cascadeFrom` — parent collection: when the parent doc is deleted, docs in this collection are cascade-deleted.
|
|
261
261
|
- `trackKeys?` — fields usable as NATS routing keys for scoped `trackDocs` subscriptions.
|
|
262
|
-
- `getter?(ids)` — batch resolver for collections with no local table (
|
|
262
|
+
- `getter?(ids)` — batch resolver for collections with no local table (e.g. `userPublic` resolves via ugly.bot).
|
|
263
263
|
- `skipReadValidation?` — opt out of per-row zod validation on reads for very hot collections. Writes are always validated. Global kill-switch: `UGLY_DB_VALIDATE_READS=0`.
|
|
264
264
|
- `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
|
|
265
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 })`.
|
|
266
266
|
|
|
267
267
|
All documents extend `DBObject`: `{ _id, version, created, updated }`.
|
|
268
|
-
Use `dbDefaults()` to stamp `version` / `created` / `updated` on
|
|
269
|
-
|
|
270
|
-
`
|
|
271
|
-
`CLAUDE.md`).
|
|
268
|
+
Use `dbDefaults()` to stamp `version` / `created` / `updated` on inserts.
|
|
269
|
+
**Always generate `_id` with `nanoid()`** — never `crypto.randomUUID()`,
|
|
270
|
+
`Date.now()`, or `Math.random()` (see `CLAUDE.md`).
|
|
272
271
|
|
|
273
|
-
After schema changes, run `npm run db:schema-gen` then `npm run
|
|
274
|
-
|
|
275
|
-
|
|
272
|
+
After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`.
|
|
273
|
+
The app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
|
|
274
|
+
only as a last resort).
|
|
276
275
|
|
|
277
276
|
---
|
|
278
277
|
|
|
@@ -297,21 +296,19 @@ export type AppPages = typeof pages;
|
|
|
297
296
|
|
|
298
297
|
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime
|
|
299
298
|
object carrying `PageMeta` plus a phantom `_params` used only for
|
|
300
|
-
TypeScript inference.
|
|
299
|
+
TypeScript inference. `PageMeta` fields:
|
|
301
300
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
- `auth` (default `true`) — protected route. When `isAuthenticated()` returns false on this route, the router synchronously renders the framework's `<AuthRoot>` in place of the page (dispatches on `window.__UGLY_APP_AUTH_MODE__`: Mode A → `<LoginPopup>`, Mode B → `<MagicLinkForm>` plus optional Google button). Apps cannot override this fallback — the framework owns the login UX so no route can accidentally ship as auth-required with no way to log in.
|
|
305
|
-
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is silently dropped to `ssr: false` with a warning: the SSR document is served from a shared edge cache and must not depend on the viewer.
|
|
301
|
+
- `auth` (default `true`) — protected route. When `isAuthenticated()` returns false on this route, the router synchronously renders the framework's `<AuthRoot>` in place of the page (Mode A → `<LoginPopup>`, Mode B → `<MagicLinkForm>` plus optional Google button). Apps cannot override this fallback.
|
|
302
|
+
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is silently downgraded to `ssr: false` with a warning: the SSR document is served from a shared edge cache and must not depend on the viewer.
|
|
306
303
|
- `cacheQuery?: string[]` — query params that participate in the SSR edge-cache key. Anything NOT listed bypasses the cache instead of poisoning it (e.g. `cacheQuery: ['q']` on a search page).
|
|
307
|
-
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is
|
|
304
|
+
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is `DEFAULT_SSR_CACHE_TIMEOUT` (1 year) because the `buildId` is already part of the cache key. Override only for pages that drift between deploys.
|
|
308
305
|
|
|
309
306
|
Path syntax: `:param` matches a single path segment; `*param` is greedy
|
|
310
|
-
(captures slashes). Query-string params are declared in `Params` but
|
|
311
|
-
|
|
307
|
+
(captures slashes). Query-string params are declared in `Params` but never
|
|
308
|
+
appear in the path template.
|
|
312
309
|
|
|
313
|
-
`definePages<T>(p)` is a pass-through identity — use it to
|
|
314
|
-
registry
|
|
310
|
+
`definePages<T>(p)` is a pass-through identity — use it to name the
|
|
311
|
+
registry.
|
|
315
312
|
|
|
316
313
|
### `createRouter()` (`ugly-app/client`)
|
|
317
314
|
|
|
@@ -332,19 +329,19 @@ export const {
|
|
|
332
329
|
**Config:**
|
|
333
330
|
- `pages` (required) — the `PageRegistry` from `shared/pages.ts`.
|
|
334
331
|
- `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`.
|
|
335
|
-
- `ssrPages
|
|
332
|
+
- `ssrPages?: SsrPageMap<Pages>` — `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.
|
|
336
333
|
|
|
337
334
|
**Returns:**
|
|
338
335
|
|
|
339
|
-
- **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated
|
|
340
|
-
- **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing
|
|
336
|
+
- **`RouterProvider`** — props: `children`, `fallback?`, `isAuthenticated?: () => boolean`, `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.
|
|
337
|
+
- **`RouterView`** — renders the active page with animated transitions. Props: `durationMs?`, `easing?: EasingFunction`, `transitionComponent?: React.ComponentType<RouterTransitionProps>` (replaces the default `ViewFlipper`), `renderPage?(state: RouterStateRaw) => ReactElement` (sync alternative to `allPages` loaders).
|
|
341
338
|
- **`useRouter()`** — returns `RouterContextValue<Pages>` (typed navigation + popup API). Throws if used outside `<RouterProvider>`.
|
|
342
339
|
- **`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`).
|
|
343
340
|
- **`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).
|
|
344
341
|
|
|
345
|
-
A standalone `Link` is also exported from `ugly-app/client` for code
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
A standalone `Link` is also exported from `ugly-app/client` for code that
|
|
343
|
+
can't easily reach the typed one; it accepts an optional `router` prop and
|
|
344
|
+
falls back to `<RouterProvider>` context.
|
|
348
345
|
|
|
349
346
|
**Never use a bare `<a href="/route">`** for internal navigation — it
|
|
350
347
|
triggers a full document reload (white flash + repaint). See the "client
|
|
@@ -369,10 +366,9 @@ export const allPages = {
|
|
|
369
366
|
- **`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.
|
|
370
367
|
|
|
371
368
|
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch
|
|
372
|
-
dynamically imported module"): on the first failure they trigger a
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
new chunk hashes.
|
|
369
|
+
dynamically imported module"): on the first failure they trigger a single
|
|
370
|
+
`window.location.reload()` (guarded by `sessionStorage` so a genuinely
|
|
371
|
+
broken chunk can't loop) to fetch fresh chunk hashes.
|
|
376
372
|
|
|
377
373
|
```ts
|
|
378
374
|
// pages/SlowPageLoader.tsx
|
|
@@ -402,15 +398,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
|
|
|
402
398
|
back(); // browser history back
|
|
403
399
|
```
|
|
404
400
|
|
|
405
|
-
Route names and params are fully typed against `pages`. `push` /
|
|
406
|
-
|
|
407
|
-
|
|
401
|
+
Route names and params are fully typed against `pages`. `push` / `replace`
|
|
402
|
+
no-op with a `console.error` when `buildUrl()` produces a URL that doesn't
|
|
403
|
+
match a registered route.
|
|
408
404
|
|
|
409
405
|
### Popups — `openPopup()`
|
|
410
406
|
|
|
411
|
-
`useRouter().openPopup()` is the canonical modal / sheet / menu API.
|
|
412
|
-
|
|
413
|
-
|
|
407
|
+
`useRouter().openPopup()` is the canonical modal / sheet / menu API. The
|
|
408
|
+
router owns the popup layer, drives a spring animation, and stacks popups
|
|
409
|
+
z-index-correctly above every page.
|
|
414
410
|
|
|
415
411
|
```tsx
|
|
416
412
|
const { openPopup } = useRouter();
|
|
@@ -435,8 +431,8 @@ handle.hide(); // dismiss programmatically — same as router.closePopup(handle.
|
|
|
435
431
|
- **`contextMenu`** — same as transient, intended for menus and pickers.
|
|
436
432
|
|
|
437
433
|
`renderLayer` receives `{ content, spring, hide }`: `spring` is a
|
|
438
|
-
`createAnimatedValue()` result driving 0 → 1; `hide` closes the popup.
|
|
439
|
-
|
|
434
|
+
`createAnimatedValue()` result driving 0 → 1; `hide` closes the popup. The
|
|
435
|
+
default layer animates open with `easeOut` (250 ms) and closed with
|
|
440
436
|
`easeIn` (200 ms). Popups render as siblings of the router's children
|
|
441
437
|
(managed inside `RouterProvider`), so they stack above every page.
|
|
442
438
|
|
|
@@ -445,10 +441,10 @@ The default layer animates open with `easeOut` (250 ms) and closed with
|
|
|
445
441
|
`html, body, #root` are `overflow: hidden` — the document itself never
|
|
446
442
|
scrolls, so a page that just renders tall content is clipped. For pages
|
|
447
443
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
444
|
+
e.g. a public page special-cased before the auth gate), wrap the content
|
|
445
|
+
in `SimpleScrollView` (exported from `ugly-app/client`). For long /
|
|
446
|
+
virtualized lists with scroll-position persistence, use the richer
|
|
447
|
+
`ScrollView`. See the "page scrolling" rule in `CLAUDE.md`.
|
|
452
448
|
|
|
453
449
|
---
|
|
454
450
|
|
|
@@ -471,6 +467,9 @@ bootstrapApp({
|
|
|
471
467
|
});
|
|
472
468
|
```
|
|
473
469
|
|
|
470
|
+
`bootstrapApp(options): void` — returns nothing; it renders directly to the
|
|
471
|
+
DOM.
|
|
472
|
+
|
|
474
473
|
**`BootstrapAppOptions`:**
|
|
475
474
|
|
|
476
475
|
| Field | Description |
|
|
@@ -488,28 +487,28 @@ bootstrapApp({
|
|
|
488
487
|
| `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. |
|
|
489
488
|
| `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
|
|
490
489
|
|
|
491
|
-
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
490
|
+
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after
|
|
491
|
+
cookie verification). If absent, it renders unauthenticated immediately —
|
|
492
|
+
the router's per-route auth guard surfaces `<AuthRoot>` only when the user
|
|
493
|
+
opens a protected route. If the token is present, it connects the socket,
|
|
494
|
+
mounts `<AppProvider>`, and renders.
|
|
496
495
|
|
|
497
496
|
After render, a hidden iframe silently calls
|
|
498
|
-
`${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
497
|
+
`${UGLY_BOT_URL}/oauth/silent`: if it returns a fresh code, the cookie is
|
|
498
|
+
refreshed via `POST /auth/verify`; if the returned account differs from
|
|
499
|
+
the current session, the page reloads onto the live account (guarded once
|
|
500
|
+
per from→to pair to prevent loops). Any `?ugly_oauth_code=…` in the URL
|
|
501
|
+
(redirect-fallback OAuth code from ugly.bot) is redeemed at the top of
|
|
502
|
+
bootstrap before anything renders.
|
|
504
503
|
|
|
505
504
|
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it
|
|
506
505
|
renders `<MagicLinkCallback>` instead of the regular app shell.
|
|
507
506
|
|
|
508
507
|
### `AppProvider` & `useApp()`
|
|
509
508
|
|
|
510
|
-
`bootstrapApp` mounts `<AppProvider>` automatically after socket
|
|
511
|
-
|
|
512
|
-
|
|
509
|
+
`bootstrapApp` mounts `<AppProvider>` automatically after socket connect.
|
|
510
|
+
Use `useApp()` inside any page to access the active user, socket, and
|
|
511
|
+
app-scoped services.
|
|
513
512
|
|
|
514
513
|
```ts
|
|
515
514
|
const {
|
|
@@ -517,28 +516,26 @@ const {
|
|
|
517
516
|
user, // UserBase doc
|
|
518
517
|
socket, // AppSocket — typed RPC client
|
|
519
518
|
uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS
|
|
520
|
-
showPopup, // flat overlay layer (see note)
|
|
521
|
-
hidePopup,
|
|
522
|
-
hideAllPopups,
|
|
519
|
+
showPopup, // (content: ReactElement) => id — flat overlay layer (see note)
|
|
520
|
+
hidePopup, // (id: string) => void
|
|
521
|
+
hideAllPopups, // () => void
|
|
523
522
|
runAsync, // (label, async () => {…}, options?) — shows loading overlay
|
|
524
|
-
splashDone, // (step: string) — mark a splash-screen step complete
|
|
523
|
+
splashDone, // (step: string) => void — mark a splash-screen step complete
|
|
525
524
|
localizer, // (key, params?) => string — alias for useLocalizer()
|
|
526
525
|
} = useApp();
|
|
527
526
|
```
|
|
528
527
|
|
|
529
|
-
`useApp<TAsyncOptions>()` is generic in the async-options type so apps
|
|
530
|
-
|
|
531
|
-
`AppProvider` `cloneElement`s the overlay with
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
prop, and finally to identity.
|
|
528
|
+
`useApp<TAsyncOptions>()` is generic in the async-options type so apps can
|
|
529
|
+
pass a custom option through to a custom `loadingOverlay` element. When
|
|
530
|
+
`runAsync` is pending, `AppProvider` `cloneElement`s the overlay with
|
|
531
|
+
`{ label, asyncOptions }`. `useAppOptional()` returns `null` outside the
|
|
532
|
+
provider; `useLocalizer()` prefers `<StringsProvider>` data, falls back to
|
|
533
|
+
the `AppProvider` `localizer` prop, and finally to identity.
|
|
536
534
|
|
|
537
535
|
Two popup APIs coexist: `useRouter().openPopup()` (spring-animated,
|
|
538
|
-
dismissible, mode-aware — **the default choice**) and
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
router's.
|
|
536
|
+
dismissible, mode-aware — **the default choice**) and `useApp().showPopup()`
|
|
537
|
+
(a flat overlay layer returning a string id, for content that must sit
|
|
538
|
+
*outside* the router transition). Prefer the router's.
|
|
542
539
|
|
|
543
540
|
### Direct socket access
|
|
544
541
|
|
|
@@ -555,8 +552,8 @@ router's.
|
|
|
555
552
|
| `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
|
|
556
553
|
| `disconnect()` | Close the connection. |
|
|
557
554
|
|
|
558
|
-
For pure HTTP (no WebSocket), use
|
|
559
|
-
from `ugly-app/client`.
|
|
555
|
+
For pure HTTP (no WebSocket), use
|
|
556
|
+
`createHttpClient({ requests, token?, baseUrl? })` from `ugly-app/client`.
|
|
560
557
|
|
|
561
558
|
---
|
|
562
559
|
|
|
@@ -565,14 +562,14 @@ from `ugly-app/client`.
|
|
|
565
562
|
ugly-app uses HttpOnly cookies and server-side JWT injection — no
|
|
566
563
|
`localStorage`, no client-side token handling.
|
|
567
564
|
|
|
568
|
-
Two modes, picked from the `auth` block in the project's `.uglyapp`
|
|
569
|
-
config:
|
|
565
|
+
Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
570
566
|
|
|
571
567
|
- **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.
|
|
572
568
|
- **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`.
|
|
573
569
|
|
|
574
|
-
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client
|
|
575
|
-
|
|
570
|
+
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code
|
|
571
|
+
(incl. `<AuthRoot>`) can branch correctly. In Mode B with Google
|
|
572
|
+
configured, `window.__UGLY_APP_GOOGLE_CLIENT_ID__` is also injected.
|
|
576
573
|
|
|
577
574
|
### Mode A — ugly.bot OAuth (default)
|
|
578
575
|
|
|
@@ -596,9 +593,9 @@ configured it adds Google as an extra provider on the same router.
|
|
|
596
593
|
|
|
597
594
|
### Token-in-URL embed
|
|
598
595
|
|
|
599
|
-
Any GET request with `?token=<JWT>` will, if the token verifies, set
|
|
600
|
-
|
|
601
|
-
|
|
596
|
+
Any GET request with `?token=<JWT>` will, if the token verifies, set the
|
|
597
|
+
cookie and 302-redirect to the same URL without the token parameter —
|
|
598
|
+
useful for embedding any page in an iframe.
|
|
602
599
|
|
|
603
600
|
### Built-in auth routes
|
|
604
601
|
|
|
@@ -632,8 +629,8 @@ configurator.setAuth({
|
|
|
632
629
|
## Database — `TypedDB`
|
|
633
630
|
|
|
634
631
|
Access via `app.db`. All methods accept a `CollectionDef` (from
|
|
635
|
-
`defineCollections()`) or, on the `raw*` / `getQuery*` variants, a
|
|
636
|
-
|
|
632
|
+
`defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain
|
|
633
|
+
collection name string.
|
|
637
634
|
|
|
638
635
|
### Writing
|
|
639
636
|
|
|
@@ -658,10 +655,10 @@ await db.batch(async () => {
|
|
|
658
655
|
});
|
|
659
656
|
```
|
|
660
657
|
|
|
661
|
-
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
658
|
+
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`.
|
|
659
|
+
All keys are dot-notation, fully typed against the collection's schema.
|
|
660
|
+
Partial updates go through optimistic concurrency to prevent lost updates
|
|
661
|
+
on concurrent writers.
|
|
665
662
|
|
|
666
663
|
### Reading
|
|
667
664
|
|
|
@@ -745,10 +742,9 @@ Imports available from `ugly-app`:
|
|
|
745
742
|
|
|
746
743
|
## AI providers
|
|
747
744
|
|
|
748
|
-
AI calls are proxied through ugly.bot — your app never holds a
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
billing.
|
|
745
|
+
AI calls are proxied through ugly.bot — your app never holds a provider
|
|
746
|
+
key. Pass `UGLY_BOT_TOKEN` in the environment and the framework handles
|
|
747
|
+
routing, balance tracking, retries, and per-user billing.
|
|
752
748
|
|
|
753
749
|
### Server-side text generation
|
|
754
750
|
|
|
@@ -770,9 +766,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
|
|
|
770
766
|
});
|
|
771
767
|
```
|
|
772
768
|
|
|
773
|
-
Available models are exposed via `textGenModels` / `textGenModelData`
|
|
774
|
-
|
|
775
|
-
|
|
769
|
+
Available models are exposed via `textGenModels` / `textGenModelData` from
|
|
770
|
+
`ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq,
|
|
771
|
+
Fireworks, Kimi, and Kie families.
|
|
776
772
|
|
|
777
773
|
### Server-side image generation
|
|
778
774
|
|
|
@@ -858,24 +854,23 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
|
858
854
|
await storage.delete('public', destKey);
|
|
859
855
|
```
|
|
860
856
|
|
|
861
|
-
Client-side, use `socket.uploadFile(file, key)` — it requests a
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
857
|
+
Client-side, use `socket.uploadFile(file, key)` — it requests a presigned
|
|
858
|
+
URL via the built-in `uploadUrl` framework request and streams the upload.
|
|
859
|
+
In dev, uploads go through a same-origin `/_s3` proxy to avoid CORS with
|
|
860
|
+
local MinIO.
|
|
865
861
|
|
|
866
|
-
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for
|
|
867
|
-
|
|
862
|
+
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
|
|
863
|
+
isolation.
|
|
868
864
|
|
|
869
865
|
On Cloudflare Workers, storage uses the R2 binding directly; presigned
|
|
870
|
-
PUTs are not exposed (browser uploads must go through a Worker
|
|
871
|
-
endpoint).
|
|
866
|
+
PUTs are not exposed (browser uploads must go through a Worker endpoint).
|
|
872
867
|
|
|
873
868
|
### Static assets — `static/`
|
|
874
869
|
|
|
875
870
|
Large, rarely-changing files (3D models, textures, audio, fonts) go in the
|
|
876
871
|
project-root `static/` folder. `ugly-app build:static` hashes them and
|
|
877
|
-
generates `shared/StaticAssets.ts`; publish uploads only the files whose
|
|
878
|
-
changed.
|
|
872
|
+
generates `shared/StaticAssets.ts`; publish uploads only the files whose
|
|
873
|
+
bytes changed.
|
|
879
874
|
|
|
880
875
|
```ts
|
|
881
876
|
import { staticUrl } from '../shared/StaticAssets'
|
|
@@ -885,26 +880,25 @@ const url = staticUrl('models/char2.ugm')
|
|
|
885
880
|
```
|
|
886
881
|
|
|
887
882
|
The bucket gets its own custom domain, so **Cloudflare's CDN serves these
|
|
888
|
-
directly** — no Worker invocation per request, and R2 answers `Range`
|
|
889
|
-
for progressive loaders.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
rule, without which COEP `require-corp` would silently block them.
|
|
883
|
+
directly** — no Worker invocation per request, and R2 answers `Range`
|
|
884
|
+
natively for progressive loaders. Publish also sets the bucket CORS policy
|
|
885
|
+
and a `Cross-Origin-Resource-Policy: cross-origin` response rule, without
|
|
886
|
+
which COEP `require-corp` would silently block them.
|
|
893
887
|
|
|
894
888
|
Unlike `/{buildId}/assets/*`, these URLs do not change when you deploy, so
|
|
895
|
-
returning users do not re-download unchanged files. Assets that no build
|
|
896
|
-
the last `retentionDays` (default 7) references are deleted from R2
|
|
889
|
+
returning users do not re-download unchanged files. Assets that no build
|
|
890
|
+
from the last `retentionDays` (default 7) references are deleted from R2
|
|
897
891
|
automatically, along with expired `builds/` prefixes.
|
|
898
892
|
|
|
899
|
-
Requires a provisioned app domain — publish fails rather than emitting
|
|
900
|
-
nothing serves. Configure in `.uglyapp`:
|
|
893
|
+
Requires a provisioned app domain — publish fails rather than emitting
|
|
894
|
+
URLs nothing serves. Configure in `.uglyapp`:
|
|
901
895
|
|
|
902
896
|
```json
|
|
903
897
|
"staticAssets": { "dir": "static", "retentionDays": 7 }
|
|
904
898
|
```
|
|
905
899
|
|
|
906
|
-
Dev serves the same files off disk at same-origin, so a newly added asset
|
|
907
|
-
before it has ever been published.
|
|
900
|
+
Dev serves the same files off disk at same-origin, so a newly added asset
|
|
901
|
+
works before it has ever been published.
|
|
908
902
|
|
|
909
903
|
### `hostBundle` — serve the vite bundle from the CDN too
|
|
910
904
|
|
|
@@ -913,25 +907,26 @@ before it has ever been published.
|
|
|
913
907
|
```
|
|
914
908
|
|
|
915
909
|
Opt-in. Publish provisions `static.<zone>` (or `<app>-static.<zone>` for a
|
|
916
|
-
subdomain app), persists the `baseUrl`, and vite then emits every asset
|
|
917
|
-
against it. Because the URLs are content-addressed rather than
|
|
918
|
-
prefixed, a byte-identical chunk keeps its URL across deploys —
|
|
919
|
-
users re-download only what actually changed, and each deploy
|
|
920
|
-
handful of KB instead of the whole bundle.
|
|
921
|
-
|
|
922
|
-
The first deploy only provisions; the bundle moves to the CDN on the next
|
|
923
|
-
That is deliberate — a single pass would point the bundle at a
|
|
924
|
-
TLS certificate may not have issued yet.
|
|
925
|
-
|
|
926
|
-
**Workers are handled for you, but know why.** The bundle is then
|
|
927
|
-
and `new Worker(url)` *rejects a cross-origin script URL
|
|
928
|
-
structural, and no CORS or CORP header lifts it.
|
|
929
|
-
shim that routes such URLs through a same-origin
|
|
930
|
-
script, so `new Worker(new URL('./x.worker.ts',
|
|
931
|
-
working. `SharedWorker` is **not** shimmed: every
|
|
932
|
-
same trick would give each caller a private
|
|
933
|
-
service workers by root-absolute path
|
|
934
|
-
against the document, so those stay
|
|
910
|
+
subdomain app), persists the `baseUrl`, and vite then emits every asset
|
|
911
|
+
URL against it. Because the URLs are content-addressed rather than
|
|
912
|
+
build-id prefixed, a byte-identical chunk keeps its URL across deploys —
|
|
913
|
+
so returning users re-download only what actually changed, and each deploy
|
|
914
|
+
uploads a handful of KB instead of the whole bundle.
|
|
915
|
+
|
|
916
|
+
The first deploy only provisions; the bundle moves to the CDN on the next
|
|
917
|
+
one. That is deliberate — a single pass would point the bundle at a
|
|
918
|
+
hostname whose TLS certificate may not have issued yet.
|
|
919
|
+
|
|
920
|
+
**Workers are handled for you, but know why.** The bundle is then
|
|
921
|
+
cross-origin, and `new Worker(url)` *rejects a cross-origin script URL
|
|
922
|
+
outright* — that is structural, and no CORS or CORP header lifts it.
|
|
923
|
+
`bootstrapApp` installs a shim that routes such URLs through a same-origin
|
|
924
|
+
`blob:` importing the real script, so `new Worker(new URL('./x.worker.ts',
|
|
925
|
+
import.meta.url))` keeps working. `SharedWorker` is **not** shimmed: every
|
|
926
|
+
blob URL is unique, so the same trick would give each caller a private
|
|
927
|
+
worker and break sharing. Register service workers by root-absolute path
|
|
928
|
+
(`/sw.js`) — `register()` resolves against the document, so those stay
|
|
929
|
+
same-origin.
|
|
935
930
|
|
|
936
931
|
---
|
|
937
932
|
|
|
@@ -963,16 +958,15 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
|
|
|
963
958
|
configurator.setWorkers(cronTasks, cronHandlers);
|
|
964
959
|
```
|
|
965
960
|
|
|
966
|
-
Each worker can have `inputSchema`, `outputSchema`, `schedule`,
|
|
967
|
-
`
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
961
|
+
Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
|
|
962
|
+
`description`. Workers without a schedule are still invocable via
|
|
963
|
+
`POST /_workers/run` (auth: localhost in dev, `Authorization: Bearer $CRON_SECRET`
|
|
964
|
+
in prod). Scheduled workers also appear in `/_cron/manifest` for the
|
|
965
|
+
deploy orchestrator.
|
|
971
966
|
|
|
972
|
-
On the Workers adapter, scheduled workers dispatch through Cloudflare
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
queues only).
|
|
967
|
+
On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
|
|
968
|
+
Triggers and durable queueing goes through Cloudflare Queues. On the Node
|
|
969
|
+
adapter, `enqueueWorker` runs the handler inline (in-process queues only).
|
|
976
970
|
|
|
977
971
|
---
|
|
978
972
|
|
|
@@ -987,9 +981,9 @@ configurator.setStrings({
|
|
|
987
981
|
});
|
|
988
982
|
```
|
|
989
983
|
|
|
990
|
-
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`,
|
|
991
|
-
|
|
992
|
-
|
|
984
|
+
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
|
|
985
|
+
`window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` /
|
|
986
|
+
`useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
|
|
993
987
|
|
|
994
988
|
---
|
|
995
989
|
|
|
@@ -1014,10 +1008,9 @@ export const experiments: Experiment[] = [
|
|
|
1014
1008
|
configurator.setExperiments(experiments);
|
|
1015
1009
|
```
|
|
1016
1010
|
|
|
1017
|
-
Bucketing is deterministic: `hash(experimentId + userId)` (or
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
branch assignments.
|
|
1011
|
+
Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId`
|
|
1012
|
+
for unauthenticated users). The framework's `initSession` / `captureEvent`
|
|
1013
|
+
requests automatically tag events with the user's branch assignments.
|
|
1021
1014
|
|
|
1022
1015
|
---
|
|
1023
1016
|
|
|
@@ -1050,10 +1043,10 @@ The same developer source compiles for two runtimes via
|
|
|
1050
1043
|
- **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.
|
|
1051
1044
|
|
|
1052
1045
|
Developer-facing APIs (`createTypedDB`, `subscribeDoc`,
|
|
1053
|
-
`createStorageClient`, `setCronTasks`, `setWorkers`) don't change
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1046
|
+
`createStorageClient`, `setCronTasks`, `setWorkers`) don't change between
|
|
1047
|
+
adapters. Local Workers dev boots via `npm run dev:workers`, which starts
|
|
1048
|
+
a small Node HTTP proxy speaking Neon's wire format so the Worker can talk
|
|
1049
|
+
to a real Postgres.
|
|
1057
1050
|
|
|
1058
1051
|
---
|
|
1059
1052
|
|
|
@@ -1108,8 +1101,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via
|
|
|
1108
1101
|
| `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
|
|
1109
1102
|
| `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
|
|
1110
1103
|
|
|
1111
|
-
Inside a scaffolded project, the same commands are available via
|
|
1112
|
-
run …` scripts — see `templates/CLAUDE.md`.
|
|
1104
|
+
Inside a scaffolded project, the same commands are available via
|
|
1105
|
+
`npm run …` scripts — see `templates/CLAUDE.md`.
|
|
1113
1106
|
|
|
1114
1107
|
---
|
|
1115
1108
|
|