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.
- package/README.md +216 -223
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/bootstrapApp.d.ts.map +1 -1
- package/dist/client/bootstrapApp.js +21 -4
- package/dist/client/bootstrapApp.js.map +1 -1
- package/dist/client/createSocket.d.ts +24 -0
- package/dist/client/createSocket.d.ts.map +1 -1
- package/dist/client/createSocket.js +46 -2
- package/dist/client/createSocket.js.map +1 -1
- package/dist/client/refreshWebPush.d.ts.map +1 -1
- package/dist/client/refreshWebPush.js +4 -0
- package/dist/client/refreshWebPush.js.map +1 -1
- package/dist/client/webPush.d.ts +33 -0
- package/dist/client/webPush.d.ts.map +1 -1
- package/dist/client/webPush.js +95 -7
- package/dist/client/webPush.js.map +1 -1
- package/dist/conversation/server/Conversation.d.ts.map +1 -1
- package/dist/conversation/server/Conversation.js +9 -0
- package/dist/conversation/server/Conversation.js.map +1 -1
- package/dist/inspect/host-bundle.js +8 -8
- package/dist/markdown/client/MdastViewer.d.ts.map +1 -1
- package/dist/markdown/client/MdastViewer.js +63 -2
- package/dist/markdown/client/MdastViewer.js.map +1 -1
- package/dist/native/wrapper.d.ts +5 -0
- package/dist/native/wrapper.d.ts.map +1 -1
- package/dist/native/wrapper.js +6 -0
- package/dist/native/wrapper.js.map +1 -1
- package/dist/server/adapter/workers/createWorkersApp.d.ts +25 -0
- package/dist/server/adapter/workers/createWorkersApp.d.ts.map +1 -1
- package/dist/server/adapter/workers/createWorkersApp.js +53 -1
- package/dist/server/adapter/workers/createWorkersApp.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/sqlite/SqliteMigrations.d.ts +73 -0
- package/dist/server/sqlite/SqliteMigrations.d.ts.map +1 -0
- package/dist/server/sqlite/SqliteMigrations.js +55 -0
- package/dist/server/sqlite/SqliteMigrations.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/client/bootstrapApp.tsx +26 -7
- package/src/client/createSocket.test.ts +28 -0
- package/src/client/createSocket.ts +56 -6
- package/src/client/refreshWebPush.ts +3 -0
- package/src/client/webPush.test.ts +87 -0
- package/src/client/webPush.ts +117 -7
- package/src/conversation/server/Conversation.ts +9 -0
- package/src/markdown/client/MdastViewer.tsx +69 -1
- package/src/native/wrapper.ts +6 -0
- package/src/server/adapter/workers/createWorkersApp.ts +81 -1
- package/src/server/index.ts +17 -0
- package/src/server/sqlite/SqliteMigrations.test.ts +108 -0
- 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.
|
|
4
|
-
with `npx ugly-app init my-app` and get an opinionated Node +
|
|
5
|
-
stack with type-safe RPC over WebSocket and HTTP,
|
|
6
|
-
built-in auth, AI generation, storage,
|
|
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
|
|
11
|
-
AI provider keys, push, email, and deployment. Your app talks
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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) => (
|
|
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 }`)
|
|
103
|
-
`app.pushSend()` to a per-route typed API. `pages` is optional;
|
|
104
|
-
call `configurator.setPages()` still work — they just get
|
|
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)` |
|
|
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
|
|
120
|
-
check, NATS connection + KV buckets, data-proxy connection,
|
|
121
|
-
flush, TTL cleanup for log tables, console/error capture,
|
|
122
|
-
forwarding. Postgres, NATS, storage, and AI clients
|
|
123
|
-
host without `DATABASE_URL` / `NATS_URL` /
|
|
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 |
|
|
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)` |
|
|
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
|
|
156
|
-
captured imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`,
|
|
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
|
|
169
|
-
the normal RPC pipeline. App-provided handlers with the same
|
|
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
|
|
180
|
-
| `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` / `perfSnapshotCaptureNoAuth` | Same-origin, public endpoints used by browser telemetry
|
|
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()
|
|
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,
|
|
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
|
|
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 /
|
|
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.
|
|
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 }`.
|
|
264
|
-
`dbDefaults()` to stamp `version` / `created` / `updated` on
|
|
265
|
-
**Always generate `_id` with `nanoid()`** — never
|
|
266
|
-
`Date.now()`, or `Math.random()` (see
|
|
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
|
|
269
|
-
The app refuses to start when drift is detected (set
|
|
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
|
|
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 }>(),
|
|
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
|
|
294
|
-
carrying `PageMeta` plus a phantom `_params` used only for
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
341
|
-
can't easily reach the typed one; it accepts an optional `router`
|
|
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
|
|
345
|
-
a full document reload (white flash + repaint). See the "client
|
|
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
|
|
368
|
-
`window.location.reload()` (guarded by `sessionStorage` so a
|
|
369
|
-
chunk can't loop) to fetch the fresh `index.html` with
|
|
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();
|
|
402
|
+
back(); // browser history back
|
|
397
403
|
```
|
|
398
404
|
|
|
399
|
-
Route names and params are fully typed against `pages`. `push` /
|
|
400
|
-
no-op with a `console.error` when `buildUrl()` produces a URL
|
|
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.
|
|
406
|
-
router owns the popup layer, drives a spring animation, and stacks
|
|
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
|
|
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.
|
|
433
|
-
default layer animates open with `easeOut` (250 ms) and closed with
|
|
434
|
-
(200 ms). Popups render as siblings of the router's children
|
|
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
|
|
441
|
-
own their scrolling (or render outside the normal authed chrome —
|
|
442
|
-
public page special-cased before the auth gate), wrap the
|
|
443
|
-
`SimpleScrollView` (exported from `ugly-app/client`). For
|
|
444
|
-
lists with scroll-position persistence, use 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
|
|
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
|
|
486
|
-
cookie verification). If absent, it renders unauthenticated
|
|
487
|
-
router's per-route auth guard surfaces `<AuthRoot>`
|
|
488
|
-
protected route. If the token is present, it
|
|
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
|
|
492
|
-
if it returns a fresh code, the cookie
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
|
504
|
-
`useApp()` inside any page to access the active user,
|
|
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
|
|
513
|
-
showPopup, //
|
|
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
|
|
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
|
|
523
|
-
pass a custom option through to a custom `loadingOverlay` element.
|
|
524
|
-
`cloneElement`s the overlay with `{ label, asyncOptions
|
|
525
|
-
pending. `useAppOptional()` returns `null`
|
|
526
|
-
returns a localizer that prefers
|
|
527
|
-
`AppProvider` `localizer`
|
|
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
|
|
531
|
-
flat overlay layer returning a string id, for
|
|
532
|
-
the router transition). Prefer the
|
|
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`
|
|
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
|
|
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
|
|
578
|
-
provider as primary; if `providers.google.clientId` is
|
|
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
|
|
590
|
-
cookie and 302-redirect to the same URL without the token
|
|
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
|
|
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`,
|
|
652
|
-
All keys are dot-notation, fully typed against the collection's
|
|
653
|
-
Partial updates go through optimistic concurrency to prevent
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
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
|
|
739
|
-
Pass `UGLY_BOT_TOKEN` in the environment and the
|
|
740
|
-
balance tracking, retries, and per-user
|
|
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`
|
|
763
|
-
`ugly-app` — the platform supports Claude, GPT, Gemini, Together,
|
|
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
|
|
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();
|
|
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
|
|
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
|
|
815
|
-
your app server (see the "STT/TTS routes to ugly.bot" rule in
|
|
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
|
|
850
|
-
via the built-in `uploadUrl` framework request and
|
|
851
|
-
uploads go through a same-origin `/_s3`
|
|
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
|
|
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
|
|
857
|
-
are not exposed (browser uploads must go through a Worker
|
|
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`,
|
|
890
|
-
`description`. Workers without a schedule are still
|
|
891
|
-
`POST /_workers/run` (auth: localhost in dev,
|
|
892
|
-
`Authorization: Bearer $CRON_SECRET` in prod). Scheduled workers also
|
|
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
|
|
896
|
-
Triggers and durable queueing goes through Cloudflare Queues. On
|
|
897
|
-
adapter, `enqueueWorker` runs the handler inline (in-process
|
|
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__`,
|
|
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
|
|
940
|
-
unauthenticated users). The framework's `initSession`
|
|
941
|
-
requests automatically tag events with the user's
|
|
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
|
|
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`,
|
|
998
|
-
`setCronTasks`, `setWorkers`) don't change
|
|
999
|
-
boots via `npm run dev:workers`,
|
|
1000
|
-
|
|
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
|
|
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
|
|
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
|