@tomorrowos/sdk 1.0.1 → 1.1.0

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/VERCEL_SETUP.md CHANGED
@@ -1,1056 +1,1087 @@
1
- # Vercel Setup Protocol for TomorrowOS CMS
2
-
3
- > **This file is a contract for Vercel Agent / AI coding assistants setting up TomorrowOS on Vercel.**
4
- > Follow it literally. Ask questions in order. Do not invent secrets. Prefer configuring **Vercel Environment Variables**, `.env` (non-secrets only), `brand.json`, and `server.ts` store wiring over inventing a custom CMS.
5
- >
6
- > **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`).
7
- >
8
- > Protocol id: `vercel-setup/1.15` - pairs with `@tomorrowos/sdk` **0.9.70+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
9
- >
10
- > **Questions source of truth:** `VERCEL_QUESTIONS.md` - Agents must **open that file and paste each Q block verbatim**. Do not paraphrase.
11
- > **Q1/Q2 = choice questions. Q3 = form only:** open **all seven optional inputs at once**, then **HARD STOP and WAIT** for submit or "skip". Never auto-skip because fields are optional. Never ask a branding choice first. Never show only Name + Primary colour. Logo = **Upload** above **Logo URL**.
12
- > **Copy questions verbatim from `VERCEL_QUESTIONS.md`.** Do not invent alternate Q2 options (no "Supabase Storage", no S3, no "disable media", no making Blob the recommended option).
13
- > **`TOMORROWOS_STORE` and `DATABASE_SSL` are agent-only** - set them automatically after Q1; never put them in a user-facing Env form for the customer to type.
14
-
15
- ## NON-NEGOTIABLE VERCEL RUNTIME CONTRACT
16
-
17
- The following rules are **mandatory acceptance criteria**, not suggestions. The Agent must preserve them throughout setup, Preview, Publish, fixes, and upgrades:
18
-
19
- 1. **One TomorrowOS instance only.** Use the shared instance exported through `api/index.ts`. **Never create a second `TomorrowOS` instance in `api/ws.ts`** or any other route.
20
- 2. **Never use `public/` as the Vercel Output Directory.** The dashboard Output Directory must be **empty / unset**. Serve the Control Panel at runtime from `cms-panel/` through TomorrowOS `staticRoot`.
21
- 3. **Preview may use Next only as an isolated reverse-proxy shell under `preview/`.** Preview must not replace TomorrowOS or determine the Production runtime.
22
- 4. **Publish must use the Fluid Function.** Production must run `api/index.ts` ??the shared exported `http.Server`, with Fluid compute enabled. It must not use `next start`, a static export, or a bare `(req, res)` handler.
23
- 5. **After every Production deployment, verify all three gates before saying setup is complete:**
24
- - `GET /status` returns TomorrowOS **JSON**, not HTML.
25
- - `GET /` returns the TomorrowOS **Control Panel**.
26
- - WebSocket upgrade on `/` or `/api` returns **`101 Switching Protocols`**, not `200` HTML.
27
- 6. **Pair devices only against the Production URL.** Do not instruct users to pair against v0 Preview or a local/internal URL.
28
- 7. If any gate fails, **fix and redeploy**. Never describe a deployment as successful based only on the Control Panel loading.
29
-
30
- These rules override any platform-generated recommendation that conflicts with them.
31
-
32
- ## User trigger (copy-paste)
33
-
34
- When a human pastes a prompt like this, **read this file and start Question 1 immediately**:
35
-
36
- ```text
37
- Follow @tomorrowos/sdk (an NPM package) VERCEL_SETUP.md and set up my TomorrowOS CMS from scratch.
38
-
39
- Open VERCEL_QUESTIONS.md first.
40
- For Q1, Q2, Q3: paste each question block VERBATIM. Do not rephrase options.
41
- For Q3: DO NOT ask a branding choice. Open one form with ALL SEVEN optional inputs at once, then HARD STOP and WAIT for my submit or "skip". Never auto-continue with defaults without my reply. Never only Name + Primary colour. Logo = Upload above Logo URL.
42
- After Q1 database choice, YOU set TOMORROWOS_STORE and DATABASE_SSL. Do not ask me to fill those.
43
- After Q2 media storage choice, YOU set TOMORROWOS_MEDIA. Do not ask me to fill those.
44
- Run: npx @tomorrowos/sdk@latest init . --hosting v0 --force
45
- Keep cms-starter-v0 layout.
46
- Apply the NON-NEGOTIABLE VERCEL RUNTIME CONTRACT exactly:
47
- one TomorrowOS instance only
48
- never create a second TomorrowOS in api/ws.ts
49
- never use public/ as Vercel Output Directory
50
- Preview Next only under preview/
51
- Publish must be the Fluid Function
52
- after deploy verify: GET /status ? JSON, GET / ? Control Panel, WebSocket on / or /api ? 101
53
- pair devices against the Production URL only
54
- ```
55
-
56
- Alternate triggers:
57
-
58
- - ?Set up TomorrowOS on Vercel using VERCEL_SETUP.md?
59
- - ?Configure my TomorrowOS CMS (Vercel wizard)?
60
-
61
- When any of these fire, **ignore `REPLIT_SETUP.md`** for the Q&A phase (you may skim Replit docs only for shared SDK semantics).
62
-
63
- ---
64
-
65
- ## IRON RULE: Preview vs Publish (dual runtime)
66
-
67
- > **This is the #1 deployability rule on Vercel. Non-negotiable.**
68
-
69
- TomorrowOS CMS is **one Node process** that serves:
70
-
71
- - Browser Control Panel at `GET /` (`public/` via `staticRoot`)
72
- - Device **WebSocket** on the same origin (`https` ??players use `wss`)
73
-
74
- ### Production / Publish ??Node only (no Next.js)
75
-
76
- | Required | Forbidden |
77
- |----------|-----------|
78
- | Single **Node HTTP server** entry (`tsx server.ts` / exported `http.Server` from `listen()`) | **Next.js as the Production CMS** (`next start`, Next App Router as the live app) |
79
- | **Framework Preset = Other / `null`** (dashboard **and** project settings API) | Leaving Framework Preset as **Next.js** (causes `404: NOT_FOUND`) |
80
- | **Fluid compute** enabled (needed for WebSockets on Vercel) | Classic short-lived serverless with no WebSocket / no Fluid |
81
- | Durable store = **Postgres** (Supabase or Neon, pooled URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
82
- | Media = **Cloudinary** or **Vercel Blob** (durable HTTPS URLs) | Relying on local `public/uploads` alone for production fleets |
83
- | Env vars in **Vercel Project ??Settings ??Environment Variables** | Committing passwords / API secrets / `.env` with secrets |
84
- | `cms.hostingTarget`: `"vercel"` in `brand.json` | Creating `.replit` / Replit artifact files |
85
-
86
- **Publish / Production Deploy** must run **`TomorrowOS.listen(...)`** directly. Do **not** put Next.js in front of Production traffic.
87
-
88
- ### Publish `404: NOT_FOUND` ??mandatory prevention (read before every deploy)
89
-
90
- The production URL showing:
91
-
92
- ```text
93
- 404: NOT_FOUND
94
- Code: NOT_FOUND
95
- ```
96
-
97
- almost always means **Vercel still thinks this is a Next.js / static deploy**, not a Node TomorrowOS server.
98
-
99
- **Before every Production deploy, you MUST verify all of the following:**
100
-
101
- 1. **Project ??Settings ??General ??Framework Preset = Other** (or unset / null).
102
- If it still says **Next.js**, patch it via Vercel API / CLI / dashboard **before** redeploying.
103
- `"framework": null` in `vercel.json` alone is **not enough** if the dashboard still says Next.js.
104
- 2. **Install Command** = `npm install` / `pnpm install` / `yarn` (normal install).
105
- 3. **Build Command** = a **no-op that exits 0** (e.g. `node -e "process.exit(0)"` or `npm run build` where `build` is that no-op).
106
- **Never** run `next build` for Production.
107
- 4. **Production entry** = Fluid **Vercel Function** `api/index.ts` that **`export default server`** from shared `cms-app.ts` (see [WebSockets docs](https://vercel.com/docs/functions/websockets)). Local `npm start` may still use root `server.ts`.
108
- 5. **Output Directory** = **empty / unset**. Do **not** set `outputDirectory: "."` as a fake static site.
109
- 6. **Do** use a single `api/index.ts` that exports the TomorrowOS `http.Server`. **Do not** create a second TomorrowOS in `api/ws.ts`, and **do not** use bare `(req, res)` handlers that drop upgrades.
110
- 7. If Preview needs Next, keep Next files **isolated** so Production auto-detect cannot latch onto them:
111
- - Prefer `preview/` (or similar) for `next.config.*`, `app/`, `pages/` used only by v0 Preview, **or**
112
- - Ensure Production Framework Preset is forced to **Other** and never runs `next` as the CMS.
113
-
114
- **Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND` or WebSocket upgrade returns 200 HTML.
115
-
116
- ### Publish static trap ??`public/` served without Node (read this)
117
-
118
- A common **worse** failure mode than `404: NOT_FOUND`:
119
-
120
- - `GET /` loads the Control Panel HTML (from static `public/index.html`)
121
- - `GET /status`, `/devices`, etc. return **HTML** (Vercel 404 page)
122
- - The panel shows **CMS unreachable** and console:
123
- `Unexpected token 'T', "The page c"... is not valid JSON`
124
-
125
- **Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *?public if it exists??). **No Function** is running TomorrowOS.
126
-
127
- **Fix (Scheme A):** use `cms-panel/` (not `public/`), Fluid Function `api/index.ts` + rewrites, Output Directory empty. Static UI is served **at runtime** via `staticRoot` in `listen()`.
128
-
129
- **Publish gate (mandatory):** after deploy, `GET /status` must return **JSON** starting with `{`, not HTML starting with `<` or `The page`. **And** WebSocket upgrade on `/` or `/api` must return **101**.
130
-
131
- ```bash
132
- curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
133
- # Expected: {"status":"success",...}
134
- # Failure: The page could not be found OR <!DOCTYPE html>
135
- ```
136
-
137
- **Setup is NOT complete** while `/status` returns HTML, the panel shows the JSON parse error, or devices cannot open `wss://`.
138
-
139
- ### Preview (v0 / Vercel Agent sandbox) ??Next.js shell allowed
140
-
141
- v0 Preview is often a **Next.js runtime** hardwired to `next dev`. A raw `tsx server.ts` may listen successfully while Preview still shows a blank page, because the sandbox intercepts the public port with Next before requests reach TomorrowOS.
142
-
143
- **For Preview only**, you **must** keep a thin Next.js host that **reverse-proxies** HTTP to TomorrowOS on an **internal** port.
144
-
145
- | Preview requirement | Detail |
146
- |---------------------|--------|
147
- | Next.js remains the Preview host | Satisfies v0 / sandbox expectations |
148
- | TomorrowOS on internal port | e.g. `TOMORROWOS_INTERNAL_PORT=3001` (not the public Preview port) |
149
- | Transparent HTTP reverse proxy | Next route/middleware forwards `/` (and Control Panel paths) ??`http://127.0.0.1:3001` |
150
- | Start both processes in Preview | Next + `tsx server.ts` (or a `dev:preview` script that starts both) |
151
-
152
- **Preview WebSocket note:** HTTP proxy is enough for Control Panel HTML/API smoke checks. Device `wss` pairing may not work through a Next proxy in Preview ??that is acceptable. **Pair devices against the Production Publish URL**, not v0 Preview.
153
-
154
- **Do not** replace TomorrowOS with a Next-only CMS. Next is a **Preview adapter**, not the product.
155
-
156
- ---
157
-
158
- ## Questionnaire scope (STRICT)
159
-
160
- Ask **only** these questions, in **this exact order**. **Copy the ?Ask exactly??blocks verbatim** ??do not paraphrase options.
161
-
162
- | Step | Section | When |
163
- |------|---------|------|
164
- | 1 | **Question 1** ? Database (Supabase ? Neon) | Always |
165
- | 2 | **Question 2** ? Media (**Cloudinary recommended** ? Vercel Blob) | Always (after Q1) |
166
- | 3 | **Question 3** - Brand form (`brand.json` only; **not a choice** - open all 7 optional inputs at once) | Always last |
167
-
168
- **That is the complete list.** Do **not** ask how many screens / devices.
169
-
170
- **Forbidden invented Question 2 options (never offer these in chat or UI):**
171
-
172
- - ???Vercel Blob **(recommended)**??as option 1 ??Blob is option **2**, not recommended over Cloudinary
173
- - ???Supabase Storage??/ S3 bucket / `SUPABASE_SERVICE_ROLE_KEY` for media
174
- - ???No media storage??/ ?disable media uploads??/ ?remote URLs only??as a numbered choice
175
- - ??Any media list that does not start with **Cloudinary (recommended)**
176
-
177
- **Forbidden Env UX after Question 1:**
178
-
179
- - ??Showing the user empty fields for `TOMORROWOS_STORE` or `DATABASE_SSL` to fill
180
- - ??Asking in chat ?what should TOMORROWOS_STORE be???
181
- - ??After DB choice + URL paste: **you** write those two vars silently (see Q1 Steps B/C/D)
182
-
183
- **Forbidden extra questions (never ask, even if the user skips a step):**
184
-
185
- - ??OpenAI / Anthropic / Gemini / any LLM API key (`sk-?`, etc.)
186
- - ???AI pairing?? ?AI feature logic?? or inventing auth that needs an LLM key
187
- - ??Asking the user to type `TOMORROWOS_STORE` or `DATABASE_SSL`
188
- - ??Any secret not listed in Questions 1?? (DB URL, media credentials, brand)
189
-
190
- TomorrowOS pairing and Control Panel logic live in `@tomorrowos/sdk`. They do **not** call OpenAI.
191
-
192
- If the user **skips** a Question 2 option, do **not** invent a substitute question. Re-state Question 2 choices **verbatim** ? never pivot to LLM keys, Supabase Storage, or local disk uploads.
193
-
194
- If the user says ?just set it up, don?t ask questions,??respond:
195
-
196
- > I need a few quick answers from VERCEL_SETUP.md (database ??Supabase or Neon preferred ??media storage with Cloudinary recommended, and branding). It takes about a minute and ensures the CMS works on Vercel.
197
-
198
- Then ask **Question 1**.
199
-
200
- ---
201
-
202
- ## Hard rules
203
-
204
- 1. **Open `VERCEL_QUESTIONS.md` first.** Paste Q1 ??configure ??paste Q2 ??configure ??paste Q3. Never paraphrase question options. Never use `REPLIT_SETUP.md` as the questionnaire. **Never** ask for OpenAI or other LLM API keys.
205
- 2. **Question 2 options are fixed:** (1) Cloudinary **(recommended)**, (2) Vercel Blob. Do not reorder; do not invent Supabase Storage, local disk, or disable-uploads.
206
- 3. **Cloudinary:** open **one** Vercel Env popup with **all** Cloudinary fields together. Do **not** quiz one credential at a time.
207
- 4. **`TOMORROWOS_STORE` + `DATABASE_SSL` = agent-owned.** After Q1 choice (and URL if needed), set them yourself in Vercel Env Vars. **Never** include them as blank fields for the customer. Mapping: Supabase ? `TOMORROWOS_STORE=supabase` + `DATABASE_SSL=true`; Neon ? `TOMORROWOS_STORE=postgres` + `DATABASE_SSL=true`. **Do not offer SQLite as a questionnaire choice** ? it is ephemeral on Vercel and unsuitable for production.
208
- 5. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
209
- 6. **Postgres env naming:** prefer **`SUPABASE_URL`** for Supabase; prefer **`DATABASE_URL`** for Neon.
210
- 7. **Always prefer pooled connection strings** (Supabase Session pooler **6543**; Neon pooled host).
211
- 8. **Prefer `npx @tomorrowos/sdk@latest init --hosting v0`**. For Replit/Railway use `npx @tomorrowos/sdk@latest init .`. Do not rebuild pairing / WebSocket APIs. Untagged `npx @tomorrowos/sdk init` is forbidden (stale cache).
212
- 9. **Never commit secrets.** Use Vercel Environment Variables.
213
- 10. **Skip Replit-only files.**
214
- 11. **SQLite is not a production store on Vercel.**
215
- 12. **Enable Fluid compute** for Production WebSockets.
216
- 13. **Production:** `api/index.ts` exports `http.Server` from `TomorrowOS.listen`. Not `next start`. No second TomorrowOS in `api/ws.ts`.
217
- 14. **Preview:** Next reverse-proxy shell when v0 is Next-hardwired.
218
- 15. After Q&A: **configure ??install ??deploy ??minimal verify**.
219
- 16. **No inventing CMS login** from a branding URL.
220
- 17. **Skip ??invent.** Stay on Q1?Q3 only.
221
- 18. **Publish gate:** `/status` JSON + WebSocket 101; no static `public/` trap.
222
- 19. **Q3 is a form, not a question ? and you must WAIT.** After Q2, open one dialog with **all seven** optional inputs visible together, then **stop and wait** for submit or explicit **"skip"**. **Never** auto-continue because fields are optional. **Never** ask a preliminary branding choice. **Never** show only Product Name + Primary colour. **Never** use multiple-choice / Option A-B for branding. Logo = **Upload** above **Logo URL**.
223
-
224
- **Question order:** Q1 -> (auto-set store env) -> Q2 -> Q3 form (**wait for user**) -> execution checklist.
225
-
226
- ---
227
-
228
- ## Scheme A ??Vercel Functions + WebSockets (mandatory for Production)
229
-
230
- > **Goal:** Preview may use Next + proxy; **Publish** runs TomorrowOS as a **Vercel Function** (`api/index.ts`) on **Fluid**, with WebSocket support per [Vercel WebSockets docs](https://vercel.com/docs/functions/websockets).
231
- > **Scope:** Layout + `vercel.json` + dashboard. Prefer `npx @tomorrowos/sdk@latest init --hosting v0` (`cms-starter-v0`).
232
- > **Do not** break Replit/Railway: those keep using default `init` ??`cms-starter` (root `server.ts` + `server.listen`, no `api/`).
233
-
234
- ### Why Scheme A exists
235
-
236
- | Layer | Preview (v0) | Publish (Production) |
237
- |-------|----------------|----------------------|
238
- | Host | Next.js shell + HTTP proxy | **Vercel Function** `api/index.ts` ??`export default server` |
239
- | Static UI | Proxied from TomorrowOS | Served by **`staticRoot`** (`cms-panel/`) inside `listen()` ??**not** Vercel Output Directory |
240
- | WebSocket | Optional / may fail in Preview | **Required** ??Fluid Function upgrade (`wss://` ??`/` rewritten to `/api`, or direct `/api`) |
241
- | Next files | Allowed under `preview/` | **Must not** live at project root |
242
-
243
- **Root cause of ?Control Panel works, TV cannot connect??** HTTP can be served without WebSocket upgrades. Classic root-only `server.ts` / plain serverless handlers often return **200 HTML** on `Upgrade` instead of **101**. Official pattern is a Function that exports `http.Server` with `ws` attached ([docs](https://vercel.com/docs/functions/websockets)).
244
-
245
- ### A1 ??Project layout (Agent must enforce)
246
-
247
- **Production (Vercel / v0 starter):**
248
-
249
- ```
250
- cms-app.ts ??shared TomorrowOS.listen + export const server
251
- api/index.ts ??export { server as default } from "../cms-app.js"
252
- server.ts ??local/Preview: same default export (npm start / internal port)
253
- vercel.json ??fluid + rewrites + functions.maxDuration
254
- package.json
255
- brand.json
256
- cms-panel/ ??CMS static UI (NOT named public/ ??avoids static-output trap)
257
- preview/ ??Next proxy for v0 Preview only
258
- ```
259
-
260
- **Preview only ??keep ALL Next under `preview/`:**
261
-
262
- ```
263
- preview/
264
- next.config.mjs
265
- app/
266
- ??
267
- ```
268
-
269
- **Do NOT:**
270
-
271
- - ??Leave `next.config.mjs` + `app/` at project root
272
- - ??Set Output Directory to `public`, `cms-panel`, or `.`
273
- - ??Create a **second** TomorrowOS instance in `api/ws.ts` (separate isolate = broken pairing memory)
274
- - ??Use `(req, res) =>` serverless handlers that drop `upgrade`
275
- - ??Put `startCommand` / `processes` in `vercel.json` (v0 schema rejects them)
276
-
277
- **`package.json` scripts:**
278
-
279
- ```json
280
- {
281
- "scripts": {
282
- "dev": "tsx watch server.ts",
283
- "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
284
- "dev:tomorrowos": "cross-env TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
285
- "dev:next": "cd preview && next dev -p 3000",
286
- "start": "tsx server.ts",
287
- "build": "node -e \"process.exit(0)\""
288
- }
289
- }
290
- ```
291
-
292
- - **Local / Railway-style:** `npm start` ??`tsx server.ts` ??SDK calls `server.listen` (no `VERCEL` env).
293
- - **Vercel Production:** Function loads `api/index.ts`; SDK skips `listen` when `process.env.VERCEL` is set and Vercel owns the socket.
294
-
295
- ### A2 ??Function entry (matches Vercel WebSockets docs)
296
-
297
- ```ts
298
- // api/index.ts
299
- export { server as default } from "../cms-app.js";
300
- ```
301
-
302
- ```ts
303
- // cms-app.ts (shared)
304
- const server = tomorrowos.listen({
305
- port: Number(process.env.PORT) || 3000,
306
- host: "0.0.0.0",
307
- staticRoot: join(__dirname, "cms-panel"),
308
- });
309
- export { server, tomorrowos };
310
- ```
311
-
312
- SDK behaviour (0.9.32+):
313
-
314
- - Accepts WebSocket upgrades on `/`, `/api`, `/api/ws` (players may use either)
315
- - When `VERCEL` is set: **`autoListen` defaults to false** ??do not bind a port yourself
316
- - Pattern aligns with docs: create `http.Server`, attach `ws`, **`export default server`**
317
-
318
- ### A3 ??`vercel.json` (Production)
319
-
320
- ```json
321
- {
322
- "fluid": true,
323
- "framework": null,
324
- "installCommand": "npm install",
325
- "buildCommand": "npm run build",
326
- "functions": {
327
- "api/index.ts": {
328
- "maxDuration": 300,
329
- "memory": 1024,
330
- "includeFiles": "{cms-panel/**,brand.json,assets/**}"
331
- }
332
- },
333
- "rewrites": [
334
- { "source": "/((?!api/).*)", "destination": "/api" }
335
- ]
336
- }
337
- ```
338
-
339
- **`maxDuration` plan limits:** Hobby / many default plans allow **1??00** seconds. **800** requires a higher Vercel plan. Prefer **`300`** unless the account already allows more ??otherwise Publish fails with *?maxDuration must be between 1 and 300??upgrade your plan??.
340
-
341
- **Why rewrites:** Public URLs stay `https://app.vercel.app/` and `/status` while the Function mount is `/api`. Device `wss://app.vercel.app/` is rewritten to the same Function so one isolate handles HTTP + WS.
342
-
343
- **v0 schema:** never add `startCommand` or `processes`.
344
-
345
- **Critical:**
346
-
347
- - `"fluid": true` is required for WebSockets
348
- - **Do not** set `outputDirectory` to `cms-panel` / `public`
349
- - `"buildCommand"` = no-op ??**never** `next build` for Production
350
- - Dashboard Output Directory must stay **empty**
351
-
352
- ### A4 ??Vercel Project Settings (dashboard ??mandatory)
353
-
354
- | Setting | Required value |
355
- |---------|----------------|
356
- | Framework Preset | **Other** / null ??**not Next.js** |
357
- | Install Command | `npm install` |
358
- | Build Command | `npm run build` (no-op) |
359
- | **Output Directory** | **empty** |
360
- | Fluid compute | **On** |
361
- | WebSockets | Account must allow Functions WebSockets (see docs ?Permissions Required?? |
362
-
363
- ### A5 ??Publish acceptance (all must pass)
364
-
365
- 1. `curl -s https://PROD/status` ??JSON (`{"status":...}`)
366
- 2. `curl -s https://PROD/` ??Control Panel HTML
367
- 3. **WebSocket upgrade returns 101**, not 200 HTML:
368
-
369
- ```bash
370
- curl.exe -i --http1.1 ^
371
- -H "Connection: Upgrade" -H "Upgrade: websocket" ^
372
- -H "Sec-WebSocket-Version: 13" ^
373
- -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" ^
374
- "https://PROD/"
375
- # Also try: https://PROD/api
376
- ```
377
-
378
- 4. Control Panel does **not** show CMS unreachable / `Unexpected token 'T'`
379
- 5. Fluid on; Framework = Other
380
-
381
- If (1)+(2) pass but (3) fails ??Function is serving HTTP only. Re-check Fluid, `api/index.ts` default export, rewrites, and WebSockets permission. **Do not** invent a second `api/ws.ts` TomorrowOS instance.
382
-
383
- ### A6 ??What Scheme A does **not** require
384
-
385
- - ??No Next.js as Production CMS
386
- - ??No replacing SDK transport with hand-rolled pairing
387
- - ??No SQLite as production store on Vercel
388
- - ??No changes to Replit `cms-starter` layout for Vercel-only fixes
389
-
390
- ### A7 ??If v0 Publish still fails (Path B / C)
391
-
392
- **Path B:** Deploy with `vercel --prod` or Git integration (full Vercel), same Scheme A layout.
393
-
394
- **Path C:** Host CMS on **Railway / Fly.io / Replit** (long-lived Node). Use Vercel only for Preview if needed.
395
-
396
- ### A8 ??Migrate an existing broken Vercel project
397
-
398
- 1. Scaffold/compare with `cms-starter-v0` (`init --hosting v0`) or copy:
399
- - `cms-app.ts`, `api/index.ts`, `server.ts`, `vercel.json`
400
- - rename `public/` ??`cms-panel/` if still present
401
- 2. Ensure **one** Function exports the **same** `server` from `cms-app.ts`
402
- 3. Fluid on ??Redeploy
403
- 4. Verify A5 (especially WebSocket 101)
404
- 5. On TVs: enter `https://YOUR.vercel.app/` (players also try `/api` automatically on vercel.app hosts)
405
-
406
- ---
407
-
408
- ## Runtime & Vercel deploy rules (mandatory)
409
-
410
- ### Production runtime (Publish)
411
-
412
- - Serve TomorrowOS via **Vercel Function** `api/index.ts` ??`export default server` (Fluid + WebSockets).
413
- - Keep root **`server.ts`** for **local** `npm start` / Preview internal port only (same `cms-app.ts`).
414
- - Keep **`@tomorrowos/sdk`** (0.9.32+) and put **`tsx` in `dependencies`** for local start.
415
- - Node **20+**.
416
- - **Do not** replace the SDK with a hand-rolled server that drops WebSocket upgrades.
417
- - **Forbidden as the Production CMS:** `next start`, static export only, or a bare `(req,res)` handler without `http.Server` + `ws`.
418
-
419
- ### Expected `package.json` scripts
420
-
421
- ```json
422
- {
423
- "scripts": {
424
- "dev": "tsx watch server.ts",
425
- "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
426
- "dev:tomorrowos": "cross-env PORT=3001 TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
427
- "dev:next": "cd preview && next dev -p 3000",
428
- "start": "tsx server.ts",
429
- "build": "node -e \"process.exit(0)\""
430
- }
431
- }
432
- ```
433
-
434
- Adapt script names / process runners as needed. **Invariant:**
435
-
436
- - **Local `npm start`** = TomorrowOS Node with `server.listen`.
437
- - **Vercel Production** = Function `api/index.ts` (SDK `autoListen=false` when `VERCEL` is set).
438
- - **`build`** = no-op exit 0 (never `next build` for Production).
439
- - **Preview / v0** = Next on public port + TomorrowOS on internal port + proxy.
440
-
441
- ### Minimal dependencies (Production)
442
-
443
- ```json
444
- {
445
- "type": "module",
446
- "engines": {
447
- "node": ">=20"
448
- },
449
- "dependencies": {
450
- "@tomorrowos/sdk": "latest",
451
- "dotenv": "^17.2.3",
452
- "tsx": "^4.19.0"
453
- },
454
- "devDependencies": {
455
- "@types/node": "^20.0.0",
456
- "typescript": "^5.5.0"
457
- },
458
- "scripts": {
459
- "dev": "tsx watch server.ts",
460
- "start": "tsx server.ts",
461
- "build": "node -e \"process.exit(0)\""
462
- }
463
- }
464
- ```
465
-
466
- Use the latest published `@tomorrowos/sdk` when scaffolding (do not invent versions).
467
-
468
- Add nothing extra for Blob when using `@tomorrowos/sdk@0.9.70+` (`@vercel/blob` is bundled). Only ensure `BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob` when the user chooses **Vercel Blob** in Question 2.
469
-
470
- For Preview shell only, add as needed: `next`, `react`, `react-dom`, and a process runner (`concurrently` or equivalent). These must **not** become the Production CMS.
471
-
472
- ### `server.ts` pattern (Vercel-adapted ??Production)
473
-
474
- Keep the server at **project root** (e.g. `server.ts` / `server.mts`). Do **not** move Production into `api/` solely to satisfy Vercel routing myths.
475
-
476
- ```ts
477
- import "dotenv/config";
478
- import { readFileSync } from "fs";
479
- import { fileURLToPath } from "url";
480
- import { dirname, join } from "path";
481
- import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
482
-
483
- const __dirname = dirname(fileURLToPath(import.meta.url));
484
- const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
485
-
486
- const store = createTomorrowOSStore({
487
- // Supabase: SUPABASE_URL (pooler). Neon: DATABASE_URL (pooled).
488
- databaseUrl:
489
- process.env.SUPABASE_URL ||
490
- process.env.NEON_DATABASE_URL ||
491
- process.env.DATABASE_URL,
492
- sqlitePath: join(__dirname, "data", "tomorrowos.db") // local/dev fallback only
493
- });
494
-
495
- const tomorrowos = new TomorrowOS({ brand, store });
496
-
497
- const port =
498
- Number(process.env.TOMORROWOS_INTERNAL_PORT) ||
499
- Number(process.env.PORT) ||
500
- 3000;
501
-
502
- const server = tomorrowos.listen({
503
- port,
504
- host: "0.0.0.0",
505
- staticRoot: join(__dirname, "public")
506
- });
507
-
508
- // Vercel / Node server capture: export the HTTP server, not only TomorrowOS.
509
- export default server;
510
- ```
511
-
512
- - **Production:** `PORT` from Vercel ??TomorrowOS listens on that port (no Next).
513
- - **Preview:** set `TOMORROWOS_INTERNAL_PORT=3001` (or similar) so Next can own `:3000` and proxy inward.
514
-
515
- Load env with `dotenv` for local/`vercel dev`. On production Deploy, Vercel injects Environment Variables natively.
516
-
517
- ### Preview adapter (v0 / Next reverse proxy) ??required when Preview is Next-hardwired
518
-
519
- Detect Preview / v0 (Next already present, or Preview blank while TomorrowOS logs ?listening??. Then:
520
-
521
- 1. Keep a **minimal** Next app **isolated when possible** (e.g. under `preview/`) so Production Framework detection does not latch onto root `next.config.*`.
522
- 2. TomorrowOS listens on **`127.0.0.1:TOMORROWOS_INTERNAL_PORT`** (default **3001**).
523
- 3. Add a **catch-all** Next route (or middleware) that proxies HTTP to TomorrowOS:
524
-
525
- ```ts
526
- // Example: preview app proxy handler
527
- // Forward method, headers, and body to http://127.0.0.1:3001/<path>
528
- // Return the upstream status, headers, and body to the browser.
529
- ```
530
-
531
- Preferred UX: proxy **`/`** itself (rewrites) so the Preview URL shows the Control Panel at `/`, not only under `/api/...`.
532
-
533
- 4. Start **both** processes for Preview (`dev:preview` pattern above).
534
- 5. Document in the final summary: **Preview = Next shell + proxy; Publish = Node only.**
535
-
536
- **Forbidden:**
537
-
538
- - Making Next the Production CMS or the only server after Publish.
539
- - Deleting TomorrowOS `server.ts` because Preview needed Next.
540
- - Claiming setup complete if Production entrypoint is still `next start`.
541
- - Claiming setup complete if Production `GET /` is `404: NOT_FOUND`.
542
- - Creating root `api/index.*` + `routes` catch-all that exports a raw `http.Server` ?to fix 404??
543
-
544
- ### `vercel.json` (Production ??explicit Node, not Next)
545
-
546
- Write `vercel.json` so Production cannot be mistaken for Next. Minimum:
547
-
548
- ```json
549
- {
550
- "fluid": true,
551
- "framework": null,
552
- "installCommand": "npm install",
553
- "buildCommand": "npm run build"
554
- }
555
- ```
556
-
557
- Use `pnpm` / `yarn` variants of install/build if that is the project?s package manager.
558
-
559
- **Do not** add `startCommand`, `processes`, or `outputDirectory` to `vercel.json` when v0 schema rejects them. Start = `package.json` `"start": "tsx server.ts"`. Output Directory = **empty in dashboard**.
560
-
561
- **Also force Project Settings (dashboard or Vercel API) ??this is mandatory (see Scheme A4):**
562
-
563
- | Setting | Required value |
564
- |---------|----------------|
565
- | Framework Preset | **Other** / null ??**not Next.js** |
566
- | Install Command | `npm install` (or pnpm/yarn) |
567
- | Build Command | `npm run build` (no-op exit 0) |
568
- | Output Directory | **empty / null** ??**NOT `public`** |
569
- | Start Command | `npm run start` ??`tsx server.ts` (or `.mts`) ??set in **dashboard**, not `vercel.json` |
570
- | Fluid compute | **On** |
571
-
572
- If `next` / `next.config.*` exist for Preview and the dashboard still auto-selects Next.js:
573
-
574
- 1. Patch Framework Preset to **Other** via API/CLI/dashboard.
575
- 2. Redeploy.
576
- 3. Hit Production `GET /status` ??must be **JSON**, not HTML.
577
- 4. Hit Production `GET /` ??must be Control Panel HTML, not `404: NOT_FOUND`.
578
-
579
- If Vercel?s UI and this file disagree on bundling, **prefer whatever keeps a single long-lived Node HTTP server with WebSocket upgrade on `/` for Production**. Document the adaptation in the final summary.
580
-
581
- ### WebSocket / multi-instance note
582
-
583
- - Device sockets are pinned to **one** function/instance. TomorrowOS device maps are **in-memory**.
584
- - For large fleets on multi-instance Vercel, prefer **one stable instance / region**, or accept that reconnects may land on another instance until the product adds shared presence (out of scope for this setup).
585
- - Always use **Postgres** (Supabase or Neon) so pairings/playlists survive cold starts and deploys.
586
- - **Pair TVs to the Production HTTPS origin**, not the v0 Preview URL.
587
-
588
- ### What not to create
589
-
590
- - ??`.replit`, `.replit-artifact`
591
- - ??Next.js as the **Production** CMS (App Router replacing TomorrowOS)
592
- - ??Root `api/` catch-all exporting raw `http.Server` to ?fix??Publish 404
593
- - ??Separate ?API project??without the Control Panel
594
- - ??Committing database passwords, Cloudinary secrets, or Blob tokens
595
-
596
- ---
597
-
598
- ## Question 1 ? Database (always)
599
-
600
- > **Do not** ask screen counts. Present both options; recommend **Supabase** for Vercel fleets.
601
- > **After the user picks a database:** collect the URL if needed, then **you** set `TOMORROWOS_STORE` + `DATABASE_SSL` (see mapping below). The customer must **never** be asked to type those two names or values.
602
- > **Forbidden:** offering SQLite as a user-facing Q1 choice (ephemeral filesystem on Vercel).
603
-
604
- ### Agent-owned env mapping (mandatory)
605
-
606
- | User chose | You set (silently) | User only provides |
607
- |------------|--------------------|--------------------|
608
- | 1 Supabase | `TOMORROWOS_STORE=supabase`, `DATABASE_SSL=true` | `SUPABASE_URL` (pooler string) |
609
- | 2 Neon | `TOMORROWOS_STORE=postgres`, `DATABASE_SSL=true` | `DATABASE_URL` (pooled string) |
610
-
611
- If you open an Env configuration UI for Q1, fields visible to the user may include **only** `SUPABASE_URL` or `DATABASE_URL`. **Do not** show blank `TOMORROWOS_STORE` / `DATABASE_SSL` inputs ? write those yourself in the same step.
612
-
613
- ### Step A ? Ask storage choice
614
-
615
- **Ask exactly:**
616
-
617
- > Which database should TomorrowOS use on Vercel?
618
- >
619
- > **1. Supabase Postgres (recommended)** ? durable pairing/playlists; use the **Session pooler** URL (`*.pooler.supabase.com:6543`).
620
- >
621
- > **2. Neon Postgres** ? serverless Postgres native to Vercel; use Neon's **pooled** connection string from the Neon dashboard (not the direct un-pooled host for serverless).
622
- >
623
- > Reply with **1** or **2** (or "Supabase" / "Neon").
624
-
625
- ### Step B ??If **1 / Supabase**
626
-
627
- **Ask exactly (connection string only ??not TOMORROWOS_STORE / DATABASE_SSL):**
628
-
629
- > Paste your Supabase Postgres connection string. I will store it as **`SUPABASE_URL`**. I will set **`TOMORROWOS_STORE`** and **`DATABASE_SSL`** for you automatically.
630
- >
631
- > In Supabase: **Project Settings ??Database ??Connection string ??Connection pooling** (Session mode).
632
- > Preferred shape:
633
- > `postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres`
634
- >
635
- > **Do not** use the direct host `db.*.supabase.co:5432` for Vercel.
636
-
637
- **You must then (automatic ??do NOT ask the user to type store/ssl vars):**
638
-
639
- 1. Set Vercel Env Vars in one step:
640
- - `SUPABASE_URL=<user pooler string>` ??from user
641
- - `TOMORROWOS_STORE=supabase` ??**you set; never ask**
642
- - `DATABASE_SSL=true` ??**you set; never ask**
643
- 2. Wire `cms-app.ts` / `server.ts` store as in **Runtime & Vercel deploy rules**.
644
- 3. Optional committed `.env.example` with **placeholders only**.
645
- 4. **Do not** commit the real connection string.
646
- 5. **Do not** open a form whose blank fields include `TOMORROWOS_STORE` or `DATABASE_SSL`.
647
-
648
- ### Step C ??If **2 / Neon**
649
-
650
- **Ask exactly (connection string only):**
651
-
652
- > Paste your **Neon pooled** Postgres connection string. I will store it as **`DATABASE_URL`**. I will set **`TOMORROWOS_STORE`** and **`DATABASE_SSL`** for you automatically.
653
- >
654
- > In Neon: **Dashboard ??Connection details ??Pooled connection**.
655
- > Typical shape:
656
- > `postgresql://[user]:[password]@[endpoint]-pooler.[region].aws.neon.tech/[dbname]?sslmode=require`
657
-
658
- **You must then (automatic):**
659
-
660
- 1. Set Vercel Env Vars:
661
- - `DATABASE_URL=<user pooled string>` ??from user
662
- - `TOMORROWOS_STORE=postgres` ??**you set; never ask**
663
- - `DATABASE_SSL=true` ??**you set; never ask**
664
- 2. Wire `createTomorrowOSStore` for Postgres.
665
- 3. **Do not** commit the real connection string.
666
- 4. **Do not** ask the user to fill `TOMORROWOS_STORE` or `DATABASE_SSL` in chat or Env UI.
667
- 5. If you see `ENETUNREACH` on `:5432`, switch to Neon's **pooled** URL.
668
-
669
- **Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`** (all Q1 branches).
670
-
671
- ---
672
-
673
- ## Question 2 ??Media storage
674
-
675
- > **This question is only about media files (images/videos).**
676
- > It is **not** about OpenAI, AI pairing, or LLM keys. If you are about to ask for an `sk-` key, **stop** ??you are off-protocol.
677
- > **Copy the two options below verbatim.** Wrong examples that must **never** appear: "Vercel Blob (recommended)", "Supabase Storage", "No media storage / disable uploads", "local disk only".
678
-
679
- ### Step A ? Ask storage choice
680
-
681
- **Ask exactly (wording must match ? Cloudinary is recommended):**
682
-
683
- > How should playlist media (images/videos) be stored?
684
- >
685
- > **1. Cloudinary (recommended)** ? durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection. **Prefer this on Vercel.**
686
- >
687
- > **2. Vercel Blob** ? Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Use when you want media on the same Vercel project without a Cloudinary account.
688
- >
689
- > Reply with **1** or **2** (or "Cloudinary" / "Vercel Blob").
690
-
691
- If you catch yourself about to offer Blob-as-recommended, Supabase Storage, local disk, or "no media" ? **stop and paste the block above instead**.
692
-
693
- ### Step B ??If **1 / Cloudinary** (same Question 2 ??**one Env popup**)
694
-
695
- > **Critical UX rule:** Immediately open the **Vercel Environment Variables configuration popup / form** with **all Cloudinary fields in one UI**. Do **not** ask for cloud name, then key, then secret in separate chat messages.
696
-
697
- **Preferred:** Vercel Env Vars UI (or Agent secrets form) with these fields together:
698
-
699
- | Field | Env name | Required |
700
- |-------|----------|----------|
701
- | Cloud name | `CLOUDINARY_CLOUD_NAME` | Yes |
702
- | API Key | `CLOUDINARY_API_KEY` | Yes |
703
- | API Secret | `CLOUDINARY_API_SECRET` | Yes |
704
- | Folder (optional) | `CLOUDINARY_FOLDER` | No (e.g. `tomorrowos`) |
705
-
706
- **If a multi-field popup is unavailable, ask once in a single message** (still not three turns):
707
-
708
- > Paste Cloudinary credentials from [cloudinary.com/console](https://cloudinary.com/console) (I will store them as Vercel Env Vars):
709
- >
710
- > 1. `CLOUDINARY_CLOUD_NAME`
711
- > 2. `CLOUDINARY_API_KEY`
712
- > 3. `CLOUDINARY_API_SECRET`
713
- > 4. optional `CLOUDINARY_FOLDER`
714
-
715
- **You must then:**
716
-
717
- 1. Save all three required Env Vars for Production + Preview + Development as appropriate.
718
- 2. **Do not** invent placeholders.
719
- 3. **Do not** proceed to Question 3 until all three required vars exist.
720
- 4. **Do not** run a formal Cloudinary upload test unless the user asks.
721
-
722
- The SDK auto-detects these env vars.
723
-
724
- ### Step C - If **2 / Vercel Blob** (same Question 2)
725
-
726
- **Ask exactly:**
727
-
728
- > I will enable **Vercel Blob** for media uploads.
729
- >
730
- > 1. In the Vercel project: **Storage ? Create ? Blob** (or link an existing Blob store to this project).
731
- > 2. Confirm **`BLOB_READ_WRITE_TOKEN`** is available (Vercel usually injects it when Blob is linked).
732
- >
733
- > Paste the token only if it is not already set in your project Env Vars. Do you already have Blob linked on this Vercel project? (yes / no)
734
-
735
- **You must then:**
736
-
737
- 1. Set Vercel Env Var: `BLOB_READ_WRITE_TOKEN=<token>` (if not auto-injected). Prefer the Env popup when available.
738
- 2. Set Vercel Env Var yourself (agent-owned): `TOMORROWOS_MEDIA=vercel-blob`. **Do not** ask the user to type this.
739
- 3. Ensure `@tomorrowos/sdk@0.9.70+` is installed (`@vercel/blob` is a transitive dependency ? no project-level Blob bridge / custom `put()` middleware).
740
- 4. The SDK **natively** uploads via Vercel Blob when the token is present (same HTTP routes as local: `/media/upload` and chunked complete). Returned asset URLs are absolute `https://*.public.blob.vercel-storage.com/...`.
741
- 5. Confirm Control Panel **Media Server** shows provider **Blob** (not Local). Do **not** invent status copy about "Blob bridge" or paste the URL pattern as a status detail.
742
- 6. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
743
-
744
- **Forbidden for Q2=Blob:** scaffolding a custom "Blob bridge", writing status text like "uploads persist to durable https://*.public.blob.vercel-storage.com/ URLs via the Blob bridge", or leaving uploads on local `cms-panel/uploads`.
745
- ---
746
-
747
- ## Question 3 - Brand form (`brand.json` only; **not a choice question**; **must wait**)
748
-
749
- > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, `vercel.json`, or `server.ts` store wiring beyond what Q1-Q2 already required.
750
- >
751
- > **IRON RULE - form, not question; show then WAIT:** As soon as Q2 is done, **open the brand form**. Do **not** ask a choice first. Show **all seven optional fields on one screen at the same time**. Then **HARD STOP** until the user submits the form, replies with values, or says **"skip"**.
752
- >
753
- > **Optional fields ? optional step.** Leaving a field blank keeps the starter default **after the user responds**. It does **not** authorize you to skip waiting or auto-apply defaults in the same turn.
754
- >
755
- > - **Forbidden:** any preliminary question ("customize branding?", "skip or set brand?", Option A/B)
756
- > - **Forbidden:** multiple-choice / radio / "reply 1 or 2" for branding
757
- > - **Forbidden:** showing only Product Name + Primary colour (or any subset under 7)
758
- > - **Forbidden:** field-by-field Next wizard (name -> wait -> tagline -> ...)
759
- > - **Forbidden:** one freeform / Other box for all brand values
760
- > - **Forbidden:** auto-continuing to init / scaffold / env / deploy because "fields are optional" or "I shouldn't re-ask"
761
- > - **Required:** six text inputs + Logo Upload above Logo URL, all visible together
762
- > - **Required:** wait for one user action (submit / values / "skip") before scaffolding continues
763
- >
764
- > **Self-check:** If the UI you are about to show has fewer than six text fields for name/tagline/four colours, **abort and rebuild** - that UI is off-protocol.
765
- >
766
- > **Every field is optional** once the form is shown. After the user submits (including all-blank) or says "skip": keep starter **`brand.json`** for blanks; **do not** re-ask Q3 a second time. **Do not** invent a second branding prompt.
767
- >
768
- > Default TomorrowOS Control Panel has **no login**. Do **not** scaffold login/auth unless the user explicitly asks.
769
-
770
- ### Preferred UI (open immediately - all seven at once - then wait)
771
-
772
- Open **one** dialog titled **Brand your TomorrowOS experience**. Render **each row as its own control**. All rows must appear **together** on first paint - not revealed after a choice, not after filling a previous field. After opening, **end your turn** and wait.
773
-
774
- | # | Exact label | Control type | Blank default |
775
- |---|-------------|--------------|---------------|
776
- | 1 | **Product Name** | text input | existing `brand.json` `name` (e.g. `My Venue`) |
777
- | 2 | **Tagline** | text input | existing tagline (e.g. `Digital signage`) |
778
- | 3 | **Primary colour** | text input (hex) | e.g. `#FF8A3D` |
779
- | 4 | **Background colour** | text input (hex) | e.g. `#FAFAF9` |
780
- | 5 | **Text colour** | text input (hex) | e.g. `#0A0908` |
781
- | 6 | **Secondary colour** | text input (hex) | e.g. `#F5F3EF` |
782
- | 7 | **Logo** | **Upload** (SVG/PNG file picker) **above** **Logo URL** (text input) | keep `./assets/logo.svg` |
783
-
784
- Helper text: *All fields optional. Leave blank to keep starter brand.json defaults. Submit once, or reply "skip". I will wait before continuing setup.*
785
-
786
- **Logo UX:** Upload control directly above Logo URL. Either is fine; if both provided, prefer the uploaded file.
787
-
788
- ### If a form UI is unavailable - one chat message only (still wait)
789
-
790
- Only when the platform **cannot** render multi-field forms. Do **not** ask a choice first. Send **exactly one** message listing all seven fields, then **end your turn and wait** (user may answer partially, attach a logo, or say "skip"):
791
-
792
- > Brand your TomorrowOS experience (**all optional - reply once**). Leave any field blank to keep starter `brand.json` defaults. I will wait for your reply before scaffolding:
793
- >
794
- > 1. **Product Name**
795
- > 2. **Tagline**
796
- > 3. **Primary colour** (hex)
797
- > 4. **Background colour** (hex)
798
- > 5. **Text colour** (hex)
799
- > 6. **Secondary colour** (hex)
800
- > 7. **Logo** - attach SVG/PNG or paste a Logo URL
801
- >
802
- > Or reply **skip** to keep all defaults.
803
-
804
- ### After the user submits
805
-
806
- **You must:**
807
-
808
- 1. For each blank field, **keep the existing starter `brand.json` value** - do not invent new colours/names.
809
- 2. For filled fields, write them into `brand.json` only.
810
- 3. If Logo is a URL: fetch into `./assets/logo.png` or `./assets/logo.svg` when possible and set `logoPath`; if fetch fails, keep existing `logoPath` and note it.
811
- 4. If Logo is an upload: save under `./assets/` and set `logoPath` (prefer upload over URL when both are present).
812
- 5. Always set **`cms.hostingTarget`: `"vercel"`** without asking.
813
- 6. Default `cms.expectedScreens` to `5` unless the user already volunteered a number.
814
- 7. Show a one-line summary (name + primary hex + logo path) - **do not** ask a second branding question.
815
- 8. **Do not** build login, signup, OAuth, or clone a marketing site from branding answers.
816
-
817
- ### Write `brand.json`
818
-
819
- Validate mentally against `brand.schema.json`. Minimum shape (values = user input **or** existing defaults):
820
-
821
- ```json
822
- {
823
- "name": "<Product Name or existing brand.json name>",
824
- "tagline": "<Tagline or existing>",
825
- "targetPlatforms": ["tizen"],
826
- "primaryColor": "#FF8A3D",
827
- "secondaryColor": "#F5F3EF",
828
- "backgroundColor": "#FAFAF9",
829
- "textColor": "#0A0908",
830
- "logoPath": "./assets/logo.svg",
831
- "fontFamily": "Inter",
832
- "cms": {
833
- "useCase": "other",
834
- "hostingTarget": "vercel",
835
- "expectedScreens": 5,
836
- "features": {
837
- "bulkCommands": false,
838
- "proofOfPlay": false,
839
- "contentScheduling": true,
840
- "userManagement": false
841
- }
842
- },
843
- "protocolVersion": "1.0"
844
- }
845
- ```
846
-
847
- Set `cmsEndpoint` only if they already know the public `https://?vercel.app` URL; otherwise tell them to pair TVs with the **Published** HTTPS origin (player maps `https://` ? `wss://`).
848
-
849
- ---
850
-
851
- ## After all answers ? execution checklist
852
-
853
- ### A. Seed the project
854
-
855
- ```bash
856
- npx @tomorrowos/sdk@latest init .
857
- # If not empty and user confirms starter overwrite:
858
- # npx @tomorrowos/sdk@latest init . --force
859
- ```
860
-
861
- Then (**Scheme A ??mandatory for Publish**):
862
-
863
- 1. Keep **`server.ts`** at **project root** with top-level `listen()` + `export default server`.
864
- 2. If Preview needs Next: **move** `next.config.*`, `app/`, Next `tsconfig` / `postcss` / `next-env.d.ts` into **`preview/`** ??remove them from root so Production is not mistaken for Next/static.
865
- 3. Write `vercel.json` per **Scheme A3** (`fluid`, `framework: null`, no-op `buildCommand` ??**no** `startCommand`).
866
- 4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty, Start = `npm run start`, Fluid on). Patch dashboard via API if it still says Next.js.
867
- 5. Do **not** add root `api/` catch-all for raw `http.Server`.
868
- 6. Do **not** add `.replit*`.
869
- 7. If Q2 = Vercel Blob: set `BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob` (SDK-native; no custom bridge).
870
-
871
- ### B. Environment Variables (Vercel dashboard + local)
872
-
873
- | Name | Required | Who fills it |
874
- |------|----------|--------------|
875
- | `SUPABASE_URL` | If Q1 = Supabase | User pastes URL |
876
- | `DATABASE_URL` | If Q1 = Neon | User pastes URL |
877
- | `TOMORROWOS_STORE` | Yes | **Agent auto-sets only** ? never a blank field for the user (`supabase` / `postgres`) |
878
- | `DATABASE_SSL` | Yes for Postgres | **Agent auto-sets only** ??never a blank field for the user (`true`) |
879
- | `CLOUDINARY_*` | If Q2 = Cloudinary | User via **one Env popup** (all fields) |
880
- | `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | User / Blob link |
881
- | `TOMORROWOS_MEDIA` | If Q2 = Vercel Blob | **Agent auto-sets** `vercel-blob` |
882
- | `TOMORROWOS_INTERNAL_PORT` | Preview only | Agent (e.g. `3001`) |
883
- | `PORT` | Optional | Vercel injects for Production |
884
-
885
- Mirror non-secrets in `.env` for local `npm run dev` if helpful; never commit secrets.
886
-
887
- ### C. Install and run
888
-
889
- ```bash
890
- npm install
891
- npm run start
892
- # Preview / v0: npm run dev:preview (Next + TomorrowOS internal)
893
- ```
894
-
895
- Confirm logs: `[TomorrowOS] listening on http://0.0.0.0:?`
896
- Confirm browser:
897
-
898
- - **Preview:** Control Panel visible through the Next proxy at `/`
899
- - **Production start:** Control Panel served directly by TomorrowOS at `/`
900
-
901
- ### D. Deploy (Publish ??Node only)
902
-
903
- ```bash
904
- vercel --prod
905
- # or git push ??Vercel Git integration
906
- ```
907
-
908
- **Publish acceptance checklist (all must pass ??Scheme A5):**
909
-
910
- 1. Framework Preset = **Other** / null (dashboard confirms ??not Next.js)
911
- 2. Output Directory = **empty** (not `public`)
912
- 3. Start Command = Node `TomorrowOS.listen` (`npm run start` / `tsx server.ts`)
913
- 4. Fluid compute on
914
- 5. Env Vars present for Production
915
- 6. Production `GET /status` returns **JSON** ??**not** HTML (`Unexpected token 'T'` means this failed)
916
- 7. Production `GET /` returns Control Panel HTML ??**not** `404: NOT_FOUND`
917
-
918
- If step 6 fails but step 7 passes: **static `public/` trap** ??Vercel is not running Node. Re-apply Scheme A3?A4; try **Path B** (Git / `vercel --prod`); do **not** invent `api/` + `rewrites`.
919
-
920
- Tell the user:
921
-
922
- - **Control Panel (Publish):** `https://YOUR-PROJECT.vercel.app`
923
- - **TV CMS endpoint:** same HTTPS origin (players use `wss://`)
924
- - **Preview:** Next shell is for v0 only — do not pair devices to Preview
925
-
926
- Then **you must tell the user how to install a player and pair**. Do **not** only link a file — spell out the steps:
927
-
928
- #### Samsung Tizen (6.5 and 7.0)
929
-
930
- 1. On the display, open **App Management** → install via **Custom App** URL:
931
-
932
- ```txt
933
- https://tmr.sh/tizen
934
- ```
935
-
936
- Or use Control Panel → **Download Players → Samsung** and install via USB.
937
- 2. Choose orientation, then enter the **Production** CMS URL: `https://YOUR-PROJECT.vercel.app/` (never Preview, never `localhost`).
938
- 3. Enter the **six-character** pairing code in Control Panel → **Pair**.
939
-
940
- #### BrightSign (Series 3-6)
941
-
942
- 1. On the **Production** Control Panel → **Download Players → BrightSign** (preferred — `cmsEndpoint` is filled for this CMS),
943
- or download `https://tmr.sh/app/brightsign/brightsign_package.zip` and set `cmsEndpoint` in `config.js` to `https://YOUR-PROJECT.vercel.app/`.
944
- 2. Unzip and copy the **contents** to the SD card root (`autorun.brs`, `config.js`, player files).
945
- 3. Power-cycle the player, wait ~10s through the black boot window, then pair with the six-character code.
946
-
947
- 4. Create a playlist, upload media, **Publish** to the device.
948
-
949
- More detail: https://docs.tomorrowos.org/docs/os/tizen and https://docs.tomorrowos.org/docs/os/brightsign
950
-
951
- ### E. Minimal verification only
952
-
953
- **Do:**
954
-
955
- 1. Preview: Control Panel HTML at `/` (via Next proxy in `preview/` if applicable)
956
- 2. Publish: `GET /status` ??JSON; `GET /` ??Control Panel from **Node** TomorrowOS
957
- 3. Publish: panel does **not** show `Unexpected token 'T'` / CMS unreachable
958
- 4. Server status: Media OK when Cloudinary or Blob configured
959
- 5. Database: if ERROR shows **`ENETUNREACH` ??IPv6 ??`:5432`**, switch to **pooled** URL (Supabase `:6543` or Neon pooler) and redeploy ??call this out explicitly to the user
960
- 6. Fluid compute on for Production
961
-
962
- **Do not block** on long WebSocket/device pairing tests unless the user asks. Prefer pairing against **Publish**, not Preview.
963
-
964
- ---
965
-
966
- ## Failure recovery cheat sheet
967
-
968
- | Symptom | Likely cause | Fix |
969
- |---------|--------------|-----|
970
- | Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no Function | Scheme A: Framework = **Other**, Fluid on, `api/index.ts` + rewrites; redeploy |
971
- | Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** ??no Function | Rename to `cms-panel/`; Output empty; Scheme A Function entry; **Path B** if needed |
972
- | Only `/api/status` returns JSON, `/status` is 404 | Missing **rewrites** to `/api` | Add Scheme A3 rewrite `/(.*) ??/api` (exclude existing `/api/`); redeploy |
973
- | Control Panel OK, TV **could not connect** | WebSocket upgrade not reaching Function (200 HTML) | Fluid on; `export default server`; verify 101 on `/` and `/api`; check WebSockets permission |
974
- | `vercel.json` error: invalid `startCommand` | v0 schema rejects it | Remove from `vercel.json`; Function entry does not need startCommand |
975
- | Separate `api/ws.ts` with a second `new TomorrowOS` | Two isolates ??pairing memory split | **One** Function only (`api/index.ts` ??shared `cms-app.ts`) |
976
- | `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty**; use `cms-panel/`; Path B redeploy |
977
- | Build runs `next build` / missing routes-manifest | Next auto-detected from root `next.config` / `app/` | Move Next to `preview/`; Framework null/Other; `build` = no-op |
978
- | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy ??internal TomorrowOS port |
979
- | Preview works, Publish broken / Next-only | Production still on Next | Switch Production to Fluid Function `api/index.ts` |
980
- | `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
981
- | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
982
- | Uploads break / broken thumbs in prod | Local uploads only | Cloudinary or Vercel Blob |
983
- | Blob uploads 401 / missing token | Blob not linked | Vercel Storage ??Blob + `BLOB_READ_WRITE_TOKEN` |
984
- | WebSocket fails on Publish | Fluid off / static-only / Next as Production / no Function WS | Enable Fluid; Scheme A; test 101 upgrade |
985
- | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected ??pair on Publish URL |
986
- | Agent asked user to fill `TOMORROWOS_STORE` / `DATABASE_SSL` | Off-protocol | Agent must auto-set after DB choice; remove those fields from user Env forms |
987
- | Agent offered Blob-as-recommended / Supabase Storage / local disk / "no media" for Q2 | Invented options | Re-ask Q2 verbatim: **1 Cloudinary (recommended)**, 2 Vercel Blob |
988
- | Agent asked Cloudinary key/secret in three chat turns | Off-protocol | Use **one Env popup** with all Cloudinary fields |
989
- | Agent asked for OpenAI / `sk-` key during setup | Hallucinated ?AI pairing??requirement | **Refuse.** TomorrowOS does not need LLM keys. Return to Q2 media choices only |
990
- | Agent asked a branding choice / Option A-B / "customize?" before the form | Off-protocol Q3 | Close it; immediately open the **7-field form** (no choice step) |
991
- | Agent showed only Name + Primary colour (or any subset) | Off-protocol Q3 | Rebuild with **all seven** inputs visible at once |
992
- | Agent auto-continued after showing Q3 because "fields are optional" / "shouldn't re-ask" | Off-protocol Q3 | **HARD STOP** and wait for submit or explicit "skip"; do not scaffold yet |
993
- | Agent asked branding as Option A URL vs Option B, or field-by-field Next | Off-protocol Q3 | Re-open Q3 as **seven separate optional inputs** in one dialog; blanks keep `brand.json` |
994
- | Agent built a login page from branding answers | Misread brand step as product scope | Remove login; keep starter Control Panel; apply answers only to `brand.json` |
995
- | Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
996
- | Replit files present | Copied wrong protocol | Delete `.replit*`; use this file |
997
-
998
- ---
999
-
1000
- ## How this differs from `REPLIT_SETUP.md`
1001
-
1002
- | Topic | Replit | Vercel |
1003
- |-------|--------|--------|
1004
- | Secrets | Replit Secrets | Vercel Environment Variables |
1005
- | Deploy config | `.replit`, artifact `kind=web` | `vercel.json` + Fluid; **Framework = Other**; **no** `.replit` |
1006
- | `hostingTarget` | `"here"` | `"vercel"` |
1007
- | Database | Supabase (primary) | Supabase **or Neon** (pooled) |
1008
- | Media | Cloudinary or Replit Object Storage | Cloudinary **or Vercel Blob** |
1009
- | Preview | Replit Preview / Autoscale Node | **Next shell + proxy** when v0 hardwires Next |
1010
- | Publish | Long-lived Node | **Node `TomorrowOS.listen` only** (not Next); gate on no `404 NOT_FOUND` |
1011
- | Brand | Replit 7-field optional dialog | **Form not question:** all 7 optional inputs at once; no choice first; Logo Upload above Logo URL |
1012
-
1013
- ---
1014
-
1015
- ## Final summary template (Agent — human)
1016
-
1017
- After setup, report:
1018
-
1019
- 1. **Live URL** (Production Publish)
1020
- 2. **Publish health:** Framework = Other; Output — `public`; `GET /status` — JSON; `GET /` OK
1021
- 3. **Preview note:** Next proxy shell used / not needed
1022
- 4. **Env Vars set** (names only — never values); note which were auto-set (`TOMORROWOS_STORE`, `DATABASE_SSL`)
1023
- 5. **Store:** Supabase pooler or Neon pooled
1024
- 6. **Media:** Cloudinary or Vercel Blob
1025
- 7. **Brand:** name + `hostingTarget: vercel` (from optional Q3 fields; blanks = starter defaults)
1026
- 8. **Runtime split:** Preview = …; Publish = Node TomorrowOS (confirm **not** Next)
1027
- 9. **Open action:** if Database still ERROR, paste the pooler instruction again
1028
- 10. **Connect a screen:** include the Tizen Custom App URL / BrightSign SD install + six-character pairing steps from section **D. Deploy** above (required — do not skip)
1029
-
1030
- Protocol complete when:
1031
-
1032
- - **Preview** shows the Control Panel at `/` (via Next proxy if required), **and**
1033
- - **Publish** returns Control Panel at `/` **and** `/status` JSON from pure Node `TomorrowOS.listen` with Fluid (not static `public/`, not `404: NOT_FOUND`, not `Unexpected token 'T'`), **and**
1034
- - Q1–Q3 configuration is saved, **and**
1035
- - the user has been told how to install Tizen and/or BrightSign and pair
1036
-
1037
- ? even if a sandbox DB probe still flakes **after** the pooled URL is correctly set for Production.
1038
-
1039
- ---
1040
-
1041
- ## Protocol version
1042
-
1043
- `vercel-setup/1.15` - pairs with `@tomorrowos/sdk` 0.9.70+ and `VERCEL_QUESTIONS.md` 1.5.
1044
-
1045
- **Changelog 1.15:** After Publish is healthy, agents **must** tell the user how to install Tizen (Custom App `https://tmr.sh/tizen`) and BrightSign (CMS zip → SD root) and pair against the **Production** URL. Final summary requires this screen-connect step.
1046
-
1047
- **Changelog 1.14:** Vercel Blob is **SDK-native** (`BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob`). No project Blob bridge. Status providers use display names (Neon, Supabase, Blob, Cloudinary, Replit Object Storage). Forbidden marketing status copy about blob URL patterns.
1048
-
1049
- **Changelog 1.13:** Q3 must **HARD STOP and WAIT** for user submit or "skip". Optional fields do **not** authorize auto-continuing with defaults. Clarifies "do not re-ask" = after the user already replied, not "skip waiting".
1050
-
1051
- **Changelog 1.12:** Q3 is a **form, not a question**. No preliminary branding choice. Must show **all seven** optional inputs at once (never only Name + Primary colour). Logo = Upload above Logo URL.
1052
-
1053
- **Changelog 1.11:** Q3 must render **seven separate input controls** (not one freeform / Other box). Logo field = **Upload** above **Logo URL**; blanks keep starter `brand.json`.
1054
-
1055
- **Changelog 1.10:** Q3 is a **single** seven-field optional branding dialog (Product Name, Tagline, Primary / Background / Text / Secondary colour, Logo URL or upload). Removed Option A website-URL vs Option B split. Blanks keep starter `brand.json`. Pairs with new `VERCEL_UPGRADE.md`.
1056
-
1
+ # Vercel Setup Protocol for TomorrowOS CMS
2
+
3
+ > **This file is a contract for Vercel Agent / AI coding assistants setting up TomorrowOS on Vercel.**
4
+ > Follow it literally. Ask questions in order. Do not invent secrets. Prefer configuring **Vercel Environment Variables**, `.env` (non-secrets only), `brand.json`, and `server.ts` store wiring over inventing a custom CMS.
5
+ >
6
+ > **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`).
7
+ >
8
+ > Protocol id: `vercel-setup/1.16` - pairs with `@tomorrowos/sdk` **0.9.70+** (`templates/cms-starter-v0`, Fluid Functions WebSockets).
9
+ >
10
+ > **Questions source of truth:** `VERCEL_QUESTIONS.md` - Agents must **open that file and paste each Q block verbatim**. Do not paraphrase.
11
+ > **Q1/Q2 = choice questions. Q3 = form only:** open **all seven optional inputs at once**, then **HARD STOP and WAIT** for submit or "skip". Never auto-skip because fields are optional. Never ask a branding choice first. Never show only Name + Primary colour. Logo = **Upload** above **Logo URL**.
12
+ > **Q4 = optional `CMS_PASSWORD`:** blank/skip is valid; can set later. Control Panel auth only — never Vercel Deployment Protection / whole-site password.
13
+ > **Copy questions verbatim from `VERCEL_QUESTIONS.md`.** Do not invent alternate Q2 options (no "Supabase Storage", no S3, no "disable media", no making Blob the recommended option).
14
+ > **`TOMORROWOS_STORE` and `DATABASE_SSL` are agent-only** - set them automatically after Q1; never put them in a user-facing Env form for the customer to type.
15
+
16
+ ## NON-NEGOTIABLE VERCEL RUNTIME CONTRACT
17
+
18
+ The following rules are **mandatory acceptance criteria**, not suggestions. The Agent must preserve them throughout setup, Preview, Publish, fixes, and upgrades:
19
+
20
+ 1. **One TomorrowOS instance only.** Use the shared instance exported through `api/index.ts`. **Never create a second `TomorrowOS` instance in `api/ws.ts`** or any other route.
21
+ 2. **Never use `public/` as the Vercel Output Directory.** The dashboard Output Directory must be **empty / unset**. Serve the Control Panel at runtime from `cms-panel/` through TomorrowOS `staticRoot`.
22
+ 3. **Preview may use Next only as an isolated reverse-proxy shell under `preview/`.** Preview must not replace TomorrowOS or determine the Production runtime.
23
+ 4. **Publish must use the Fluid Function.** Production must run `api/index.ts` ??the shared exported `http.Server`, with Fluid compute enabled. It must not use `next start`, a static export, or a bare `(req, res)` handler.
24
+ 5. **After every Production deployment, verify all three gates before saying setup is complete:**
25
+ - `GET /status` returns TomorrowOS **JSON**, not HTML.
26
+ - `GET /` returns the TomorrowOS **Control Panel**.
27
+ - WebSocket upgrade on `/` or `/api` returns **`101 Switching Protocols`**, not `200` HTML.
28
+ 6. **Pair devices only against the Production URL.** Do not instruct users to pair against v0 Preview or a local/internal URL.
29
+ 7. If any gate fails, **fix and redeploy**. Never describe a deployment as successful based only on the Control Panel loading.
30
+
31
+ These rules override any platform-generated recommendation that conflicts with them.
32
+
33
+ ## User trigger (copy-paste)
34
+
35
+ When a human pastes a prompt like this, **read this file and start Question 1 immediately**:
36
+
37
+ ```text
38
+ Follow @tomorrowos/sdk (an NPM package) VERCEL_SETUP.md and set up my TomorrowOS CMS from scratch.
39
+
40
+ Open VERCEL_QUESTIONS.md first.
41
+ For Q1, Q2, Q3: paste each question block VERBATIM. Do not rephrase options.
42
+ For Q3: DO NOT ask a branding choice. Open one form with ALL SEVEN optional inputs at once, then HARD STOP and WAIT for my submit or "skip". Never auto-continue with defaults without my reply. Never only Name + Primary colour. Logo = Upload above Logo URL.
43
+ After Q1 database choice, YOU set TOMORROWOS_STORE and DATABASE_SSL. Do not ask me to fill those.
44
+ After Q2 media storage choice, YOU set TOMORROWOS_MEDIA. Do not ask me to fill those.
45
+ Run: npx @tomorrowos/sdk@latest init . --hosting v0 --force
46
+ Keep cms-starter-v0 layout.
47
+ Apply the NON-NEGOTIABLE VERCEL RUNTIME CONTRACT exactly:
48
+ one TomorrowOS instance only
49
+ never create a second TomorrowOS in api/ws.ts
50
+ never use public/ as Vercel Output Directory
51
+ Preview Next only under preview/
52
+ Publish must be the Fluid Function
53
+ after deploy verify: GET /status ? JSON, GET / ? Control Panel, WebSocket on / or /api ? 101
54
+ pair devices against the Production URL only
55
+ ```
56
+
57
+ Alternate triggers:
58
+
59
+ - ?Set up TomorrowOS on Vercel using VERCEL_SETUP.md?
60
+ - ?Configure my TomorrowOS CMS (Vercel wizard)?
61
+
62
+ When any of these fire, **ignore `REPLIT_SETUP.md`** for the Q&A phase (you may skim Replit docs only for shared SDK semantics).
63
+
64
+ ---
65
+
66
+ ## IRON RULE: Preview vs Publish (dual runtime)
67
+
68
+ > **This is the #1 deployability rule on Vercel. Non-negotiable.**
69
+
70
+ TomorrowOS CMS is **one Node process** that serves:
71
+
72
+ - Browser Control Panel at `GET /` (`public/` via `staticRoot`)
73
+ - Device **WebSocket** on the same origin (`https` ??players use `wss`)
74
+
75
+ ### Production / Publish ??Node only (no Next.js)
76
+
77
+ | Required | Forbidden |
78
+ |----------|-----------|
79
+ | Single **Node HTTP server** entry (`tsx server.ts` / exported `http.Server` from `listen()`) | **Next.js as the Production CMS** (`next start`, Next App Router as the live app) |
80
+ | **Framework Preset = Other / `null`** (dashboard **and** project settings API) | Leaving Framework Preset as **Next.js** (causes `404: NOT_FOUND`) |
81
+ | **Fluid compute** enabled (needed for WebSockets on Vercel) | Classic short-lived serverless with no WebSocket / no Fluid |
82
+ | Durable store = **Postgres** (Supabase or Neon, pooled URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
83
+ | Media = **Cloudinary** or **Vercel Blob** (durable HTTPS URLs) | Relying on local `public/uploads` alone for production fleets |
84
+ | Env vars in **Vercel Project ??Settings ??Environment Variables** | Committing passwords / API secrets / `.env` with secrets |
85
+ | `cms.hostingTarget`: `"vercel"` in `brand.json` | Creating `.replit` / Replit artifact files |
86
+
87
+ **Publish / Production Deploy** must run **`TomorrowOS.listen(...)`** directly. Do **not** put Next.js in front of Production traffic.
88
+
89
+ ### Publish `404: NOT_FOUND` ??mandatory prevention (read before every deploy)
90
+
91
+ The production URL showing:
92
+
93
+ ```text
94
+ 404: NOT_FOUND
95
+ Code: NOT_FOUND
96
+ ```
97
+
98
+ almost always means **Vercel still thinks this is a Next.js / static deploy**, not a Node TomorrowOS server.
99
+
100
+ **Before every Production deploy, you MUST verify all of the following:**
101
+
102
+ 1. **Project ??Settings ??General ??Framework Preset = Other** (or unset / null).
103
+ If it still says **Next.js**, patch it via Vercel API / CLI / dashboard **before** redeploying.
104
+ `"framework": null` in `vercel.json` alone is **not enough** if the dashboard still says Next.js.
105
+ 2. **Install Command** = `npm install` / `pnpm install` / `yarn` (normal install).
106
+ 3. **Build Command** = a **no-op that exits 0** (e.g. `node -e "process.exit(0)"` or `npm run build` where `build` is that no-op).
107
+ **Never** run `next build` for Production.
108
+ 4. **Production entry** = Fluid **Vercel Function** `api/index.ts` that **`export default server`** from shared `cms-app.ts` (see [WebSockets docs](https://vercel.com/docs/functions/websockets)). Local `npm start` may still use root `server.ts`.
109
+ 5. **Output Directory** = **empty / unset**. Do **not** set `outputDirectory: "."` as a fake static site.
110
+ 6. **Do** use a single `api/index.ts` that exports the TomorrowOS `http.Server`. **Do not** create a second TomorrowOS in `api/ws.ts`, and **do not** use bare `(req, res)` handlers that drop upgrades.
111
+ 7. If Preview needs Next, keep Next files **isolated** so Production auto-detect cannot latch onto them:
112
+ - Prefer `preview/` (or similar) for `next.config.*`, `app/`, `pages/` used only by v0 Preview, **or**
113
+ - Ensure Production Framework Preset is forced to **Other** and never runs `next` as the CMS.
114
+
115
+ **Setup is NOT complete** while Production `GET /` returns `404: NOT_FOUND` or WebSocket upgrade returns 200 HTML.
116
+
117
+ ### Publish static trap ??`public/` served without Node (read this)
118
+
119
+ A common **worse** failure mode than `404: NOT_FOUND`:
120
+
121
+ - `GET /` loads the Control Panel HTML (from static `public/index.html`)
122
+ - `GET /status`, `/devices`, etc. return **HTML** (Vercel 404 page)
123
+ - The panel shows **CMS unreachable** and console:
124
+ `Unexpected token 'T', "The page c"... is not valid JSON`
125
+
126
+ **Root cause:** Vercel deployed `public/` as a **static site** (Output Directory = `public` or Framework=Other default *?public if it exists??). **No Function** is running TomorrowOS.
127
+
128
+ **Fix (Scheme A):** use `cms-panel/` (not `public/`), Fluid Function `api/index.ts` + rewrites, Output Directory empty. Static UI is served **at runtime** via `staticRoot` in `listen()`.
129
+
130
+ **Publish gate (mandatory):** after deploy, `GET /status` must return **JSON** starting with `{`, not HTML starting with `<` or `The page`. **And** WebSocket upgrade on `/` or `/api` must return **101**.
131
+
132
+ ```bash
133
+ curl -s https://YOUR-PROJECT.vercel.app/status | head -c 80
134
+ # Expected: {"status":"success",...}
135
+ # Failure: The page could not be found OR <!DOCTYPE html>
136
+ ```
137
+
138
+ **Setup is NOT complete** while `/status` returns HTML, the panel shows the JSON parse error, or devices cannot open `wss://`.
139
+
140
+ ### Preview (v0 / Vercel Agent sandbox) ??Next.js shell allowed
141
+
142
+ v0 Preview is often a **Next.js runtime** hardwired to `next dev`. A raw `tsx server.ts` may listen successfully while Preview still shows a blank page, because the sandbox intercepts the public port with Next before requests reach TomorrowOS.
143
+
144
+ **For Preview only**, you **must** keep a thin Next.js host that **reverse-proxies** HTTP to TomorrowOS on an **internal** port.
145
+
146
+ | Preview requirement | Detail |
147
+ |---------------------|--------|
148
+ | Next.js remains the Preview host | Satisfies v0 / sandbox expectations |
149
+ | TomorrowOS on internal port | e.g. `TOMORROWOS_INTERNAL_PORT=3001` (not the public Preview port) |
150
+ | Transparent HTTP reverse proxy | Next route/middleware forwards `/` (and Control Panel paths) ??`http://127.0.0.1:3001` |
151
+ | Start both processes in Preview | Next + `tsx server.ts` (or a `dev:preview` script that starts both) |
152
+
153
+ **Preview WebSocket note:** HTTP proxy is enough for Control Panel HTML/API smoke checks. Device `wss` pairing may not work through a Next proxy in Preview ??that is acceptable. **Pair devices against the Production Publish URL**, not v0 Preview.
154
+
155
+ **Do not** replace TomorrowOS with a Next-only CMS. Next is a **Preview adapter**, not the product.
156
+
157
+ ---
158
+
159
+ ## Questionnaire scope (STRICT)
160
+
161
+ Ask **only** these questions, in **this exact order**. **Copy the ?Ask exactly??blocks verbatim** ??do not paraphrase options.
162
+
163
+ | Step | Section | When |
164
+ |------|---------|------|
165
+ | 1 | **Question 1** ? Database (Supabase ? Neon) | Always |
166
+ | 2 | **Question 2** ? Media (**Cloudinary recommended** ? Vercel Blob) | Always (after Q1) |
167
+ | 3 | **Question 3** - Brand form (`brand.json` only; **not a choice** - open all 7 optional inputs at once) | Always (after Q2) |
168
+ | 4 | **Question 4** - Optional CMS password Env `CMS_PASSWORD` (**optional**; blank = skip, can set later) | Always last |
169
+
170
+ **That is the complete list (Q1–Q4).** Do **not** ask how many screens / devices.
171
+
172
+ **Forbidden invented Question 2 options (never offer these in chat or UI):**
173
+
174
+ - ???Vercel Blob **(recommended)**??as option 1 ??Blob is option **2**, not recommended over Cloudinary
175
+ - ???Supabase Storage??/ S3 bucket / `SUPABASE_SERVICE_ROLE_KEY` for media
176
+ - ???No media storage??/ ?disable media uploads??/ ?remote URLs only??as a numbered choice
177
+ - ??Any media list that does not start with **Cloudinary (recommended)**
178
+
179
+ **Forbidden Env UX after Question 1:**
180
+
181
+ - ??Showing the user empty fields for `TOMORROWOS_STORE` or `DATABASE_SSL` to fill
182
+ - ??Asking in chat ?what should TOMORROWOS_STORE be???
183
+ - ??After DB choice + URL paste: **you** write those two vars silently (see Q1 Steps B/C/D)
184
+
185
+ **Forbidden extra questions (never ask, even if the user skips a step):**
186
+
187
+ - ??OpenAI / Anthropic / Gemini / any LLM API key (`sk-?`, etc.)
188
+ - ???AI pairing?? ?AI feature logic?? or inventing auth that needs an LLM key
189
+ - ??Asking the user to type `TOMORROWOS_STORE` or `DATABASE_SSL`
190
+ - Any secret not listed in Questions 1–4 (DB URL, media credentials, brand, optional `CMS_PASSWORD`)
191
+
192
+ TomorrowOS pairing and Control Panel logic live in `@tomorrowos/sdk`. They do **not** call OpenAI.
193
+
194
+ If the user **skips** a Question 2 option, do **not** invent a substitute question. Re-state Question 2 choices **verbatim** ? never pivot to LLM keys, Supabase Storage, or local disk uploads.
195
+
196
+ If the user says ?just set it up, don?t ask questions,??respond:
197
+
198
+ > I need a few quick answers from VERCEL_SETUP.md (database — Supabase or Neon preferred — media storage with Cloudinary recommended, branding, and optional Control Panel password). It takes about a minute and ensures the CMS works on Vercel.
199
+
200
+ Then ask **Question 1**.
201
+
202
+ ---
203
+
204
+ ## Hard rules
205
+
206
+ 1. **Open `VERCEL_QUESTIONS.md` first.** Paste Q1 → configure → paste Q2 → configure → paste Q3 → paste Q4. Never paraphrase question options. Never use `REPLIT_SETUP.md` as the questionnaire. **Never** ask for OpenAI or other LLM API keys.
207
+ 2. **Question 2 options are fixed:** (1) Cloudinary **(recommended)**, (2) Vercel Blob. Do not reorder; do not invent Supabase Storage, local disk, or disable-uploads.
208
+ 3. **Cloudinary:** open **one** Vercel Env popup with **all** Cloudinary fields together. Do **not** quiz one credential at a time.
209
+ 4. **`TOMORROWOS_STORE` + `DATABASE_SSL` = agent-owned.** After Q1 choice (and URL if needed), set them yourself in Vercel Env Vars. **Never** include them as blank fields for the customer. Mapping: Supabase ? `TOMORROWOS_STORE=supabase` + `DATABASE_SSL=true`; Neon ? `TOMORROWOS_STORE=postgres` + `DATABASE_SSL=true`. **Do not offer SQLite as a questionnaire choice** ? it is ephemeral on Vercel and unsuitable for production.
210
+ 5. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, brand colours, **or LLM API keys**.
211
+ 6. **Postgres env naming:** prefer **`SUPABASE_URL`** for Supabase; prefer **`DATABASE_URL`** for Neon.
212
+ 7. **Always prefer pooled connection strings** (Supabase Session pooler **6543**; Neon pooled host).
213
+ 8. **Prefer `npx @tomorrowos/sdk@latest init --hosting v0`**. For Replit/Railway use `npx @tomorrowos/sdk@latest init .`. Do not rebuild pairing / WebSocket APIs. Untagged `npx @tomorrowos/sdk init` is forbidden (stale cache).
214
+ 9. **Never commit secrets.** Use Vercel Environment Variables.
215
+ 10. **Skip Replit-only files.**
216
+ 11. **SQLite is not a production store on Vercel.**
217
+ 12. **Enable Fluid compute** for Production WebSockets.
218
+ 13. **Production:** `api/index.ts` exports `http.Server` from `TomorrowOS.listen`. Not `next start`. No second TomorrowOS in `api/ws.ts`.
219
+ 14. **Preview:** Next reverse-proxy shell when v0 is Next-hardwired.
220
+ 15. After Q&A: **configure ??install ??deploy ??minimal verify**.
221
+ 16. **No inventing CMS login** from a branding URL.
222
+ 17. **Skip → invent.** Stay on Q1–Q4 only.
223
+ 18. **Publish gate:** `/status` JSON + WebSocket 101; no static `public/` trap.
224
+ 19. **Q3 is a form, not a question ? and you must WAIT.** After Q2, open one dialog with **all seven** optional inputs visible together, then **stop and wait** for submit or explicit **"skip"**. **Never** auto-continue because fields are optional. **Never** ask a preliminary branding choice. **Never** show only Product Name + Primary colour. **Never** use multiple-choice / Option A-B for branding. Logo = **Upload** above **Logo URL**.
225
+
226
+ **Question order:** Q1 -> (auto-set store env) -> Q2 -> Q3 form (**wait for user**) -> Q4 optional `CMS_PASSWORD` -> execution checklist.
227
+
228
+ ---
229
+
230
+ ## Scheme A ??Vercel Functions + WebSockets (mandatory for Production)
231
+
232
+ > **Goal:** Preview may use Next + proxy; **Publish** runs TomorrowOS as a **Vercel Function** (`api/index.ts`) on **Fluid**, with WebSocket support per [Vercel WebSockets docs](https://vercel.com/docs/functions/websockets).
233
+ > **Scope:** Layout + `vercel.json` + dashboard. Prefer `npx @tomorrowos/sdk@latest init --hosting v0` (`cms-starter-v0`).
234
+ > **Do not** break Replit/Railway: those keep using default `init` ??`cms-starter` (root `server.ts` + `server.listen`, no `api/`).
235
+
236
+ ### Why Scheme A exists
237
+
238
+ | Layer | Preview (v0) | Publish (Production) |
239
+ |-------|----------------|----------------------|
240
+ | Host | Next.js shell + HTTP proxy | **Vercel Function** `api/index.ts` ??`export default server` |
241
+ | Static UI | Proxied from TomorrowOS | Served by **`staticRoot`** (`cms-panel/`) inside `listen()` ??**not** Vercel Output Directory |
242
+ | WebSocket | Optional / may fail in Preview | **Required** ??Fluid Function upgrade (`wss://` ??`/` rewritten to `/api`, or direct `/api`) |
243
+ | Next files | Allowed under `preview/` | **Must not** live at project root |
244
+
245
+ **Root cause of ?Control Panel works, TV cannot connect??** HTTP can be served without WebSocket upgrades. Classic root-only `server.ts` / plain serverless handlers often return **200 HTML** on `Upgrade` instead of **101**. Official pattern is a Function that exports `http.Server` with `ws` attached ([docs](https://vercel.com/docs/functions/websockets)).
246
+
247
+ ### A1 ??Project layout (Agent must enforce)
248
+
249
+ **Production (Vercel / v0 starter):**
250
+
251
+ ```
252
+ cms-app.ts ??shared TomorrowOS.listen + export const server
253
+ api/index.ts ??export { server as default } from "../cms-app.js"
254
+ server.ts ??local/Preview: same default export (npm start / internal port)
255
+ vercel.json ??fluid + rewrites + functions.maxDuration
256
+ package.json
257
+ brand.json
258
+ cms-panel/ ??CMS static UI (NOT named public/ ??avoids static-output trap)
259
+ preview/ ??Next proxy for v0 Preview only
260
+ ```
261
+
262
+ **Preview only ??keep ALL Next under `preview/`:**
263
+
264
+ ```
265
+ preview/
266
+ next.config.mjs
267
+ app/
268
+ ??
269
+ ```
270
+
271
+ **Do NOT:**
272
+
273
+ - ??Leave `next.config.mjs` + `app/` at project root
274
+ - ??Set Output Directory to `public`, `cms-panel`, or `.`
275
+ - ??Create a **second** TomorrowOS instance in `api/ws.ts` (separate isolate = broken pairing memory)
276
+ - ??Use `(req, res) =>` serverless handlers that drop `upgrade`
277
+ - ??Put `startCommand` / `processes` in `vercel.json` (v0 schema rejects them)
278
+
279
+ **`package.json` scripts:**
280
+
281
+ ```json
282
+ {
283
+ "scripts": {
284
+ "dev": "tsx watch server.ts",
285
+ "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
286
+ "dev:tomorrowos": "cross-env TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
287
+ "dev:next": "cd preview && next dev -p 3000",
288
+ "start": "tsx server.ts",
289
+ "build": "node -e \"process.exit(0)\""
290
+ }
291
+ }
292
+ ```
293
+
294
+ - **Local / Railway-style:** `npm start` ??`tsx server.ts` ??SDK calls `server.listen` (no `VERCEL` env).
295
+ - **Vercel Production:** Function loads `api/index.ts`; SDK skips `listen` when `process.env.VERCEL` is set and Vercel owns the socket.
296
+
297
+ ### A2 ??Function entry (matches Vercel WebSockets docs)
298
+
299
+ ```ts
300
+ // api/index.ts
301
+ export { server as default } from "../cms-app.js";
302
+ ```
303
+
304
+ ```ts
305
+ // cms-app.ts (shared)
306
+ const server = tomorrowos.listen({
307
+ port: Number(process.env.PORT) || 3000,
308
+ host: "0.0.0.0",
309
+ staticRoot: join(__dirname, "cms-panel"),
310
+ });
311
+ export { server, tomorrowos };
312
+ ```
313
+
314
+ SDK behaviour (0.9.32+):
315
+
316
+ - Accepts WebSocket upgrades on `/`, `/api`, `/api/ws` (players may use either)
317
+ - When `VERCEL` is set: **`autoListen` defaults to false** ??do not bind a port yourself
318
+ - Pattern aligns with docs: create `http.Server`, attach `ws`, **`export default server`**
319
+
320
+ ### A3 ??`vercel.json` (Production)
321
+
322
+ ```json
323
+ {
324
+ "fluid": true,
325
+ "framework": null,
326
+ "installCommand": "npm install",
327
+ "buildCommand": "npm run build",
328
+ "functions": {
329
+ "api/index.ts": {
330
+ "maxDuration": 300,
331
+ "memory": 1024,
332
+ "includeFiles": "{cms-panel/**,brand.json,assets/**}"
333
+ }
334
+ },
335
+ "rewrites": [
336
+ { "source": "/((?!api/).*)", "destination": "/api" }
337
+ ]
338
+ }
339
+ ```
340
+
341
+ **`maxDuration` plan limits:** Hobby / many default plans allow **1??00** seconds. **800** requires a higher Vercel plan. Prefer **`300`** unless the account already allows more ??otherwise Publish fails with *?maxDuration must be between 1 and 300??upgrade your plan??.
342
+
343
+ **Why rewrites:** Public URLs stay `https://app.vercel.app/` and `/status` while the Function mount is `/api`. Device `wss://app.vercel.app/` is rewritten to the same Function so one isolate handles HTTP + WS.
344
+
345
+ **v0 schema:** never add `startCommand` or `processes`.
346
+
347
+ **Critical:**
348
+
349
+ - `"fluid": true` is required for WebSockets
350
+ - **Do not** set `outputDirectory` to `cms-panel` / `public`
351
+ - `"buildCommand"` = no-op ??**never** `next build` for Production
352
+ - Dashboard Output Directory must stay **empty**
353
+
354
+ ### A4 ??Vercel Project Settings (dashboard ??mandatory)
355
+
356
+ | Setting | Required value |
357
+ |---------|----------------|
358
+ | Framework Preset | **Other** / null ??**not Next.js** |
359
+ | Install Command | `npm install` |
360
+ | Build Command | `npm run build` (no-op) |
361
+ | **Output Directory** | **empty** |
362
+ | Fluid compute | **On** |
363
+ | WebSockets | Account must allow Functions WebSockets (see docs ?Permissions Required?? |
364
+
365
+ ### A5 ??Publish acceptance (all must pass)
366
+
367
+ 1. `curl -s https://PROD/status` ??JSON (`{"status":...}`)
368
+ 2. `curl -s https://PROD/` ??Control Panel HTML
369
+ 3. **WebSocket upgrade returns 101**, not 200 HTML:
370
+
371
+ ```bash
372
+ curl.exe -i --http1.1 ^
373
+ -H "Connection: Upgrade" -H "Upgrade: websocket" ^
374
+ -H "Sec-WebSocket-Version: 13" ^
375
+ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" ^
376
+ "https://PROD/"
377
+ # Also try: https://PROD/api
378
+ ```
379
+
380
+ 4. Control Panel does **not** show CMS unreachable / `Unexpected token 'T'`
381
+ 5. Fluid on; Framework = Other
382
+
383
+ If (1)+(2) pass but (3) fails ??Function is serving HTTP only. Re-check Fluid, `api/index.ts` default export, rewrites, and WebSockets permission. **Do not** invent a second `api/ws.ts` TomorrowOS instance.
384
+
385
+ ### A6 ??What Scheme A does **not** require
386
+
387
+ - ??No Next.js as Production CMS
388
+ - ??No replacing SDK transport with hand-rolled pairing
389
+ - ??No SQLite as production store on Vercel
390
+ - ??No changes to Replit `cms-starter` layout for Vercel-only fixes
391
+
392
+ ### A7 ??If v0 Publish still fails (Path B / C)
393
+
394
+ **Path B:** Deploy with `vercel --prod` or Git integration (full Vercel), same Scheme A layout.
395
+
396
+ **Path C:** Host CMS on **Railway / Fly.io / Replit** (long-lived Node). Use Vercel only for Preview if needed.
397
+
398
+ ### A8 ??Migrate an existing broken Vercel project
399
+
400
+ 1. Scaffold/compare with `cms-starter-v0` (`init --hosting v0`) or copy:
401
+ - `cms-app.ts`, `api/index.ts`, `server.ts`, `vercel.json`
402
+ - rename `public/` ??`cms-panel/` if still present
403
+ 2. Ensure **one** Function exports the **same** `server` from `cms-app.ts`
404
+ 3. Fluid on ??Redeploy
405
+ 4. Verify A5 (especially WebSocket 101)
406
+ 5. On TVs: enter `https://YOUR.vercel.app/` (players also try `/api` automatically on vercel.app hosts)
407
+
408
+ ---
409
+
410
+ ## Runtime & Vercel deploy rules (mandatory)
411
+
412
+ ### Production runtime (Publish)
413
+
414
+ - Serve TomorrowOS via **Vercel Function** `api/index.ts` ??`export default server` (Fluid + WebSockets).
415
+ - Keep root **`server.ts`** for **local** `npm start` / Preview internal port only (same `cms-app.ts`).
416
+ - Keep **`@tomorrowos/sdk`** (0.9.32+) and put **`tsx` in `dependencies`** for local start.
417
+ - Node **20+**.
418
+ - **Do not** replace the SDK with a hand-rolled server that drops WebSocket upgrades.
419
+ - **Forbidden as the Production CMS:** `next start`, static export only, or a bare `(req,res)` handler without `http.Server` + `ws`.
420
+
421
+ ### Expected `package.json` scripts
422
+
423
+ ```json
424
+ {
425
+ "scripts": {
426
+ "dev": "tsx watch server.ts",
427
+ "dev:preview": "concurrently -k \"npm:dev:tomorrowos\" \"npm:dev:next\"",
428
+ "dev:tomorrowos": "cross-env PORT=3001 TOMORROWOS_INTERNAL_PORT=3001 tsx watch server.ts",
429
+ "dev:next": "cd preview && next dev -p 3000",
430
+ "start": "tsx server.ts",
431
+ "build": "node -e \"process.exit(0)\""
432
+ }
433
+ }
434
+ ```
435
+
436
+ Adapt script names / process runners as needed. **Invariant:**
437
+
438
+ - **Local `npm start`** = TomorrowOS Node with `server.listen`.
439
+ - **Vercel Production** = Function `api/index.ts` (SDK `autoListen=false` when `VERCEL` is set).
440
+ - **`build`** = no-op exit 0 (never `next build` for Production).
441
+ - **Preview / v0** = Next on public port + TomorrowOS on internal port + proxy.
442
+
443
+ ### Minimal dependencies (Production)
444
+
445
+ ```json
446
+ {
447
+ "type": "module",
448
+ "engines": {
449
+ "node": ">=20"
450
+ },
451
+ "dependencies": {
452
+ "@tomorrowos/sdk": "latest",
453
+ "dotenv": "^17.2.3",
454
+ "tsx": "^4.19.0"
455
+ },
456
+ "devDependencies": {
457
+ "@types/node": "^20.0.0",
458
+ "typescript": "^5.5.0"
459
+ },
460
+ "scripts": {
461
+ "dev": "tsx watch server.ts",
462
+ "start": "tsx server.ts",
463
+ "build": "node -e \"process.exit(0)\""
464
+ }
465
+ }
466
+ ```
467
+
468
+ Use the latest published `@tomorrowos/sdk` when scaffolding (do not invent versions).
469
+
470
+ Add nothing extra for Blob when using `@tomorrowos/sdk@0.9.70+` (`@vercel/blob` is bundled). Only ensure `BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob` when the user chooses **Vercel Blob** in Question 2.
471
+
472
+ For Preview shell only, add as needed: `next`, `react`, `react-dom`, and a process runner (`concurrently` or equivalent). These must **not** become the Production CMS.
473
+
474
+ ### `server.ts` pattern (Vercel-adapted ??Production)
475
+
476
+ Keep the server at **project root** (e.g. `server.ts` / `server.mts`). Do **not** move Production into `api/` solely to satisfy Vercel routing myths.
477
+
478
+ ```ts
479
+ import "dotenv/config";
480
+ import { readFileSync } from "fs";
481
+ import { fileURLToPath } from "url";
482
+ import { dirname, join } from "path";
483
+ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
484
+
485
+ const __dirname = dirname(fileURLToPath(import.meta.url));
486
+ const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
487
+
488
+ const store = createTomorrowOSStore({
489
+ // Supabase: SUPABASE_URL (pooler). Neon: DATABASE_URL (pooled).
490
+ databaseUrl:
491
+ process.env.SUPABASE_URL ||
492
+ process.env.NEON_DATABASE_URL ||
493
+ process.env.DATABASE_URL,
494
+ sqlitePath: join(__dirname, "data", "tomorrowos.db") // local/dev fallback only
495
+ });
496
+
497
+ const tomorrowos = new TomorrowOS({ brand, store });
498
+
499
+ const port =
500
+ Number(process.env.TOMORROWOS_INTERNAL_PORT) ||
501
+ Number(process.env.PORT) ||
502
+ 3000;
503
+
504
+ const server = tomorrowos.listen({
505
+ port,
506
+ host: "0.0.0.0",
507
+ staticRoot: join(__dirname, "public")
508
+ });
509
+
510
+ // Vercel / Node server capture: export the HTTP server, not only TomorrowOS.
511
+ export default server;
512
+ ```
513
+
514
+ - **Production:** `PORT` from Vercel ??TomorrowOS listens on that port (no Next).
515
+ - **Preview:** set `TOMORROWOS_INTERNAL_PORT=3001` (or similar) so Next can own `:3000` and proxy inward.
516
+
517
+ Load env with `dotenv` for local/`vercel dev`. On production Deploy, Vercel injects Environment Variables natively.
518
+
519
+ ### Preview adapter (v0 / Next reverse proxy) ??required when Preview is Next-hardwired
520
+
521
+ Detect Preview / v0 (Next already present, or Preview blank while TomorrowOS logs ?listening??. Then:
522
+
523
+ 1. Keep a **minimal** Next app **isolated when possible** (e.g. under `preview/`) so Production Framework detection does not latch onto root `next.config.*`.
524
+ 2. TomorrowOS listens on **`127.0.0.1:TOMORROWOS_INTERNAL_PORT`** (default **3001**).
525
+ 3. Add a **catch-all** Next route (or middleware) that proxies HTTP to TomorrowOS:
526
+
527
+ ```ts
528
+ // Example: preview app proxy handler
529
+ // Forward method, headers, and body to http://127.0.0.1:3001/<path>
530
+ // Return the upstream status, headers, and body to the browser.
531
+ ```
532
+
533
+ Preferred UX: proxy **`/`** itself (rewrites) so the Preview URL shows the Control Panel at `/`, not only under `/api/...`.
534
+
535
+ 4. Start **both** processes for Preview (`dev:preview` pattern above).
536
+ 5. Document in the final summary: **Preview = Next shell + proxy; Publish = Node only.**
537
+
538
+ **Forbidden:**
539
+
540
+ - Making Next the Production CMS or the only server after Publish.
541
+ - Deleting TomorrowOS `server.ts` because Preview needed Next.
542
+ - Claiming setup complete if Production entrypoint is still `next start`.
543
+ - Claiming setup complete if Production `GET /` is `404: NOT_FOUND`.
544
+ - Creating root `api/index.*` + `routes` catch-all that exports a raw `http.Server` ?to fix 404??
545
+
546
+ ### `vercel.json` (Production ??explicit Node, not Next)
547
+
548
+ Write `vercel.json` so Production cannot be mistaken for Next. Minimum:
549
+
550
+ ```json
551
+ {
552
+ "fluid": true,
553
+ "framework": null,
554
+ "installCommand": "npm install",
555
+ "buildCommand": "npm run build"
556
+ }
557
+ ```
558
+
559
+ Use `pnpm` / `yarn` variants of install/build if that is the project?s package manager.
560
+
561
+ **Do not** add `startCommand`, `processes`, or `outputDirectory` to `vercel.json` when v0 schema rejects them. Start = `package.json` `"start": "tsx server.ts"`. Output Directory = **empty in dashboard**.
562
+
563
+ **Also force Project Settings (dashboard or Vercel API) ??this is mandatory (see Scheme A4):**
564
+
565
+ | Setting | Required value |
566
+ |---------|----------------|
567
+ | Framework Preset | **Other** / null ??**not Next.js** |
568
+ | Install Command | `npm install` (or pnpm/yarn) |
569
+ | Build Command | `npm run build` (no-op exit 0) |
570
+ | Output Directory | **empty / null** ??**NOT `public`** |
571
+ | Start Command | `npm run start` ??`tsx server.ts` (or `.mts`) ??set in **dashboard**, not `vercel.json` |
572
+ | Fluid compute | **On** |
573
+
574
+ If `next` / `next.config.*` exist for Preview and the dashboard still auto-selects Next.js:
575
+
576
+ 1. Patch Framework Preset to **Other** via API/CLI/dashboard.
577
+ 2. Redeploy.
578
+ 3. Hit Production `GET /status` ??must be **JSON**, not HTML.
579
+ 4. Hit Production `GET /` ??must be Control Panel HTML, not `404: NOT_FOUND`.
580
+
581
+ If Vercel?s UI and this file disagree on bundling, **prefer whatever keeps a single long-lived Node HTTP server with WebSocket upgrade on `/` for Production**. Document the adaptation in the final summary.
582
+
583
+ ### WebSocket / multi-instance note
584
+
585
+ - Device sockets are pinned to **one** function/instance. TomorrowOS device maps are **in-memory**.
586
+ - For large fleets on multi-instance Vercel, prefer **one stable instance / region**, or accept that reconnects may land on another instance until the product adds shared presence (out of scope for this setup).
587
+ - Always use **Postgres** (Supabase or Neon) so pairings/playlists survive cold starts and deploys.
588
+ - **Pair TVs to the Production HTTPS origin**, not the v0 Preview URL.
589
+
590
+ ### What not to create
591
+
592
+ - ??`.replit`, `.replit-artifact`
593
+ - ??Next.js as the **Production** CMS (App Router replacing TomorrowOS)
594
+ - ??Root `api/` catch-all exporting raw `http.Server` to ?fix??Publish 404
595
+ - ??Separate ?API project??without the Control Panel
596
+ - ??Committing database passwords, Cloudinary secrets, or Blob tokens
597
+
598
+ ---
599
+
600
+ ## Question 1 ? Database (always)
601
+
602
+ > **Do not** ask screen counts. Present both options; recommend **Supabase** for Vercel fleets.
603
+ > **After the user picks a database:** collect the URL if needed, then **you** set `TOMORROWOS_STORE` + `DATABASE_SSL` (see mapping below). The customer must **never** be asked to type those two names or values.
604
+ > **Forbidden:** offering SQLite as a user-facing Q1 choice (ephemeral filesystem on Vercel).
605
+
606
+ ### Agent-owned env mapping (mandatory)
607
+
608
+ | User chose | You set (silently) | User only provides |
609
+ |------------|--------------------|--------------------|
610
+ | 1 Supabase | `TOMORROWOS_STORE=supabase`, `DATABASE_SSL=true` | `SUPABASE_URL` (pooler string) |
611
+ | 2 Neon | `TOMORROWOS_STORE=postgres`, `DATABASE_SSL=true` | `DATABASE_URL` (pooled string) |
612
+
613
+ If you open an Env configuration UI for Q1, fields visible to the user may include **only** `SUPABASE_URL` or `DATABASE_URL`. **Do not** show blank `TOMORROWOS_STORE` / `DATABASE_SSL` inputs ? write those yourself in the same step.
614
+
615
+ ### Step A ? Ask storage choice
616
+
617
+ **Ask exactly:**
618
+
619
+ > Which database should TomorrowOS use on Vercel?
620
+ >
621
+ > **1. Supabase Postgres (recommended)** ? durable pairing/playlists; use the **Session pooler** URL (`*.pooler.supabase.com:6543`).
622
+ >
623
+ > **2. Neon Postgres** ? serverless Postgres native to Vercel; use Neon's **pooled** connection string from the Neon dashboard (not the direct un-pooled host for serverless).
624
+ >
625
+ > Reply with **1** or **2** (or "Supabase" / "Neon").
626
+
627
+ ### Step B ??If **1 / Supabase**
628
+
629
+ **Ask exactly (connection string only ??not TOMORROWOS_STORE / DATABASE_SSL):**
630
+
631
+ > Paste your Supabase Postgres connection string. I will store it as **`SUPABASE_URL`**. I will set **`TOMORROWOS_STORE`** and **`DATABASE_SSL`** for you automatically.
632
+ >
633
+ > In Supabase: **Project Settings ??Database ??Connection string ??Connection pooling** (Session mode).
634
+ > Preferred shape:
635
+ > `postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres`
636
+ >
637
+ > **Do not** use the direct host `db.*.supabase.co:5432` for Vercel.
638
+
639
+ **You must then (automatic ??do NOT ask the user to type store/ssl vars):**
640
+
641
+ 1. Set Vercel Env Vars in one step:
642
+ - `SUPABASE_URL=<user pooler string>` ??from user
643
+ - `TOMORROWOS_STORE=supabase` ??**you set; never ask**
644
+ - `DATABASE_SSL=true` ??**you set; never ask**
645
+ 2. Wire `cms-app.ts` / `server.ts` store as in **Runtime & Vercel deploy rules**.
646
+ 3. Optional committed `.env.example` with **placeholders only**.
647
+ 4. **Do not** commit the real connection string.
648
+ 5. **Do not** open a form whose blank fields include `TOMORROWOS_STORE` or `DATABASE_SSL`.
649
+
650
+ ### Step C ??If **2 / Neon**
651
+
652
+ **Ask exactly (connection string only):**
653
+
654
+ > Paste your **Neon pooled** Postgres connection string. I will store it as **`DATABASE_URL`**. I will set **`TOMORROWOS_STORE`** and **`DATABASE_SSL`** for you automatically.
655
+ >
656
+ > In Neon: **Dashboard ??Connection details ??Pooled connection**.
657
+ > Typical shape:
658
+ > `postgresql://[user]:[password]@[endpoint]-pooler.[region].aws.neon.tech/[dbname]?sslmode=require`
659
+
660
+ **You must then (automatic):**
661
+
662
+ 1. Set Vercel Env Vars:
663
+ - `DATABASE_URL=<user pooled string>` ??from user
664
+ - `TOMORROWOS_STORE=postgres` ??**you set; never ask**
665
+ - `DATABASE_SSL=true` ??**you set; never ask**
666
+ 2. Wire `createTomorrowOSStore` for Postgres.
667
+ 3. **Do not** commit the real connection string.
668
+ 4. **Do not** ask the user to fill `TOMORROWOS_STORE` or `DATABASE_SSL` in chat or Env UI.
669
+ 5. If you see `ENETUNREACH` on `:5432`, switch to Neon's **pooled** URL.
670
+
671
+ **Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`** (all Q1 branches).
672
+
673
+ ---
674
+
675
+ ## Question 2 ??Media storage
676
+
677
+ > **This question is only about media files (images/videos).**
678
+ > It is **not** about OpenAI, AI pairing, or LLM keys. If you are about to ask for an `sk-` key, **stop** ??you are off-protocol.
679
+ > **Copy the two options below verbatim.** Wrong examples that must **never** appear: "Vercel Blob (recommended)", "Supabase Storage", "No media storage / disable uploads", "local disk only".
680
+
681
+ ### Step A ? Ask storage choice
682
+
683
+ **Ask exactly (wording must match ? Cloudinary is recommended):**
684
+
685
+ > How should playlist media (images/videos) be stored?
686
+ >
687
+ > **1. Cloudinary (recommended)** ? durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection. **Prefer this on Vercel.**
688
+ >
689
+ > **2. Vercel Blob** ? Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Use when you want media on the same Vercel project without a Cloudinary account.
690
+ >
691
+ > Reply with **1** or **2** (or "Cloudinary" / "Vercel Blob").
692
+
693
+ If you catch yourself about to offer Blob-as-recommended, Supabase Storage, local disk, or "no media" ? **stop and paste the block above instead**.
694
+
695
+ ### Step B ??If **1 / Cloudinary** (same Question 2 ??**one Env popup**)
696
+
697
+ > **Critical UX rule:** Immediately open the **Vercel Environment Variables configuration popup / form** with **all Cloudinary fields in one UI**. Do **not** ask for cloud name, then key, then secret in separate chat messages.
698
+
699
+ **Preferred:** Vercel Env Vars UI (or Agent secrets form) with these fields together:
700
+
701
+ | Field | Env name | Required |
702
+ |-------|----------|----------|
703
+ | Cloud name | `CLOUDINARY_CLOUD_NAME` | Yes |
704
+ | API Key | `CLOUDINARY_API_KEY` | Yes |
705
+ | API Secret | `CLOUDINARY_API_SECRET` | Yes |
706
+ | Folder (optional) | `CLOUDINARY_FOLDER` | No (e.g. `tomorrowos`) |
707
+
708
+ **If a multi-field popup is unavailable, ask once in a single message** (still not three turns):
709
+
710
+ > Paste Cloudinary credentials from [cloudinary.com/console](https://cloudinary.com/console) (I will store them as Vercel Env Vars):
711
+ >
712
+ > 1. `CLOUDINARY_CLOUD_NAME`
713
+ > 2. `CLOUDINARY_API_KEY`
714
+ > 3. `CLOUDINARY_API_SECRET`
715
+ > 4. optional `CLOUDINARY_FOLDER`
716
+
717
+ **You must then:**
718
+
719
+ 1. Save all three required Env Vars for Production + Preview + Development as appropriate.
720
+ 2. **Do not** invent placeholders.
721
+ 3. **Do not** proceed to Question 3 until all three required vars exist.
722
+ 4. **Do not** run a formal Cloudinary upload test unless the user asks.
723
+
724
+ The SDK auto-detects these env vars.
725
+
726
+ ### Step C - If **2 / Vercel Blob** (same Question 2)
727
+
728
+ **Ask exactly:**
729
+
730
+ > I will enable **Vercel Blob** for media uploads.
731
+ >
732
+ > 1. In the Vercel project: **Storage ? Create ? Blob** (or link an existing Blob store to this project).
733
+ > 2. Confirm **`BLOB_READ_WRITE_TOKEN`** is available (Vercel usually injects it when Blob is linked).
734
+ >
735
+ > Paste the token only if it is not already set in your project Env Vars. Do you already have Blob linked on this Vercel project? (yes / no)
736
+
737
+ **You must then:**
738
+
739
+ 1. Set Vercel Env Var: `BLOB_READ_WRITE_TOKEN=<token>` (if not auto-injected). Prefer the Env popup when available.
740
+ 2. Set Vercel Env Var yourself (agent-owned): `TOMORROWOS_MEDIA=vercel-blob`. **Do not** ask the user to type this.
741
+ 3. Ensure `@tomorrowos/sdk@0.9.70+` is installed (`@vercel/blob` is a transitive dependency ? no project-level Blob bridge / custom `put()` middleware).
742
+ 4. The SDK **natively** uploads via Vercel Blob when the token is present (same HTTP routes as local: `/media/upload` and chunked complete). Returned asset URLs are absolute `https://*.public.blob.vercel-storage.com/...`.
743
+ 5. Confirm Control Panel **Media Server** shows provider **Blob** (not Local). Do **not** invent status copy about "Blob bridge" or paste the URL pattern as a status detail.
744
+ 6. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
745
+
746
+ **Forbidden for Q2=Blob:** scaffolding a custom "Blob bridge", writing status text like "uploads persist to durable https://*.public.blob.vercel-storage.com/ URLs via the Blob bridge", or leaving uploads on local `cms-panel/uploads`.
747
+ ---
748
+
749
+ ## Question 3 - Brand form (`brand.json` only; **not a choice question**; **must wait**)
750
+
751
+ > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, `vercel.json`, or `server.ts` store wiring beyond what Q1-Q2 already required.
752
+ >
753
+ > **IRON RULE - form, not question; show then WAIT:** As soon as Q2 is done, **open the brand form**. Do **not** ask a choice first. Show **all seven optional fields on one screen at the same time**. Then **HARD STOP** until the user submits the form, replies with values, or says **"skip"**.
754
+ >
755
+ > **Optional fields ? optional step.** Leaving a field blank keeps the starter default **after the user responds**. It does **not** authorize you to skip waiting or auto-apply defaults in the same turn.
756
+ >
757
+ > - **Forbidden:** any preliminary question ("customize branding?", "skip or set brand?", Option A/B)
758
+ > - **Forbidden:** multiple-choice / radio / "reply 1 or 2" for branding
759
+ > - **Forbidden:** showing only Product Name + Primary colour (or any subset under 7)
760
+ > - **Forbidden:** field-by-field Next wizard (name -> wait -> tagline -> ...)
761
+ > - **Forbidden:** one freeform / Other box for all brand values
762
+ > - **Forbidden:** auto-continuing to init / scaffold / env / deploy because "fields are optional" or "I shouldn't re-ask"
763
+ > - **Required:** six text inputs + Logo Upload above Logo URL, all visible together
764
+ > - **Required:** wait for one user action (submit / values / "skip") before scaffolding continues
765
+ >
766
+ > **Self-check:** If the UI you are about to show has fewer than six text fields for name/tagline/four colours, **abort and rebuild** - that UI is off-protocol.
767
+ >
768
+ > **Every field is optional** once the form is shown. After the user submits (including all-blank) or says "skip": keep starter **`brand.json`** for blanks; **do not** re-ask Q3 a second time. **Do not** invent a second branding prompt.
769
+ >
770
+ > Without `CMS_PASSWORD`, the Control Panel has **no login**. Do **not** invent custom login/auth. Optional **Question 4** may set Env `CMS_PASSWORD` for Control Panel auth only (never Vercel Deployment Protection / whole-site password).
771
+
772
+ ### Preferred UI (open immediately - all seven at once - then wait)
773
+
774
+ Open **one** dialog titled **Brand your TomorrowOS experience**. Render **each row as its own control**. All rows must appear **together** on first paint - not revealed after a choice, not after filling a previous field. After opening, **end your turn** and wait.
775
+
776
+ | # | Exact label | Control type | Blank default |
777
+ |---|-------------|--------------|---------------|
778
+ | 1 | **Product Name** | text input | existing `brand.json` `name` (e.g. `My Venue`) |
779
+ | 2 | **Tagline** | text input | existing tagline (e.g. `Digital signage`) |
780
+ | 3 | **Primary colour** | text input (hex) | e.g. `#FF8A3D` |
781
+ | 4 | **Background colour** | text input (hex) | e.g. `#FAFAF9` |
782
+ | 5 | **Text colour** | text input (hex) | e.g. `#0A0908` |
783
+ | 6 | **Secondary colour** | text input (hex) | e.g. `#F5F3EF` |
784
+ | 7 | **Logo** | **Upload** (SVG/PNG file picker) **above** **Logo URL** (text input) | keep `./assets/logo.svg` |
785
+
786
+ Helper text: *All fields optional. Leave blank to keep starter brand.json defaults. Submit once, or reply "skip". I will wait before continuing setup.*
787
+
788
+ **Logo UX:** Upload control directly above Logo URL. Either is fine; if both provided, prefer the uploaded file.
789
+
790
+ ### If a form UI is unavailable - one chat message only (still wait)
791
+
792
+ Only when the platform **cannot** render multi-field forms. Do **not** ask a choice first. Send **exactly one** message listing all seven fields, then **end your turn and wait** (user may answer partially, attach a logo, or say "skip"):
793
+
794
+ > Brand your TomorrowOS experience (**all optional - reply once**). Leave any field blank to keep starter `brand.json` defaults. I will wait for your reply before scaffolding:
795
+ >
796
+ > 1. **Product Name**
797
+ > 2. **Tagline**
798
+ > 3. **Primary colour** (hex)
799
+ > 4. **Background colour** (hex)
800
+ > 5. **Text colour** (hex)
801
+ > 6. **Secondary colour** (hex)
802
+ > 7. **Logo** - attach SVG/PNG or paste a Logo URL
803
+ >
804
+ > Or reply **skip** to keep all defaults.
805
+
806
+ ### After the user submits
807
+
808
+ **You must:**
809
+
810
+ 1. For each blank field, **keep the existing starter `brand.json` value** - do not invent new colours/names.
811
+ 2. For filled fields, write them into `brand.json` only.
812
+ 3. If Logo is a URL: fetch into `./assets/logo.png` or `./assets/logo.svg` when possible and set `logoPath`; if fetch fails, keep existing `logoPath` and note it.
813
+ 4. If Logo is an upload: save under `./assets/` and set `logoPath` (prefer upload over URL when both are present).
814
+ 5. Always set **`cms.hostingTarget`: `"vercel"`** without asking.
815
+ 6. Default `cms.expectedScreens` to `5` unless the user already volunteered a number.
816
+ 7. Show a one-line summary (name + primary hex + logo path) - **do not** ask a second branding question.
817
+ 8. **Do not** build login, signup, OAuth, or clone a marketing site from branding answers.
818
+
819
+ ### Write `brand.json`
820
+
821
+ Validate mentally against `brand.schema.json`. Minimum shape (values = user input **or** existing defaults):
822
+
823
+ ```json
824
+ {
825
+ "name": "<Product Name or existing brand.json name>",
826
+ "tagline": "<Tagline or existing>",
827
+ "targetPlatforms": ["tizen"],
828
+ "primaryColor": "#FF8A3D",
829
+ "secondaryColor": "#F5F3EF",
830
+ "backgroundColor": "#FAFAF9",
831
+ "textColor": "#0A0908",
832
+ "logoPath": "./assets/logo.svg",
833
+ "fontFamily": "Inter",
834
+ "cms": {
835
+ "useCase": "other",
836
+ "hostingTarget": "vercel",
837
+ "expectedScreens": 5,
838
+ "features": {
839
+ "bulkCommands": false,
840
+ "proofOfPlay": false,
841
+ "contentScheduling": true,
842
+ "userManagement": false
843
+ }
844
+ },
845
+ "protocolVersion": "1.0"
846
+ }
847
+ ```
848
+
849
+ Set `cmsEndpoint` only if they already know the public `https://?vercel.app` URL; otherwise tell them to pair TVs with the **Published** HTTPS origin (player maps `https://` ? `wss://`).
850
+
851
+ ---
852
+
853
+ ## Question 4 — Optional CMS password (`CMS_PASSWORD`)
854
+
855
+ > **Optional.** Protect the Control Panel with app-level auth. You may **skip** and set Env Var `CMS_PASSWORD` later (then Redeploy).
856
+
857
+ **Ask (Env input or one chat reply):**
858
+
859
+ > Optional: set a Control Panel password now? If yes, enter the password to store as Environment Variable **`CMS_PASSWORD`**. Leave blank / skip to keep the panel open for now — you can add this later in Vercel → Settings → Environment Variables and redeploy.
860
+
861
+ **Why:**
862
+ - Locks the **CMS Control Panel only** (not the whole deployment).
863
+ - Devices still reach WebSocket / brand / uploads without that password.
864
+ - Safer than Vercel **Deployment Protection** / whole-site password (those break Tizen/BrightSign).
865
+ - Leaving it blank is fine; add `CMS_PASSWORD` later when you want the login page.
866
+
867
+ **Accept:**
868
+ - blank / skip / "later" / "no" → do **not** set `CMS_PASSWORD`
869
+ - any non-empty password → set Vercel Env **`CMS_PASSWORD`** (Production + Preview as needed)
870
+
871
+ **Reject / clarify:**
872
+ - Do **not** enable Vercel Deployment Protection / whole-site password for device reachability.
873
+ - Do **not** put the password in committed source.
874
+
875
+ ---
876
+
877
+ ## After all answers ? execution checklist
878
+
879
+ ### A. Seed the project
880
+
881
+ ```bash
882
+ npx @tomorrowos/sdk@latest init .
883
+ # If not empty and user confirms starter overwrite:
884
+ # npx @tomorrowos/sdk@latest init . --force
885
+ ```
886
+
887
+ Then (**Scheme A ??mandatory for Publish**):
888
+
889
+ 1. Keep **`server.ts`** at **project root** with top-level `listen()` + `export default server`.
890
+ 2. If Preview needs Next: **move** `next.config.*`, `app/`, Next `tsconfig` / `postcss` / `next-env.d.ts` into **`preview/`** ??remove them from root so Production is not mistaken for Next/static.
891
+ 3. Write `vercel.json` per **Scheme A3** (`fluid`, `framework: null`, no-op `buildCommand` ??**no** `startCommand`).
892
+ 4. **Force Project Settings** per **Scheme A4** (Framework = Other, Output Directory empty, Start = `npm run start`, Fluid on). Patch dashboard via API if it still says Next.js.
893
+ 5. Do **not** add root `api/` catch-all for raw `http.Server`.
894
+ 6. Do **not** add `.replit*`.
895
+ 7. If Q2 = Vercel Blob: set `BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob` (SDK-native; no custom bridge).
896
+
897
+ ### B. Environment Variables (Vercel dashboard + local)
898
+
899
+ | Name | Required | Who fills it |
900
+ |------|----------|--------------|
901
+ | `SUPABASE_URL` | If Q1 = Supabase | User pastes URL |
902
+ | `DATABASE_URL` | If Q1 = Neon | User pastes URL |
903
+ | `TOMORROWOS_STORE` | Yes | **Agent auto-sets only** ? never a blank field for the user (`supabase` / `postgres`) |
904
+ | `DATABASE_SSL` | Yes for Postgres | **Agent auto-sets only** ??never a blank field for the user (`true`) |
905
+ | `CLOUDINARY_*` | If Q2 = Cloudinary | User via **one Env popup** (all fields) |
906
+ | `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | User / Blob link |
907
+ | `TOMORROWOS_MEDIA` | If Q2 = Vercel Blob | **Agent auto-sets** `vercel-blob` |
908
+ | `TOMORROWOS_INTERNAL_PORT` | Preview only | Agent (e.g. `3001`) |
909
+ | `PORT` | Optional | Vercel injects for Production |
910
+ | `CMS_PASSWORD` | Optional (Q4) | User if set; blank/skip = omit (can add later) |
911
+
912
+ Mirror non-secrets in `.env` for local `npm run dev` if helpful; never commit secrets.
913
+
914
+ ### C. Install and run
915
+
916
+ ```bash
917
+ npm install
918
+ npm run start
919
+ # Preview / v0: npm run dev:preview (Next + TomorrowOS internal)
920
+ ```
921
+
922
+ Confirm logs: `[TomorrowOS] listening on http://0.0.0.0:?`
923
+ Confirm browser:
924
+
925
+ - **Preview:** Control Panel visible through the Next proxy at `/`
926
+ - **Production start:** Control Panel served directly by TomorrowOS at `/`
927
+
928
+ ### D. Deploy (Publish ??Node only)
929
+
930
+ ```bash
931
+ vercel --prod
932
+ # or git push ??Vercel Git integration
933
+ ```
934
+
935
+ **Publish acceptance checklist (all must pass ??Scheme A5):**
936
+
937
+ 1. Framework Preset = **Other** / null (dashboard confirms ??not Next.js)
938
+ 2. Output Directory = **empty** (not `public`)
939
+ 3. Start Command = Node `TomorrowOS.listen` (`npm run start` / `tsx server.ts`)
940
+ 4. Fluid compute on
941
+ 5. Env Vars present for Production
942
+ 6. Production `GET /status` returns **JSON** ??**not** HTML (`Unexpected token 'T'` means this failed)
943
+ 7. Production `GET /` returns Control Panel HTML ??**not** `404: NOT_FOUND`
944
+
945
+ If step 6 fails but step 7 passes: **static `public/` trap** ??Vercel is not running Node. Re-apply Scheme A3?A4; try **Path B** (Git / `vercel --prod`); do **not** invent `api/` + `rewrites`.
946
+
947
+ Tell the user:
948
+
949
+ - **Control Panel (Publish):** `https://YOUR-PROJECT.vercel.app`
950
+ - **TV CMS endpoint:** same HTTPS origin (players use `wss://`)
951
+ - **Preview:** Next shell is for v0 only — do not pair devices to Preview
952
+
953
+ Then **you must tell the user how to install a player and pair**. Do **not** only link a file — spell out the steps:
954
+
955
+ #### Samsung Tizen (6.5 and 7.0)
956
+
957
+ 1. On the display, open **App Management** → install via **Custom App** URL:
958
+
959
+ ```txt
960
+ https://tmr.sh/tizen
961
+ ```
962
+
963
+ Or use Control Panel → **Download Players → Samsung** and install via USB.
964
+ 2. Choose orientation, then enter the **Production** CMS URL: `https://YOUR-PROJECT.vercel.app/` (never Preview, never `localhost`).
965
+ 3. Enter the **six-character** pairing code in Control Panel → **Pair**.
966
+
967
+ #### BrightSign (Series 3-6)
968
+
969
+ 1. On the **Production** Control Panel → **Download Players → BrightSign** (preferred — `cmsEndpoint` is filled for this CMS),
970
+ or download `https://tmr.sh/app/brightsign/brightsign_package.zip` and set `cmsEndpoint` in `config.js` to `https://YOUR-PROJECT.vercel.app/`.
971
+ 2. Unzip and copy the **contents** to the SD card root (`autorun.brs`, `config.js`, player files).
972
+ 3. Power-cycle the player, wait ~10s through the black boot window, then pair with the six-character code.
973
+
974
+ 4. Create a playlist, upload media, **Publish** to the device.
975
+
976
+ More detail: https://docs.tomorrowos.org/docs/os/tizen and https://docs.tomorrowos.org/docs/os/brightsign
977
+
978
+ ### E. Minimal verification only
979
+
980
+ **Do:**
981
+
982
+ 1. Preview: Control Panel HTML at `/` (via Next proxy in `preview/` if applicable)
983
+ 2. Publish: `GET /status` ??JSON; `GET /` ??Control Panel from **Node** TomorrowOS
984
+ 3. Publish: panel does **not** show `Unexpected token 'T'` / CMS unreachable
985
+ 4. Server status: Media OK when Cloudinary or Blob configured
986
+ 5. Database: if ERROR shows **`ENETUNREACH` ??IPv6 ??`:5432`**, switch to **pooled** URL (Supabase `:6543` or Neon pooler) and redeploy ??call this out explicitly to the user
987
+ 6. Fluid compute on for Production
988
+
989
+ **Do not block** on long WebSocket/device pairing tests unless the user asks. Prefer pairing against **Publish**, not Preview.
990
+
991
+ ---
992
+
993
+ ## Failure recovery cheat sheet
994
+
995
+ | Symptom | Likely cause | Fix |
996
+ |---------|--------------|-----|
997
+ | Publish `404: NOT_FOUND` | Framework Preset still **Next.js** / no Function | Scheme A: Framework = **Other**, Fluid on, `api/index.ts` + rewrites; redeploy |
998
+ | Panel loads but **CMS unreachable** / `Unexpected token 'T', "The page c"...` | **`public/` deployed as static site** ??no Function | Rename to `cms-panel/`; Output empty; Scheme A Function entry; **Path B** if needed |
999
+ | Only `/api/status` returns JSON, `/status` is 404 | Missing **rewrites** to `/api` | Add Scheme A3 rewrite `/(.*) ??/api` (exclude existing `/api/`); redeploy |
1000
+ | Control Panel OK, TV **could not connect** | WebSocket upgrade not reaching Function (200 HTML) | Fluid on; `export default server`; verify 101 on `/` and `/api`; check WebSockets permission |
1001
+ | `vercel.json` error: invalid `startCommand` | v0 schema rejects it | Remove from `vercel.json`; Function entry does not need startCommand |
1002
+ | Separate `api/ws.ts` with a second `new TomorrowOS` | Two isolates ??pairing memory split | **One** Function only (`api/index.ts` ??shared `cms-app.ts`) |
1003
+ | `vercel inspect` shows Output = `public if it exists` | Framework=Other static default | Force Output Directory **empty**; use `cms-panel/`; Path B redeploy |
1004
+ | Build runs `next build` / missing routes-manifest | Next auto-detected from root `next.config` / `app/` | Move Next to `preview/`; Framework null/Other; `build` = no-op |
1005
+ | Preview blank but logs show TomorrowOS listening | v0 Next intercepts public port | Add Next reverse proxy ??internal TomorrowOS port |
1006
+ | Preview works, Publish broken / Next-only | Production still on Next | Switch Production to Fluid Function `api/index.ts` |
1007
+ | `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
1008
+ | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
1009
+ | Uploads break / broken thumbs in prod | Local uploads only | Cloudinary or Vercel Blob |
1010
+ | Blob uploads 401 / missing token | Blob not linked | Vercel Storage ??Blob + `BLOB_READ_WRITE_TOKEN` |
1011
+ | WebSocket fails on Publish | Fluid off / static-only / Next as Production / no Function WS | Enable Fluid; Scheme A; test 101 upgrade |
1012
+ | Devices fail only on Preview | Next proxy cannot upgrade `wss` | Expected ??pair on Publish URL |
1013
+ | Agent asked user to fill `TOMORROWOS_STORE` / `DATABASE_SSL` | Off-protocol | Agent must auto-set after DB choice; remove those fields from user Env forms |
1014
+ | Agent offered Blob-as-recommended / Supabase Storage / local disk / "no media" for Q2 | Invented options | Re-ask Q2 verbatim: **1 Cloudinary (recommended)**, 2 Vercel Blob |
1015
+ | Agent asked Cloudinary key/secret in three chat turns | Off-protocol | Use **one Env popup** with all Cloudinary fields |
1016
+ | Agent asked for OpenAI / `sk-` key during setup | Hallucinated ?AI pairing??requirement | **Refuse.** TomorrowOS does not need LLM keys. Return to Q2 media choices only |
1017
+ | Agent asked a branding choice / Option A-B / "customize?" before the form | Off-protocol Q3 | Close it; immediately open the **7-field form** (no choice step) |
1018
+ | Agent showed only Name + Primary colour (or any subset) | Off-protocol Q3 | Rebuild with **all seven** inputs visible at once |
1019
+ | Agent auto-continued after showing Q3 because "fields are optional" / "shouldn't re-ask" | Off-protocol Q3 | **HARD STOP** and wait for submit or explicit "skip"; do not scaffold yet |
1020
+ | Agent asked branding as Option A URL vs Option B, or field-by-field Next | Off-protocol Q3 | Re-open Q3 as **seven separate optional inputs** in one dialog; blanks keep `brand.json` |
1021
+ | Agent enabled Vercel Deployment Protection / whole-site password | Blocks Tizen/BrightSign | Disable host gate; use optional `CMS_PASSWORD` for Control Panel only |
1022
+ | Agent built a login page from branding answers | Misread brand step as product scope | Remove login; keep starter Control Panel; apply answers only to `brand.json` |
1023
+ | Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
1024
+ | Replit files present | Copied wrong protocol | Delete `.replit*`; use this file |
1025
+
1026
+ ---
1027
+
1028
+ ## How this differs from `REPLIT_SETUP.md`
1029
+
1030
+ | Topic | Replit | Vercel |
1031
+ |-------|--------|--------|
1032
+ | Secrets | Replit Secrets | Vercel Environment Variables |
1033
+ | Deploy config | `.replit`, artifact `kind=web` | `vercel.json` + Fluid; **Framework = Other**; **no** `.replit` |
1034
+ | `hostingTarget` | `"here"` | `"vercel"` |
1035
+ | Database | Supabase (primary) | Supabase **or Neon** (pooled) |
1036
+ | Media | Cloudinary or Replit Object Storage | Cloudinary **or Vercel Blob** |
1037
+ | Preview | Replit Preview / Autoscale Node | **Next shell + proxy** when v0 hardwires Next |
1038
+ | Publish | Long-lived Node | **Node `TomorrowOS.listen` only** (not Next); gate on no `404 NOT_FOUND` |
1039
+ | Brand | Replit 7-field optional dialog | **Form not question:** all 7 optional inputs at once; no choice first; Logo Upload above Logo URL |
1040
+
1041
+ ---
1042
+
1043
+ ## Final summary template (Agent — human)
1044
+
1045
+ After setup, report:
1046
+
1047
+ 1. **Live URL** (Production Publish)
1048
+ 2. **Publish health:** Framework = Other; Output — `public`; `GET /status` — JSON; `GET /` OK
1049
+ 3. **Preview note:** Next proxy shell used / not needed
1050
+ 4. **Env Vars set** (names only — never values); note which were auto-set (`TOMORROWOS_STORE`, `DATABASE_SSL`)
1051
+ 5. **Store:** Supabase pooler or Neon pooled
1052
+ 6. **Media:** Cloudinary or Vercel Blob
1053
+ 7. **Brand:** name + `hostingTarget: vercel` (from optional Q3 fields; blanks = starter defaults)
1054
+ 8. **CMS password:** set or skipped (yes/no only — never echo the password)
1055
+ 9. **Runtime split:** Preview = …; Publish = Node TomorrowOS (confirm **not** Next)
1056
+ 10. **Open action:** if Database still ERROR, paste the pooler instruction again
1057
+ 11. **Connect a screen:** include the Tizen Custom App URL / BrightSign SD install + six-character pairing steps from section **D. Deploy** above (required — do not skip)
1058
+
1059
+ Protocol complete when:
1060
+
1061
+ - **Preview** shows the Control Panel at `/` (via Next proxy if required), **and**
1062
+ - **Publish** returns Control Panel at `/` **and** `/status` JSON from pure Node `TomorrowOS.listen` with Fluid (not static `public/`, not `404: NOT_FOUND`, not `Unexpected token 'T'`), **and**
1063
+ - Q1–Q4 configuration is saved, **and**
1064
+ - the user has been told how to install Tizen and/or BrightSign and pair
1065
+
1066
+ ? even if a sandbox DB probe still flakes **after** the pooled URL is correctly set for Production.
1067
+
1068
+ ---
1069
+
1070
+ ## Protocol version
1071
+
1072
+ `vercel-setup/1.16` - pairs with `@tomorrowos/sdk` 0.9.70+ and `VERCEL_QUESTIONS.md` 1.6.
1073
+
1074
+ **Changelog 1.16:** Question 4 is optional Control Panel Env `CMS_PASSWORD` (blank/skip allowed; can set later). Do not use Vercel Deployment Protection / whole-site password for devices.
1075
+
1076
+ **Changelog 1.15:** After Publish is healthy, agents **must** tell the user how to install Tizen (Custom App `https://tmr.sh/tizen`) and BrightSign (CMS zip → SD root) and pair against the **Production** URL. Final summary requires this screen-connect step.
1077
+
1078
+ **Changelog 1.14:** Vercel Blob is **SDK-native** (`BLOB_READ_WRITE_TOKEN` + `TOMORROWOS_MEDIA=vercel-blob`). No project Blob bridge. Status providers use display names (Neon, Supabase, Blob, Cloudinary, Replit Object Storage). Forbidden marketing status copy about blob URL patterns.
1079
+
1080
+ **Changelog 1.13:** Q3 must **HARD STOP and WAIT** for user submit or "skip". Optional fields do **not** authorize auto-continuing with defaults. Clarifies "do not re-ask" = after the user already replied, not "skip waiting".
1081
+
1082
+ **Changelog 1.12:** Q3 is a **form, not a question**. No preliminary branding choice. Must show **all seven** optional inputs at once (never only Name + Primary colour). Logo = Upload above Logo URL.
1083
+
1084
+ **Changelog 1.11:** Q3 must render **seven separate input controls** (not one freeform / Other box). Logo field = **Upload** above **Logo URL**; blanks keep starter `brand.json`.
1085
+
1086
+ **Changelog 1.10:** Q3 is a **single** seven-field optional branding dialog (Product Name, Tagline, Primary / Background / Text / Secondary colour, Logo URL or upload). Removed Option A website-URL vs Option B split. Blanks keep starter `brand.json`. Pairs with new `VERCEL_UPGRADE.md`.
1087
+