@fluxy-chat/create-fluxy-chat 0.5.14 → 0.5.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/index.js +9 -4
  3. package/package.json +1 -1
  4. package/readme.md +2 -1
  5. package/templates/deal-room/src/App.tsx +17 -1
  6. package/templates/full/README.md +14 -4
  7. package/templates/full/fluxy.hosted.json +15 -0
  8. package/templates/full/package.json +4 -3
  9. package/templates/full/scripts/fluxy-deploy.mjs +53 -0
  10. package/templates/full/scripts/fluxy-setup.mjs +31 -7
  11. package/templates/full/src/App.tsx +14 -0
  12. package/templates/full/src/session.ts +7 -2
  13. package/templates/full/src/vite-env.d.ts +2 -1
  14. package/templates/live-cursors/.env.example +8 -5
  15. package/templates/live-cursors/package.json +2 -2
  16. package/templates/live-cursors/src/App.tsx +19 -2
  17. package/templates/live-cursors/src/vite-env.d.ts +1 -0
  18. package/templates/minimal/package.json +1 -1
  19. package/templates/polls/.env.example +13 -0
  20. package/templates/polls/README.md +13 -0
  21. package/templates/polls/index.html +12 -0
  22. package/templates/polls/package.json +24 -0
  23. package/templates/polls/src/App.tsx +135 -0
  24. package/templates/polls/src/index.css +85 -0
  25. package/templates/polls/src/main.tsx +10 -0
  26. package/templates/polls/src/session.ts +73 -0
  27. package/templates/polls/src/vite-env.d.ts +13 -0
  28. package/templates/polls/tsconfig.json +21 -0
  29. package/templates/polls/vite.config.ts +7 -0
  30. package/templates/react/.env.example +9 -5
  31. package/templates/react/README.md +7 -32
  32. package/templates/react/package.json +2 -2
  33. package/templates/react/src/App.tsx +31 -23
  34. package/templates/react/src/vite-env.d.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.19] - 2026-09-02
4
+
5
+ ### Added
6
+
7
+ - Full template `fluxy.hosted.json` deploys `rooms` templates and extension slots with `pnpm fluxy:deploy`.
8
+ - Pin `@fluxy-chat/sdk@^0.6.8` (`connectInbox`, room extensions).
9
+
10
+ ## [0.5.18] - 2026-09-01
11
+
12
+ ### Added
13
+
14
+ - Hosted `setup:hosted` reads `GET /public/demo-credentials` and writes `VITE_FLUXYCHAT_PUBLISHABLE_KEY`.
15
+ - `pnpm fluxy:deploy` on the full template: `fluxy.hosted.json` → `PUT /admin/projects/:id/publish-config`.
16
+ - React and live-cursors templates prefer `publishableKey` (pin sdk `^0.6.7` / react `^0.1.6`).
17
+
18
+ ## [0.5.17] - 2026-09-01
19
+
20
+ ### Added
21
+
22
+ - Gallery `--example polls`: `createPoll` / `votePoll`. Prefers `VITE_FLUXYCHAT_PUBLISHABLE_KEY`. Documents `POST /polls` `isAnonymous`.
23
+
24
+ ## [0.5.16] - 2026-08-31
25
+
26
+ ### Added
27
+
28
+ - `deal-room` template: Ask agent when `VITE_FLUXYCHAT_AGENT_ID` is set (`invokeAgent`).
29
+
30
+ ## [0.5.15] - 2026-08-31
31
+
32
+ ### Changed
33
+
34
+ - Help text states the scoped package: `npx @fluxy-chat/create-fluxy-chat@latest`. Unscoped `create-fluxy-chat` is not ours.
35
+
3
36
  ## [0.5.14] - 2026-08-27
4
37
 
5
38
  ### Changed
package/dist/index.js CHANGED
@@ -888,6 +888,7 @@ var GALLERY_EXAMPLES = [
888
888
  "game-tick",
889
889
  "voice-stage",
890
890
  "comments-board",
891
+ "polls",
891
892
  "whiteboard"
892
893
  ];
893
894
  function parseArgs(argv) {
@@ -983,6 +984,9 @@ function parseArgs(argv) {
983
984
  var HELP_TEXT = `
984
985
  ${pc.bold("create-fluxy-chat")} \u2014 Scaffold a FluxyChat app or bot worker
985
986
 
987
+ ${pc.yellow("Use the scoped package:")} npx @fluxy-chat/create-fluxy-chat@latest
988
+ Unscoped ${pc.bold("npx create-fluxy-chat")} is not ours and will 404.
989
+
986
990
  ${pc.bold("Usage:")}
987
991
  npx @fluxy-chat/create-fluxy-chat [project-name] [options]
988
992
 
@@ -1008,7 +1012,7 @@ ${pc.bold("Examples:")}
1008
1012
  ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-cursors --example live-cursors")}
1009
1013
  ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-doc --example tiptap-room")}
1010
1014
  ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-war --example war-room")}
1011
- ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-iot --example iot-panel")}
1015
+ ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-polls --example polls")}
1012
1016
  ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-chat --minimal")}
1013
1017
  ${pc.cyan("npx @fluxy-chat/create-fluxy-chat@latest my-bot --adapter slack")}
1014
1018
  `;
@@ -1084,6 +1088,7 @@ async function main() {
1084
1088
  "game-tick": "# Matchmake + start, then Submit input (not netcode)",
1085
1089
  "voice-stage": "# Open two tabs \u2014 join speaker/listener (signaling, not WebRTC)",
1086
1090
  "comments-board": "# Click the canvas to pin a thread",
1091
+ polls: "# Create a poll, open a second tab, vote",
1087
1092
  "live-cursors-chat": "# Open two tabs \u2014 move and chat",
1088
1093
  whiteboard: "# Open two tabs \u2014 draw strokes (Yjs, not a second CRDT)"
1089
1094
  };
@@ -1092,7 +1097,7 @@ async function main() {
1092
1097
  [
1093
1098
  `cd ${projectName}`,
1094
1099
  "cp .env.example .env",
1095
- "# Set VITE_FLUXYCHAT_WORKER_URL + public room ID or member JWT",
1100
+ "# Set VITE_FLUXYCHAT_WORKER_URL + pk_ (or public room ID / member JWT)",
1096
1101
  `${devCmd2} dev`,
1097
1102
  tryHint
1098
1103
  ].join("\n"),
@@ -1253,7 +1258,7 @@ async function main() {
1253
1258
  const devCmd = config.packageManager === "npm" ? "npm run" : config.packageManager;
1254
1259
  const nextSteps = config.full || config.adapter === "full" ? config.mode === "hosted" ? [
1255
1260
  `cd ${config.name}`,
1256
- `${devCmd} setup:hosted # guest JWT from fluxychat.com`,
1261
+ `${devCmd} setup:hosted # writes pk_ when /public/demo-credentials is on`,
1257
1262
  `${devCmd} dev # http://localhost:5173`,
1258
1263
  `# Keep this project: https://fluxychat.com/onboarding?from=cli`
1259
1264
  ].join("\n") : [
@@ -1266,7 +1271,7 @@ async function main() {
1266
1271
  ].join("\n") : config.adapter === "react" || config.minimal ? [
1267
1272
  `cd ${config.name}`,
1268
1273
  "cp .env.example .env",
1269
- "# Set VITE_FLUXYCHAT_WORKER_URL + JWT or public room ID",
1274
+ "# Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLISHABLE_KEY",
1270
1275
  `${devCmd} dev`,
1271
1276
  "# In the room, send: @assistant hello"
1272
1277
  ].join("\n") : [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluxy-chat/create-fluxy-chat",
3
- "version": "0.5.14",
3
+ "version": "0.5.19",
4
4
  "description": "Scaffold a new FluxyChat bot project with a single command",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/readme.md CHANGED
@@ -36,6 +36,7 @@ npx @fluxy-chat/create-fluxy-chat@latest my-fleet --example fleet-panel
36
36
  npx @fluxy-chat/create-fluxy-chat@latest my-game --example game-tick
37
37
  npx @fluxy-chat/create-fluxy-chat@latest my-stage --example voice-stage
38
38
  npx @fluxy-chat/create-fluxy-chat@latest my-comments --example comments-board
39
+ npx @fluxy-chat/create-fluxy-chat@latest my-polls --example polls
39
40
  npx @fluxy-chat/create-fluxy-chat@latest my-chat --template react -y
40
41
  npx @fluxy-chat/create-fluxy-chat@latest my-bot --adapter slack --pm pnpm
41
42
  ```
@@ -51,7 +52,7 @@ npx @fluxy-chat/create-fluxy-chat@latest my-bot --adapter slack --pm pnpm
51
52
  | `--yes` | `-y` | Skip prompts and accept defaults |
52
53
  | `--full` | | Full stack template: chat + `@assistant` + `pnpm setup` / `pnpm dev` |
53
54
  | `--minimal` | | Chat-only widget (`@fluxy-chat/ui-kit`), no platform modules |
54
- | `--example <name>` | | Gallery: `live-cursors`, `live-cursors-chat`, `javascript-live-cursors`, `tiptap-room`, `war-room`, `iot-panel`, `draw`, `deal-room`, `fleet-panel`, `game-tick`, `voice-stage`, `comments-board`, `whiteboard` |
55
+ | `--example <name>` | | Gallery: `live-cursors`, `live-cursors-chat`, `javascript-live-cursors`, `tiptap-room`, `war-room`, `iot-panel`, `draw`, `deal-room`, `fleet-panel`, `game-tick`, `voice-stage`, `comments-board`, `polls`, `whiteboard` |
55
56
  | `--skip-install` | | Skip dependency installation |
56
57
  | `--no-git` | | Skip git repository initialization |
57
58
  | `--help` | `-h` | Show help |
@@ -4,9 +4,10 @@ import { useFluxySession, workerUrl } from "./session";
4
4
 
5
5
  function DealBoard({ roomId }: { roomId: string }) {
6
6
  const { client } = useFluxyChat();
7
- const { messages, sendMessage, connected } = useChat({ roomId, replay: "request" });
7
+ const { messages, sendMessage, connected, invokeAgent } = useChat({ roomId, replay: "request" });
8
8
  const [draft, setDraft] = useState("Approve term sheet?");
9
9
  const [busy, setBusy] = useState(false);
10
+ const agentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim();
10
11
 
11
12
  async function propose() {
12
13
  const content = draft.trim();
@@ -45,6 +46,16 @@ function DealBoard({ roomId }: { roomId: string }) {
45
46
  URL.revokeObjectURL(url);
46
47
  }
47
48
 
49
+ async function askAgent() {
50
+ if (!agentId) return;
51
+ setBusy(true);
52
+ try {
53
+ await invokeAgent(agentId, "Summarize this deal thread and list open decisions.");
54
+ } finally {
55
+ setBusy(false);
56
+ }
57
+ }
58
+
48
59
  return (
49
60
  <section className="panel">
50
61
  <header className="chat-header">
@@ -87,6 +98,11 @@ function DealBoard({ roomId }: { roomId: string }) {
87
98
  <button type="button" className="primary" onClick={() => void exportMarkdown()}>
88
99
  Export .md
89
100
  </button>
101
+ {agentId ? (
102
+ <button type="button" disabled={busy} onClick={() => void askAgent()}>
103
+ Ask agent
104
+ </button>
105
+ ) : null}
90
106
  </form>
91
107
  </section>
92
108
  );
@@ -1,6 +1,6 @@
1
1
  # FluxyChat full stack starter
2
2
 
3
- Chat + AI agent in a Vite app. Hosted mode starts with a short local tour, then Clerk, then your own room.
3
+ Chat + AI agent in a Vite app. Hosted: `pnpm setup:hosted` writes a public `pk_` when the Worker exposes `GET /public/demo-credentials`. Otherwise copy a `pk_` from the console, or sign in for a private assistant room.
4
4
 
5
5
  ## Quick start
6
6
 
@@ -14,7 +14,16 @@ pnpm setup:hosted
14
14
  pnpm dev
15
15
  ```
16
16
 
17
- Localhost opens a 3-step tour. Last step is sign in. After Clerk you come back to a simple chat. Open a second tab to try realtime. Use Open dashboard for rooms and agents.
17
+ If demo credentials are configured on the Worker, chat starts with `publishableKey` (no Clerk). Sign in later for a private assistant room.
18
+
19
+ **Hosted overlay (one file deploy for deny / rooms / extension slots):**
20
+
21
+ ```bash
22
+ # fluxy.hosted.json → PUT /admin/projects/:id/publish-config
23
+ FLUXY_WORKER_URL=https://api.fluxychat.com FLUXY_ADMIN_JWT=eyJ... FLUXY_PROJECT_ID=prj_... pnpm fluxy:deploy
24
+ ```
25
+
26
+ This is not Worker-bundled `onPublish` callbacks. Those run in `fluxy.config.ts` on self-host. Hosted extension slots are declared `kv` / `counter` on the room Durable Object (`GET /rooms/:id/extensions`).
18
27
 
19
28
  **Self-host (your Worker):**
20
29
 
@@ -37,14 +46,15 @@ If the Worker is down, `setup:local` asks for the URL. Merge `.fluxy/worker.dev.
37
46
 
38
47
  | Command | Description |
39
48
  |---------|-------------|
40
- | `pnpm setup:hosted` | Writes worker + console URLs. Auth happens in the browser via Clerk. |
49
+ | `pnpm setup:hosted` | Worker URL + public `pk_` when `/public/demo-credentials` is configured |
50
+ | `pnpm fluxy:deploy` | PUT hosted publish-config from `fluxy.hosted.json` |
41
51
  | `pnpm setup:local` / `pnpm setup:self-host` | `POST /dev/provision` on your Worker |
42
52
  | `pnpm doctor` | Health check |
43
53
  | `pnpm dev` | Start Vite |
44
54
 
45
55
  ## Environment
46
56
 
47
- `.env` from `pnpm setup:hosted` only needs worker and console URLs. Member JWT, room, and agent come from Clerk after sign in.
57
+ `.env` from `pnpm setup:hosted` includes worker URL and, when configured, `VITE_FLUXYCHAT_PUBLISHABLE_KEY`. Clerk still creates a private assistant room.
48
58
 
49
59
  Dashboard: [fluxychat.com/dashboard](https://fluxychat.com/dashboard)
50
60
 
@@ -0,0 +1,15 @@
1
+ {
2
+ "denySubstrings": [],
3
+ "guestCanPublish": true,
4
+ "iotAutoAgentId": null,
5
+ "rooms": {
6
+ "support-*": {
7
+ "anonymous": false,
8
+ "guestCanPublish": false
9
+ },
10
+ "room-*": {
11
+ "anonymous": true,
12
+ "extensions": [{ "id": "state", "kind": "kv" }]
13
+ }
14
+ }
15
+ }
@@ -8,6 +8,7 @@
8
8
  "setup:hosted": "node scripts/fluxy-setup.mjs --mode hosted",
9
9
  "setup:local": "node scripts/fluxy-setup.mjs --mode local",
10
10
  "setup:self-host": "node scripts/fluxy-setup.mjs --mode self-host",
11
+ "fluxy:deploy": "node scripts/fluxy-deploy.mjs",
11
12
  "doctor": "node scripts/fluxy-doctor.mjs",
12
13
  "dev": "node scripts/fluxy-dev.mjs",
13
14
  "dev:app": "vite",
@@ -16,8 +17,8 @@
16
17
  "postinstall": "node scripts/hoist-fluxy-sdk.mjs"
17
18
  },
18
19
  "dependencies": {
19
- "@fluxy-chat/react": "^0.1.4",
20
- "@fluxy-chat/sdk": "^0.6.5",
20
+ "@fluxy-chat/react": "^0.1.6",
21
+ "@fluxy-chat/sdk": "^0.6.8",
21
22
  "@fluxy-chat/ui": "^0.1.4",
22
23
  "class-variance-authority": "^0.7.1",
23
24
  "clsx": "^2.1.1",
@@ -40,7 +41,7 @@
40
41
  },
41
42
  "pnpm": {
42
43
  "overrides": {
43
- "@fluxy-chat/sdk": "^0.6.5"
44
+ "@fluxy-chat/sdk": "^0.6.8"
44
45
  }
45
46
  }
46
47
  }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hosted overlay: PUT /admin/projects/:id/publish-config
4
+ * Serializes one fluxy.hosted.json (deny, guest publish, iotAutoAgentId, rooms
5
+ * templates + extension slots). Does not upload Worker callbacks.
6
+ *
7
+ * FLUXY_WORKER_URL=https://api.fluxychat.com \
8
+ * FLUXY_ADMIN_JWT=eyJ... \
9
+ * FLUXY_PROJECT_ID=prj_... \
10
+ * pnpm fluxy:deploy
11
+ */
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
+ const configPath = join(root, "fluxy.hosted.json");
18
+
19
+ const workerUrl = (process.env.FLUXY_WORKER_URL || process.env.VITE_FLUXYCHAT_WORKER_URL || "").replace(/\/$/, "");
20
+ const adminJwt = process.env.FLUXY_ADMIN_JWT || "";
21
+ const projectId = process.env.FLUXY_PROJECT_ID || process.env.VITE_FLUXYCHAT_PROJECT_ID || "";
22
+
23
+ function fail(msg) {
24
+ console.error(`fluxy deploy: ${msg}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!workerUrl) fail("set FLUXY_WORKER_URL");
29
+ if (!adminJwt) fail("set FLUXY_ADMIN_JWT (owner/admin JWT, not pk_)");
30
+ if (!projectId) fail("set FLUXY_PROJECT_ID");
31
+ if (!existsSync(configPath)) fail(`missing ${configPath}`);
32
+
33
+ const hosted = JSON.parse(readFileSync(configPath, "utf8"));
34
+ const body = {
35
+ denySubstrings: Array.isArray(hosted.denySubstrings) ? hosted.denySubstrings : [],
36
+ guestCanPublish: hosted.guestCanPublish !== false,
37
+ iotAutoAgentId: hosted.iotAutoAgentId ?? null,
38
+ rooms: hosted.rooms && typeof hosted.rooms === "object" ? hosted.rooms : {},
39
+ };
40
+
41
+ const url = `${workerUrl}/admin/projects/${encodeURIComponent(projectId)}/publish-config`;
42
+ const res = await fetch(url, {
43
+ method: "PUT",
44
+ headers: {
45
+ Authorization: `Bearer ${adminJwt}`,
46
+ "Content-Type": "application/json",
47
+ },
48
+ body: JSON.stringify(body),
49
+ });
50
+ const text = await res.text();
51
+ if (!res.ok) fail(`${res.status} ${text}`);
52
+ console.log("publish-config updated");
53
+ console.log(text);
@@ -194,6 +194,7 @@ function writeEnv(vars) {
194
194
  const lines = [
195
195
  "# Auto-generated by pnpm setup — do not commit secrets",
196
196
  `VITE_FLUXYCHAT_WORKER_URL=${vars.workerUrl}`,
197
+ `VITE_FLUXYCHAT_PUBLISHABLE_KEY=${vars.publishableKey || ""}`,
197
198
  `VITE_FLUXYCHAT_MEMBER_JWT=${vars.token}`,
198
199
  `VITE_FLUXYCHAT_ROOM_ID=${vars.roomId}`,
199
200
  `VITE_FLUXYCHAT_AGENT_ID=${vars.agentId}`,
@@ -218,13 +219,30 @@ async function setupHosted() {
218
219
  process.env.FLUXY_WORKER_URL ||
219
220
  HOSTED_WORKER_DEFAULT;
220
221
  const consoleUrl = process.env.FLUXY_CONSOLE_URL || HOSTED_CONSOLE_DEFAULT;
222
+ const fromEnvPk = (process.env.FLUXY_PUBLISHABLE_KEY || process.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY || "").trim();
221
223
 
222
224
  console.log(dim(` mode: hosted · worker: ${workerUrl}`));
223
225
 
226
+ let publishableKey = fromEnvPk.startsWith("pk_") ? fromEnvPk : "";
227
+ let roomId = process.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "";
228
+ if (!publishableKey) {
229
+ try {
230
+ const demo = await getJson(workerUrl, "/public/demo-credentials");
231
+ if (demo?.publishableKey?.startsWith("pk_")) {
232
+ publishableKey = demo.publishableKey;
233
+ roomId = demo.roomId || roomId || "general";
234
+ ok("wrote public demo pk_ (GET /public/demo-credentials)");
235
+ }
236
+ } catch {
237
+ /* demo not configured on this Worker */
238
+ }
239
+ }
240
+
224
241
  writeEnv({
225
242
  workerUrl,
243
+ publishableKey,
226
244
  token: "",
227
- roomId: "",
245
+ roomId: roomId || (publishableKey ? "general" : ""),
228
246
  agentId: "",
229
247
  agentHandle: "@assistant",
230
248
  projectId: "",
@@ -236,17 +254,22 @@ async function setupHosted() {
236
254
  mode: "hosted",
237
255
  workerUrl,
238
256
  projectId: null,
239
- roomId: null,
257
+ roomId: roomId || null,
240
258
  agentId: null,
241
259
  agentHandle: "@assistant",
242
260
  setupAt: new Date().toISOString(),
243
- auth: "clerk",
261
+ auth: publishableKey ? "publishableKey" : "clerk",
244
262
  });
245
263
 
246
- ok("wrote hosted endpoints (sign in with Clerk on pnpm dev)");
247
- console.log(dim(" Run: pnpm dev"));
248
- console.log(dim(" Sign in with Clerk. We create your project and assistant room."));
249
- console.log(dim(` Console: ${consoleUrl}/onboarding`));
264
+ if (publishableKey) {
265
+ ok("public room is ready. pnpm dev — no Clerk required");
266
+ console.log(dim(" Sign in later for a private assistant room: Open dashboard"));
267
+ } else {
268
+ ok("wrote hosted endpoints (copy a pk_ from the console, or sign in on pnpm dev)");
269
+ console.log(dim(" Run: pnpm dev"));
270
+ console.log(dim(` Console: ${consoleUrl}/onboarding`));
271
+ console.log(dim(" Hosted ops: set PUBLIC_DEMO_PUBLISHABLE_KEY on the Worker for a no-account path"));
272
+ }
250
273
  }
251
274
 
252
275
  async function setupLocal() {
@@ -323,6 +346,7 @@ async function setupLocal() {
323
346
 
324
347
  writeEnv({
325
348
  workerUrl,
349
+ publishableKey: provision.publishableKey || "",
326
350
  token: auth.token,
327
351
  roomId,
328
352
  agentId: agent.id,
@@ -4,6 +4,7 @@ import { ChatWindow } from "@fluxy-chat/ui";
4
4
  import { loadCliSession, type CliSession } from "./session";
5
5
 
6
6
  const envWorkerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim() ?? "";
7
+ const envPublishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim() ?? "";
7
8
  const envJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim() ?? "";
8
9
  const envRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() ?? "";
9
10
  const envAgentId = import.meta.env.VITE_FLUXYCHAT_AGENT_ID?.trim() ?? "";
@@ -31,6 +32,18 @@ function dashboardHref(): string {
31
32
  }
32
33
 
33
34
  function sessionFromEnv(): CliSession | null {
35
+ if (envWorkerUrl && envPublishableKey.startsWith("pk_") && envRoomId) {
36
+ return {
37
+ workerUrl: envWorkerUrl,
38
+ publishableKey: envPublishableKey,
39
+ memberJwt: envJwt || undefined,
40
+ roomId: envRoomId,
41
+ agentId: envAgentId,
42
+ agentHandle: envAgentHandle,
43
+ projectId: envProjectId,
44
+ userId: envUserId || "guest",
45
+ };
46
+ }
34
47
  if (!envWorkerUrl || !envJwt || !envRoomId) return null;
35
48
  return {
36
49
  workerUrl: envWorkerUrl,
@@ -226,6 +239,7 @@ export function App() {
226
239
  <div className="page">
227
240
  <FluxyRealtimeProvider
228
241
  workerUrl={session.workerUrl}
242
+ publishableKey={session.memberJwt ? undefined : session.publishableKey}
229
243
  authTokenProvider={session.memberJwt}
230
244
  userId={session.userId}
231
245
  >
@@ -2,7 +2,8 @@ const STORAGE_KEY = "fluxy-cli-session";
2
2
 
3
3
  export interface CliSession {
4
4
  workerUrl: string;
5
- memberJwt: string;
5
+ memberJwt?: string;
6
+ publishableKey?: string;
6
7
  roomId: string;
7
8
  agentId: string;
8
9
  agentHandle: string;
@@ -15,7 +16,11 @@ export interface CliSession {
15
16
  function isCliSession(value: unknown): value is CliSession {
16
17
  if (!value || typeof value !== "object") return false;
17
18
  const v = value as Record<string, unknown>;
18
- return typeof v.memberJwt === "string" && v.memberJwt.length > 0 && typeof v.workerUrl === "string";
19
+ if (typeof v.workerUrl !== "string" || v.workerUrl.length === 0) return false;
20
+ if (typeof v.roomId !== "string" || v.roomId.length === 0) return false;
21
+ const hasMember = typeof v.memberJwt === "string" && v.memberJwt.length > 0;
22
+ const hasPk = typeof v.publishableKey === "string" && v.publishableKey.startsWith("pk_");
23
+ return hasMember || hasPk;
19
24
  }
20
25
 
21
26
  export function readCliSessionFromHash(): CliSession | null {
@@ -2,7 +2,8 @@
2
2
 
3
3
  interface ImportMetaEnv {
4
4
  readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
- readonly VITE_FLUXYCHAT_MEMBER_JWT: string;
5
+ readonly VITE_FLUXYCHAT_PUBLISHABLE_KEY?: string;
6
+ readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
7
  readonly VITE_FLUXYCHAT_ROOM_ID: string;
7
8
  readonly VITE_FLUXYCHAT_AGENT_ID: string;
8
9
  readonly VITE_FLUXYCHAT_AGENT_HANDLE: string;
@@ -1,9 +1,12 @@
1
1
  # FluxyChat worker URL (hosted or self-hosted)
2
- VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
2
+ VITE_FLUXYCHAT_WORKER_URL=https://api.fluxychat.com
3
3
 
4
- # Option A — member JWT (~2 min via onboarding)
5
- VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
6
- VITE_FLUXYCHAT_ROOM_ID=demo
4
+ # Option A — public room (pin sdk 0.6.7+)
5
+ VITE_FLUXYCHAT_PUBLISHABLE_KEY=pk_...
6
+ VITE_FLUXYCHAT_ROOM_ID=general
7
7
 
8
- # Option B — public guest room (~60s, no signup)
8
+ # Option B — member JWT
9
+ # VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
10
+
11
+ # Option C — guest session without pk_
9
12
  # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -9,8 +9,8 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@fluxy-chat/react": "^0.1.4",
13
- "@fluxy-chat/sdk": "^0.6.5",
12
+ "@fluxy-chat/react": "^0.1.6",
13
+ "@fluxy-chat/sdk": "^0.6.7",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0",
16
16
  "zustand": "^5.0.0"
@@ -3,6 +3,7 @@ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
3
  import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
4
4
 
5
5
  const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
6
+ const publishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim();
6
7
  const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
7
8
  const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
8
9
  const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
@@ -149,6 +150,23 @@ function hashCode(value: string): number {
149
150
  }
150
151
 
151
152
  export function App() {
153
+ const pkRoomId = publicRoomId || configuredRoomId;
154
+ if (workerUrl && publishableKey) {
155
+ return (
156
+ <main className="shell">
157
+ <h1>Live cursors</h1>
158
+ <p className="mode-badge">publishableKey · two tabs, same room</p>
159
+ <FluxyRealtimeProvider workerUrl={workerUrl} publishableKey={publishableKey}>
160
+ <CursorCanvas roomId={pkRoomId} selfUserId="guest" />
161
+ </FluxyRealtimeProvider>
162
+ </main>
163
+ );
164
+ }
165
+
166
+ return <MemberOrGuestCursors />;
167
+ }
168
+
169
+ function MemberOrGuestCursors() {
152
170
  const { session, loading, error } = useFluxySession();
153
171
  const [roomId, setRoomId] = useState(configuredRoomId);
154
172
 
@@ -166,8 +184,7 @@ export function App() {
166
184
  <code>VITE_FLUXYCHAT_WORKER_URL</code>.
167
185
  </p>
168
186
  <p>
169
- Then either <code>VITE_FLUXYCHAT_MEMBER_JWT</code> or{" "}
170
- <code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code>.
187
+ Then <code>VITE_FLUXYCHAT_PUBLISHABLE_KEY</code>, a member JWT, or a public room id.
171
188
  </p>
172
189
  </main>
173
190
  );
@@ -2,6 +2,7 @@
2
2
 
3
3
  interface ImportMetaEnv {
4
4
  readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_PUBLISHABLE_KEY?: string;
5
6
  readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
7
  readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
8
  readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@fluxy-chat/ui-kit": "^0.1.3",
12
+ "@fluxy-chat/ui-kit": "^0.1.4",
13
13
  "@fluxy-chat/ui": "^0.1.4",
14
14
  "@fluxy-chat/react": "^0.1.4",
15
15
  "@fluxy-chat/sdk": "^0.6.5",
@@ -0,0 +1,13 @@
1
+ # FluxyChat worker URL (hosted or self-hosted)
2
+ VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
3
+
4
+ # Option A — public room, no member JWT (pin sdk 0.6.7+)
5
+ VITE_FLUXYCHAT_PUBLISHABLE_KEY=pk_...
6
+ VITE_FLUXYCHAT_ROOM_ID=your-public-room-id
7
+
8
+ # Option B — member JWT
9
+ # VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
10
+ # VITE_FLUXYCHAT_ROOM_ID=demo
11
+
12
+ # Option C — public guest session without pk_
13
+ # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -0,0 +1,13 @@
1
+ # FluxyChat polls
2
+
3
+ First-class room polls: `createPoll` / `votePoll` on the chat timeline.
4
+
5
+ Anonymous ballots (voter ids omitted from results) are a separate HTTP path: `POST /polls` with `isAnonymous: true`, then `POST /polls/:id/close`. That is not `createPoll`.
6
+
7
+ ```bash
8
+ npx @fluxy-chat/create-fluxy-chat@latest my-polls --example polls
9
+ cp .env.example .env
10
+ npm run dev
11
+ ```
12
+
13
+ Fastest path: `VITE_FLUXYCHAT_WORKER_URL` + `VITE_FLUXYCHAT_PUBLISHABLE_KEY` (`pk_`) + a public room id. Pin `@fluxy-chat/sdk@0.6.7` and `@fluxy-chat/react@0.1.6` once those versions are on npm.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FluxyChat polls</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "fluxy-polls",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fluxy-chat/react": "^0.1.6",
13
+ "@fluxy-chat/sdk": "^0.6.7",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "@vitejs/plugin-react": "^4.3.0",
21
+ "typescript": "^5.6.0",
22
+ "vite": "^6.0.0"
23
+ }
24
+ }
@@ -0,0 +1,135 @@
1
+ import { useState } from "react";
2
+ import {
3
+ FluxyRealtimeProvider,
4
+ useChat,
5
+ useFluxyChat,
6
+ } from "@fluxy-chat/react";
7
+ import { useFluxySession, workerUrl } from "./session";
8
+
9
+ const publishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim();
10
+ const pkRoomId =
11
+ import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() ||
12
+ import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim() ||
13
+ "demo";
14
+
15
+ function PollRoom({ roomId }: { roomId: string }) {
16
+ const { client } = useFluxyChat();
17
+ const { messages } = useChat({ roomId });
18
+ const [question, setQuestion] = useState("Ship Friday?");
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+ async function createTimelinePoll() {
22
+ setError(null);
23
+ try {
24
+ await client.createPoll(roomId, {
25
+ question,
26
+ options: ["Yes", "No", "Abstain"],
27
+ });
28
+ } catch (err) {
29
+ setError(err instanceof Error ? err.message : "createPoll failed");
30
+ }
31
+ }
32
+
33
+ return (
34
+ <section className="panel">
35
+ <header className="chat-header">
36
+ <strong>Polls · {roomId}</strong>
37
+ <span className="status">createPoll / votePoll on the timeline</span>
38
+ </header>
39
+ <div className="composer">
40
+ <input
41
+ value={question}
42
+ onChange={(event) => setQuestion(event.target.value)}
43
+ placeholder="Question"
44
+ />
45
+ <button type="button" className="primary" onClick={() => void createTimelinePoll()}>
46
+ Create poll
47
+ </button>
48
+ </div>
49
+ {error ? <p className="error" style={{ padding: "0 1rem" }}>{error}</p> : null}
50
+ <ul className="log">
51
+ {messages.map((message) => (
52
+ <li key={message.id} className="poll-card">
53
+ {message.poll ? (
54
+ <>
55
+ <p>
56
+ <strong>{message.poll.question}</strong>
57
+ {message.poll.closed ? " · closed" : ""}
58
+ </p>
59
+ <div className="poll-options">
60
+ {message.poll.options.map((option) => (
61
+ <button
62
+ key={option.index}
63
+ type="button"
64
+ disabled={message.poll?.closed}
65
+ onClick={() => void client.votePoll(message.id, option.index)}
66
+ >
67
+ {option.text} · {option.votes}
68
+ </button>
69
+ ))}
70
+ </div>
71
+ <p className="hint" style={{ marginTop: 8 }}>
72
+ {message.poll.totalVoters} voters
73
+ {message.poll.userVote != null ? ` · you voted ${message.poll.userVote}` : ""}
74
+ </p>
75
+ </>
76
+ ) : (
77
+ <p>
78
+ <strong>{message.userId}</strong> {message.content}
79
+ </p>
80
+ )}
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ </section>
85
+ );
86
+ }
87
+
88
+ export function App() {
89
+ if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
90
+
91
+ if (publishableKey) {
92
+ return (
93
+ <div className="shell">
94
+ <p className="mode-badge">publishableKey · public room {pkRoomId}</p>
95
+ <p className="hint">
96
+ Timeline polls use createPoll. Anonymous ballots (no voter ids in results) are POST /polls with isAnonymous: true. See README.
97
+ </p>
98
+ <FluxyRealtimeProvider workerUrl={workerUrl} publishableKey={publishableKey}>
99
+ <PollRoom roomId={pkRoomId} />
100
+ </FluxyRealtimeProvider>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ return <MemberOrGuestApp />;
106
+ }
107
+
108
+ function MemberOrGuestApp() {
109
+ const { session, loading, error } = useFluxySession();
110
+ if (loading) return <div className="shell">Starting…</div>;
111
+ if (error) return <div className="shell error">{error}</div>;
112
+ if (!session) {
113
+ return (
114
+ <div className="shell">
115
+ Set <code>VITE_FLUXYCHAT_PUBLISHABLE_KEY</code>, a member JWT, or a public room id.
116
+ </div>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <div className="shell">
122
+ <p className="mode-badge">{session.mode} · {session.roomId}</p>
123
+ <p className="hint">
124
+ Timeline polls use createPoll. Anonymous ballots: POST /polls with isAnonymous: true. See README.
125
+ </p>
126
+ <FluxyRealtimeProvider
127
+ workerUrl={session.workerUrl}
128
+ authTokenProvider={session.token}
129
+ userId={session.userId}
130
+ >
131
+ <PollRoom roomId={session.roomId} />
132
+ </FluxyRealtimeProvider>
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,85 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+ * { box-sizing: border-box; }
8
+ body { margin: 0; }
9
+ .shell { max-width: 720px; margin: 0 auto; padding: 1.5rem; }
10
+ .panel {
11
+ border: 1px solid #e2e8f0;
12
+ border-radius: 12px;
13
+ overflow: hidden;
14
+ background: #fff;
15
+ }
16
+ .chat-header {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ padding: 0.75rem 1rem;
20
+ background: #0f172a;
21
+ color: #fff;
22
+ }
23
+ .status { font-size: 0.75rem; opacity: 0.9; }
24
+ .roster {
25
+ margin: 0;
26
+ padding: 0.5rem 1rem;
27
+ list-style: none;
28
+ font-size: 0.8rem;
29
+ color: #64748b;
30
+ border-bottom: 1px solid #e2e8f0;
31
+ }
32
+ .log { min-height: 240px; max-height: 50vh; overflow: auto; padding: 1rem; }
33
+ .composer { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #e2e8f0; }
34
+ .composer input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
35
+ .composer button, button.primary {
36
+ padding: 0.5rem 0.75rem;
37
+ border: 0;
38
+ border-radius: 8px;
39
+ background: #2563eb;
40
+ color: #fff;
41
+ cursor: pointer;
42
+ }
43
+ .mode-badge { font-size: 0.75rem; color: #64748b; margin-bottom: 0.75rem; }
44
+ .error { color: #b91c1c; }
45
+ code { font-size: 0.85em; }
46
+ .hint { font-size: 0.8rem; color: #475569; white-space: pre-wrap; background: #f1f5f9; padding: 0.75rem; border-radius: 8px; }
47
+ .readings { margin: 0; padding: 0.75rem 1rem; list-style: none; font-size: 0.875rem; }
48
+ .canvas {
49
+ position: relative;
50
+ height: min(70vh, 480px);
51
+ background: #f8fafc;
52
+ touch-action: none;
53
+ }
54
+ .dot {
55
+ position: absolute;
56
+ width: 10px;
57
+ height: 10px;
58
+ border-radius: 999px;
59
+ transform: translate(-50%, -50%);
60
+ pointer-events: none;
61
+ }
62
+ .peer-cursor {
63
+ position: absolute;
64
+ pointer-events: none;
65
+ z-index: 2;
66
+ }
67
+ .peer-cursor span {
68
+ display: block;
69
+ margin-top: 2px;
70
+ padding: 1px 6px;
71
+ border-radius: 4px;
72
+ background: currentColor;
73
+ color: #fff;
74
+ font-size: 11px;
75
+ }
76
+ .poll-card { margin: 0 0 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid #e2e8f0; }
77
+ .poll-options { display: flex; flex-wrap: wrap; gap: 0.5rem; }
78
+ .poll-options button {
79
+ padding: 0.4rem 0.7rem;
80
+ border: 1px solid #cbd5e1;
81
+ border-radius: 8px;
82
+ background: #f8fafc;
83
+ cursor: pointer;
84
+ }
85
+ .poll-options button:disabled { opacity: 0.6; cursor: default; }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,73 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+
4
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
5
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
6
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
7
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
8
+
9
+ export interface FluxySession {
10
+ workerUrl: string;
11
+ token: string;
12
+ userId: string;
13
+ roomId: string;
14
+ mode: "member" | "guest";
15
+ }
16
+
17
+ export function useFluxySession(): {
18
+ session: FluxySession | null;
19
+ loading: boolean;
20
+ error: string | null;
21
+ } {
22
+ const [session, setSession] = useState<FluxySession | null>(null);
23
+ const [loading, setLoading] = useState(Boolean(workerUrl));
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!workerUrl) {
28
+ setLoading(false);
29
+ return;
30
+ }
31
+ if (memberJwt) {
32
+ setSession({
33
+ workerUrl,
34
+ token: memberJwt,
35
+ userId: "demo-user",
36
+ roomId: configuredRoomId,
37
+ mode: "member",
38
+ });
39
+ setLoading(false);
40
+ return;
41
+ }
42
+ if (publicRoomId) {
43
+ let cancelled = false;
44
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
45
+ displayName: "Guest",
46
+ })
47
+ .then((guest) => {
48
+ if (cancelled) return;
49
+ setSession({
50
+ workerUrl,
51
+ token: guest.token,
52
+ userId: guest.userId,
53
+ roomId: guest.roomId,
54
+ mode: "guest",
55
+ });
56
+ })
57
+ .catch((err) => {
58
+ if (!cancelled) setError(err instanceof Error ? err.message : "Guest session failed");
59
+ })
60
+ .finally(() => {
61
+ if (!cancelled) setLoading(false);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }
67
+ setLoading(false);
68
+ }, []);
69
+
70
+ return { session, loading, error };
71
+ }
72
+
73
+ export { workerUrl };
@@ -0,0 +1,13 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_PUBLISHABLE_KEY?: string;
6
+ readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
7
+ readonly VITE_FLUXYCHAT_ROOM_ID?: string;
8
+ readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["src"]
21
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173 },
7
+ });
@@ -1,9 +1,13 @@
1
1
  # FluxyChat worker URL (hosted or self-hosted)
2
- VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
2
+ VITE_FLUXYCHAT_WORKER_URL=https://api.fluxychat.com
3
3
 
4
- # Option A — member JWT (~2 min via onboarding)
5
- VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
6
- VITE_FLUXYCHAT_ROOM_ID=demo
4
+ # Option A — public room, no JWT (pin @fluxy-chat/sdk@0.6.7+)
5
+ VITE_FLUXYCHAT_PUBLISHABLE_KEY=pk_...
6
+ VITE_FLUXYCHAT_ROOM_ID=general
7
7
 
8
- # Option B — public guest room (~60s, no signup)
8
+ # Option B — member JWT
9
+ # VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
10
+ # VITE_FLUXYCHAT_ROOM_ID=demo
11
+
12
+ # Option C — guest session without pk_
9
13
  # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -1,43 +1,18 @@
1
1
  # FluxyChat React starter
2
2
 
3
- Minimal Vite + React app with `useChat`. First message in about 60 seconds via the public guest room.
4
-
5
- ## Quick start (guest, fastest)
6
-
7
- ```bash
8
- cp .env.example .env
9
- # Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLIC_ROOM_ID (public room from console)
10
- npm install
11
- npm run dev
12
- ```
13
-
14
- Open http://localhost:5173. A guest JWT is minted automatically via `joinPublicRoomAsGuest`.
15
-
16
- ## Quick start (member JWT)
3
+ `FluxyRealtimeProvider` + `publishableKey` + `useChat`. No member JWT mint.
17
4
 
18
5
  ```bash
19
6
  cp .env.example .env
20
- # Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_MEMBER_JWT + VITE_FLUXYCHAT_ROOM_ID
7
+ # Set VITE_FLUXYCHAT_WORKER_URL + VITE_FLUXYCHAT_PUBLISHABLE_KEY + room id
21
8
  npm install
22
9
  npm run dev
23
10
  ```
24
11
 
25
- ## Get credentials
26
-
27
- 1. **Guest:** create a **public** room in console, copy room ID, set `VITE_FLUXYCHAT_PUBLIC_ROOM_ID`
28
- 2. **Member:** [fluxychat.com/onboarding](https://fluxychat.com/onboarding) for Worker URL and JWT
29
- 3. **Local monorepo:** `pnpm run first-message` from the FluxyChat repo
30
-
31
- ## Scripts
32
-
33
- | Command | Description |
34
- |---------|-------------|
35
- | `npm run dev` | Vite dev server |
36
- | `npm run build` | Production build |
37
- | `npm run preview` | Preview production build |
12
+ Pin `@fluxy-chat/sdk@0.6.7` and `@fluxy-chat/react@0.1.6` once those versions are on npm.
38
13
 
39
- ## Next steps
14
+ ## Other paths
40
15
 
41
- - Add `@fluxy-chat/ui` themes (`default`, `dark`, `minimal`, `brand`)
42
- - Use `useInbox` for unified feed
43
- - See [chat-only quickstart](https://docs.fluxychat.com/docs/getting-started/chat-only-quickstart)
16
+ - Member JWT: `VITE_FLUXYCHAT_MEMBER_JWT`
17
+ - Guest session: `VITE_FLUXYCHAT_PUBLIC_ROOM_ID` (`joinPublicRoomAsGuest`)
18
+ - Credentials: [fluxychat.com/onboarding](https://fluxychat.com/onboarding) or `GET /public/demo-credentials` on a Worker that sets `PUBLIC_DEMO_PUBLISHABLE_KEY`
@@ -9,8 +9,8 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@fluxy-chat/react": "^0.1.4",
13
- "@fluxy-chat/sdk": "^0.6.5",
12
+ "@fluxy-chat/react": "^0.1.6",
13
+ "@fluxy-chat/sdk": "^0.6.7",
14
14
  "react": "^19.0.0",
15
15
  "react-dom": "^19.0.0",
16
16
  "zustand": "^5.0.0"
@@ -3,6 +3,7 @@ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
3
  import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
4
4
 
5
5
  const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
6
+ const publishableKey = import.meta.env.VITE_FLUXYCHAT_PUBLISHABLE_KEY?.trim();
6
7
  const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
7
8
  const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
8
9
  const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
@@ -21,11 +22,11 @@ function useFluxySession(): {
21
22
  error: string | null;
22
23
  } {
23
24
  const [session, setSession] = useState<FluxySession | null>(null);
24
- const [loading, setLoading] = useState(Boolean(workerUrl));
25
+ const [loading, setLoading] = useState(Boolean(workerUrl) && !publishableKey);
25
26
  const [error, setError] = useState<string | null>(null);
26
27
 
27
28
  useEffect(() => {
28
- if (!workerUrl) {
29
+ if (!workerUrl || publishableKey) {
29
30
  setLoading(false);
30
31
  return;
31
32
  }
@@ -126,30 +127,44 @@ function ChatPanel({ roomId }: { roomId: string }) {
126
127
  }
127
128
 
128
129
  export function App() {
129
- const { session, loading, error } = useFluxySession();
130
- const [roomId, setRoomId] = useState(configuredRoomId);
131
-
132
- const activeRoomId = useMemo(() => {
133
- if (session?.mode === "guest") return session.roomId;
134
- return roomId.trim() || configuredRoomId;
135
- }, [session, roomId]);
130
+ const pkRoomId = publicRoomId || configuredRoomId;
136
131
 
137
132
  if (!workerUrl) {
138
133
  return (
139
134
  <main className="shell">
140
- <h1>FluxyChat — chat-only starter</h1>
135
+ <h1>FluxyChat</h1>
141
136
  <p>
142
137
  Copy <code>.env.example</code> to <code>.env</code> and set{" "}
143
- <code>VITE_FLUXYCHAT_WORKER_URL</code>.
144
- </p>
145
- <p>
146
- Then either <code>VITE_FLUXYCHAT_MEMBER_JWT</code> (member) or{" "}
147
- <code>VITE_FLUXYCHAT_PUBLIC_ROOM_ID</code> (guest, ~60s to first message).
138
+ <code>VITE_FLUXYCHAT_WORKER_URL</code> plus <code>VITE_FLUXYCHAT_PUBLISHABLE_KEY</code>.
148
139
  </p>
149
140
  </main>
150
141
  );
151
142
  }
152
143
 
144
+ if (publishableKey) {
145
+ return (
146
+ <main className="shell">
147
+ <h1>FluxyChat</h1>
148
+ <p className="mode-badge">publishableKey · public room {pkRoomId}</p>
149
+ <FluxyRealtimeProvider workerUrl={workerUrl} publishableKey={publishableKey}>
150
+ <ChatPanel roomId={pkRoomId} />
151
+ </FluxyRealtimeProvider>
152
+ </main>
153
+ );
154
+ }
155
+
156
+ return <MemberOrGuestApp />;
157
+ }
158
+
159
+ function MemberOrGuestApp() {
160
+ const { session, loading, error } = useFluxySession();
161
+ const [roomId, setRoomId] = useState(configuredRoomId);
162
+
163
+ const activeRoomId = useMemo(() => {
164
+ if (session?.mode === "guest") return session.roomId;
165
+ return roomId.trim() || configuredRoomId;
166
+ }, [session, roomId]);
167
+
153
168
  if (loading) {
154
169
  return (
155
170
  <main className="shell">
@@ -162,14 +177,7 @@ export function App() {
162
177
  return (
163
178
  <main className="shell">
164
179
  <h1>FluxyChat</h1>
165
- <p className="error">{error ?? "Set JWT or public room ID in .env"}</p>
166
- <p>
167
- Get credentials from{" "}
168
- <a href="https://fluxychat.com/onboarding" target="_blank" rel="noreferrer">
169
- onboarding
170
- </a>{" "}
171
- or use a public room ID for guest mode.
172
- </p>
180
+ <p className="error">{error ?? "Set pk_, JWT, or public room ID in .env"}</p>
173
181
  </main>
174
182
  );
175
183
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  interface ImportMetaEnv {
4
4
  readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_PUBLISHABLE_KEY?: string;
5
6
  readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
7
  readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
8
  readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;