@polishd/next 0.1.0 → 0.2.1-dev.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.
Files changed (95) hide show
  1. package/AGENTS.md +161 -46
  2. package/CHANGELOG.md +163 -0
  3. package/README.md +275 -49
  4. package/bin/doctor.mjs +374 -0
  5. package/bin/init.mjs +215 -188
  6. package/bin/polishd.mjs +85 -0
  7. package/bin/shared.mjs +114 -0
  8. package/dist/ai/actions.d.ts +13 -2
  9. package/dist/ai/actions.d.ts.map +1 -1
  10. package/dist/ai/actions.js +19 -1
  11. package/dist/ai/actions.js.map +1 -1
  12. package/dist/ai/design.d.ts +28 -0
  13. package/dist/ai/design.d.ts.map +1 -0
  14. package/dist/ai/design.js +245 -0
  15. package/dist/ai/design.js.map +1 -0
  16. package/dist/ai/guard.d.ts +44 -3
  17. package/dist/ai/guard.d.ts.map +1 -1
  18. package/dist/ai/guard.js +13 -2
  19. package/dist/ai/guard.js.map +1 -1
  20. package/dist/ai/providers.d.ts +2 -0
  21. package/dist/ai/providers.d.ts.map +1 -1
  22. package/dist/ai/providers.js +70 -0
  23. package/dist/ai/providers.js.map +1 -1
  24. package/dist/ai/settings.d.ts +15 -1
  25. package/dist/ai/settings.d.ts.map +1 -1
  26. package/dist/ai/settings.js +38 -1
  27. package/dist/ai/settings.js.map +1 -1
  28. package/dist/ai/types.d.ts +57 -0
  29. package/dist/ai/types.d.ts.map +1 -1
  30. package/dist/client/design-scan.d.ts +37 -0
  31. package/dist/client/design-scan.d.ts.map +1 -0
  32. package/dist/client/design-scan.js +319 -0
  33. package/dist/client/design-scan.js.map +1 -0
  34. package/dist/client/init.d.ts.map +1 -1
  35. package/dist/client/init.js +67 -2
  36. package/dist/client/init.js.map +1 -1
  37. package/dist/config.d.ts +57 -3
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +62 -3
  40. package/dist/config.js.map +1 -1
  41. package/dist/dashboard/design-review-card.d.ts +15 -0
  42. package/dist/dashboard/design-review-card.d.ts.map +1 -0
  43. package/dist/dashboard/design-review-card.js +82 -0
  44. package/dist/dashboard/design-review-card.js.map +1 -0
  45. package/dist/dashboard/design.d.ts +10 -0
  46. package/dist/dashboard/design.d.ts.map +1 -0
  47. package/dist/dashboard/design.js +83 -0
  48. package/dist/dashboard/design.js.map +1 -0
  49. package/dist/dashboard/index.d.ts +35 -5
  50. package/dist/dashboard/index.d.ts.map +1 -1
  51. package/dist/dashboard/index.js +176 -18
  52. package/dist/dashboard/index.js.map +1 -1
  53. package/dist/dashboard/journeys.d.ts.map +1 -1
  54. package/dist/dashboard/journeys.js +1 -0
  55. package/dist/dashboard/journeys.js.map +1 -1
  56. package/dist/dashboard/summary.d.ts.map +1 -1
  57. package/dist/dashboard/summary.js +59 -5
  58. package/dist/dashboard/summary.js.map +1 -1
  59. package/dist/dashboard/token-auth.d.ts +56 -0
  60. package/dist/dashboard/token-auth.d.ts.map +1 -0
  61. package/dist/dashboard/token-auth.js +158 -0
  62. package/dist/dashboard/token-auth.js.map +1 -0
  63. package/dist/dashboard/unlock.d.ts +2 -0
  64. package/dist/dashboard/unlock.d.ts.map +1 -0
  65. package/dist/dashboard/unlock.js +54 -0
  66. package/dist/dashboard/unlock.js.map +1 -0
  67. package/dist/dashboard.css +2740 -0
  68. package/dist/proxy.d.ts +53 -5
  69. package/dist/proxy.d.ts.map +1 -1
  70. package/dist/proxy.js +73 -3
  71. package/dist/proxy.js.map +1 -1
  72. package/dist/route.d.ts.map +1 -1
  73. package/dist/route.js +11 -3
  74. package/dist/route.js.map +1 -1
  75. package/dist/server/design.d.ts +93 -0
  76. package/dist/server/design.d.ts.map +1 -0
  77. package/dist/server/design.js +430 -0
  78. package/dist/server/design.js.map +1 -0
  79. package/dist/server/ingest.d.ts +33 -2
  80. package/dist/server/ingest.d.ts.map +1 -1
  81. package/dist/server/ingest.js +70 -8
  82. package/dist/server/ingest.js.map +1 -1
  83. package/dist/server/queries.d.ts +16 -0
  84. package/dist/server/queries.d.ts.map +1 -1
  85. package/dist/server/queries.js +80 -16
  86. package/dist/server/queries.js.map +1 -1
  87. package/dist/session.d.ts.map +1 -1
  88. package/dist/session.js +4 -2
  89. package/dist/session.js.map +1 -1
  90. package/dist/shared/types.d.ts +31 -1
  91. package/dist/shared/types.d.ts.map +1 -1
  92. package/dist/shared/types.js +1 -0
  93. package/dist/shared/types.js.map +1 -1
  94. package/docs/SETUP.md +308 -51
  95. package/package.json +14 -7
package/docs/SETUP.md CHANGED
@@ -15,8 +15,6 @@ instead.
15
15
  - **Node 22.5 or later.** The development store uses Node's built-in
16
16
  `node:sqlite`. On older Node, Polishd degrades to a no-op rather than
17
17
  crashing, but you won't capture anything locally.
18
- - **Tailwind CSS** (v3 or v4) if you want the dashboard styled. See
19
- [Styling](#4-styling) — this step is easy to miss.
20
18
  - **Postgres** for production. Not needed for local development.
21
19
 
22
20
  ## 1. Install
@@ -31,21 +29,49 @@ Polishd at a Postgres URL. You don't need to install it yourself.
31
29
  ## 2. Scaffold
32
30
 
33
31
  ```bash
34
- npx polishd init
32
+ npx @polishd/next init
35
33
  ```
36
34
 
37
- This detects whether your app uses `src/` or the repo root, writes four files,
38
- adds `.polishd/` to `.gitignore`, and wires Tailwind. Useful flags:
35
+ This detects whether your app uses `src/` or the repo root, detects your
36
+ installed Next major so the proxy file gets the right name, writes four files,
37
+ and adds `.polishd/` to `.gitignore`. Useful flags:
39
38
 
40
39
  | Flag | Effect |
41
40
  |---|---|
42
41
  | `--dry-run` | Print what would happen; write nothing |
43
- | `--force` | Overwrite files that already exist |
42
+ | `--force` | Regenerate files `init` itself wrote, backing each up to `.bak` |
44
43
  | `--js` | Emit JavaScript instead of TypeScript |
45
44
  | `--config` | Also write a starter `polishd.config.ts` |
46
45
 
47
- If a file already exists, `init` skips it and tells you. For an existing
48
- `middleware.ts` it prints a merge snippet rather than clobbering your code.
46
+ **`--force` will never overwrite a file `init` didn't write.** Generated files
47
+ carry a marker; anything without one is left alone whether or not you pass the
48
+ flag, so re-running is safe even if you've since hand-written a proxy. When it
49
+ finds a proxy it didn't author, it prints merge instructions instead — see
50
+ [Composing with an existing proxy](#composing-with-an-existing-proxy).
51
+
52
+ Then check the result:
53
+
54
+ ```bash
55
+ npx @polishd/next doctor
56
+ ```
57
+
58
+ ### Installing with a coding agent
59
+
60
+ Paste this to Claude Code, Cursor, Copilot, or any other coding agent:
61
+
62
+ ```text
63
+ Install @polishd/next into this Next.js app.
64
+
65
+ Follow the procedure in AGENTS.md from the package exactly — after installing,
66
+ read node_modules/@polishd/next/AGENTS.md. If the package isn't installed yet,
67
+ read it at https://github.com/muskanvaswan/polishd/blob/main/AGENTS.md
68
+
69
+ Work through its phases in order and do not skip Phase 7 (verification).
70
+ Several of its constraints fail silently at runtime rather than at build time,
71
+ so a passing build does not mean the install worked.
72
+
73
+ Finish by running `npx @polishd/next doctor` and fixing everything it reports.
74
+ ```
49
75
 
50
76
  ## 3. What the four files do
51
77
 
@@ -81,8 +107,12 @@ export const config = {
81
107
  > entire `/api` segment. The proxy's only job is setting a cookie, which page
82
108
  > navigations already trigger.
83
109
 
84
- **Already have a proxy or middleware?** Don't replace it. Thread your response
85
- through instead, and merge the matcher — keeping the `api` exclusion:
110
+ ### Composing with an existing proxy
111
+
112
+ **Already have a proxy or middleware?** Don't replace it — Next loads exactly
113
+ one such module, so Polishd has to go inside yours.
114
+
115
+ Threading the response is the easy half:
86
116
 
87
117
  ```ts
88
118
  export function proxy(request: NextRequest) {
@@ -91,6 +121,42 @@ export function proxy(request: NextRequest) {
91
121
  }
92
122
  ```
93
123
 
124
+ A redirect or rewrite you return keeps working and carries the analytics cookie.
125
+
126
+ The hard half is the matcher, and it's easy to miss. One file means one
127
+ `config.matcher`, so merging means widening it to the union of both — and
128
+ Polishd's matcher is near-total. A handler you deliberately scoped to two routes
129
+ now runs on **every request in the app**. Nothing errors. Your redirects just
130
+ start firing where they never did before.
131
+
132
+ `composePolishd` fixes that if you tell it the original scope:
133
+
134
+ ```ts
135
+ import { composePolishd } from "@polishd/next/proxy";
136
+
137
+ function myProxy(request: NextRequest) {
138
+ if (!hasSession(request)) return NextResponse.redirect(new URL("/login", request.url));
139
+ }
140
+
141
+ // Still runs only on /admin/*, even though the matcher below is near-total.
142
+ export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });
143
+
144
+ export const config = {
145
+ matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
146
+ };
147
+ ```
148
+
149
+ Read your existing `config.matcher` before replacing it and pass exactly that
150
+ value. `matcher` takes Next's matcher strings (`"/admin/:path*"`,
151
+ `"/((?!api).*)"`) or a `RegExp` for full control. Omit it only if your handler
152
+ really did run everywhere.
153
+
154
+ Widening is safe from Polishd's side: `withPolishdSession` returns early once
155
+ the cookie exists, so the extra paths cost a cookie lookup and nothing more.
156
+
157
+ The union literal still has to be written by hand — Next parses it statically,
158
+ so no helper can generate it.
159
+
94
160
  ### `src/instrumentation-client.ts` — capture
95
161
 
96
162
  Next runs this before hydration, so capture starts on the first paint.
@@ -125,6 +191,7 @@ export const POST = createPolishdRoute();
125
191
  ### `src/app/polishd/page.tsx` — the dashboard
126
192
 
127
193
  ```tsx
194
+ import "@polishd/next/dashboard.css";
128
195
  import { createPolishdPage } from "@polishd/next/dashboard";
129
196
 
130
197
  export const runtime = "nodejs";
@@ -133,44 +200,144 @@ export const dynamic = "force-dynamic";
133
200
  export default createPolishdPage();
134
201
  ```
135
202
 
136
- Same inline-config rule as the route. **This is unguarded** — see
203
+ Same inline-config rule as the route. In production this refuses to render
204
+ until you decide how it is protected — see
137
205
  [Protecting the dashboard](#5-protecting-the-dashboard).
138
206
 
139
- ## 4. Styling
207
+ ### The dashboard's own shell
140
208
 
141
- The dashboard is built entirely from Tailwind utility classes and ships no
142
- stylesheet of its own. Tailwind has to be told to scan the package — it will not
143
- find it by default. Miss this and `/polishd` loads fine but renders as unstyled
144
- HTML.
209
+ The dashboard is a route in *your* app, so it inherits your `<body>`. That
210
+ matters when your body doesn't scroll — the default shape of every dashboard
211
+ shell, chat UI, editor, and map app, all of which set `overflow: hidden` on
212
+ body and scroll their own inner regions.
145
213
 
146
- **Tailwind v4** (no config file; your CSS starts with `@import "tailwindcss"`):
214
+ By default Polishd renders into a self-contained shell (`position: fixed`, its
215
+ own background and scroll container) so host body styles can't strand content
216
+ below the fold. Opt out to inherit your page instead:
147
217
 
148
- ```css
149
- @import "tailwindcss";
150
- @source "../../node_modules/@polishd/next/dist";
218
+ ```tsx
219
+ export default createPolishdPage({ shell: false });
151
220
  ```
152
221
 
153
- The path is relative to the stylesheet. From `src/app/globals.css` that's
154
- `../../`. v4's automatic content detection deliberately skips `node_modules`, so
155
- this line is not optional.
222
+ **If the dashboard still looks cut off**, an ancestor is establishing a
223
+ containing block — `transform`, `filter`, `contain`, or `will-change` on
224
+ anything above the route defeats `position: fixed`. Rare this close to the
225
+ root. The complete escape is a second root layout via a route group, which
226
+ gives the dashboard its own `<html>`/`<body>` and total isolation from your
227
+ global CSS:
156
228
 
157
- **Tailwind v3** — add the glob to `content` in `tailwind.config.ts`:
229
+ ```
230
+ app/
231
+ (site)/layout.tsx ← your existing root layout, moved into a group
232
+ (polishd)/
233
+ layout.tsx ← renders its own <html> and <body>
234
+ polishd/page.tsx
235
+ ```
158
236
 
159
- ```ts
160
- content: [
161
- "./src/**/*.{ts,tsx}",
162
- "./node_modules/@polishd/next/dist/**/*.js",
163
- ],
237
+ This is the nuclear option, and it has real costs: you must move your own root
238
+ layout into a group, and navigation between the two roots forces a full page
239
+ reload. Reach for it only for a host with genuinely hostile global CSS.
240
+
241
+ ## 4. Styling
242
+
243
+ The dashboard ships its own stylesheet, compiled at publish time. One import,
244
+ which `init` writes for you:
245
+
246
+ ```tsx
247
+ // app/polishd/page.tsx
248
+ import "@polishd/next/dashboard.css";
164
249
  ```
165
250
 
166
- **Not using Tailwind?** Everything works, but the dashboard will be unstyled.
167
- The colors are self-contained (no dependency on your theme), so adding Tailwind
168
- purely for the dashboard is a reasonable option if you want it to look right.
251
+ **Tailwind is not required in your app.** The dashboard renders identically
252
+ whether or not you use it, and whichever version you use.
253
+
254
+ The stylesheet is built so it cannot interfere with the app it lands in, and so
255
+ the app cannot interfere with it:
256
+
257
+ - **No global preflight.** Tailwind's reset is the part that restyles every
258
+ element on a page, and it is deliberately left out. A scoped equivalent covers
259
+ the dashboard's own subtree instead, so your headings, lists, buttons and form
260
+ controls are untouched.
261
+ - **Every rule is scoped to `.polishd-root`**, checked at build time — no
262
+ selector in the file can match your markup.
263
+ - **The scope is written twice** (`.polishd-root.polishd-root`) so the dashboard
264
+ wins specificity ties. Tailwind class names are a global namespace: a host with
265
+ its own `.flex` or `.hidden` would otherwise style the dashboard's markup, and
266
+ which one won would depend on stylesheet order.
267
+ - **No cascade layers.** Unlayered CSS beats layered CSS outright regardless of
268
+ specificity, so a plain `h1 { color: … }` in your app would override the
269
+ dashboard's typography if its utilities stayed in `@layer utilities`.
270
+ - **An explicit font stack**, rather than inheriting yours — with no global
271
+ preflight, inheriting would land on the browser default and render the whole
272
+ dashboard in serif.
273
+
274
+ The one thing it cannot win is a host `!important` rule targeting the same
275
+ property. Nothing short of `!important` on our side would, and a package
276
+ shouting over its host is worse than a rare collision. If you hit one, the
277
+ route-group layout in [The dashboard's own shell](#the-dashboards-own-shell)
278
+ isolates the dashboard completely.
279
+
280
+ ### Upgrading from 0.1.x
281
+
282
+ **No change is required.** Your existing `@source
283
+ "…/node_modules/@polishd/next/dist"` (or the `content` glob in
284
+ `tailwind.config.*`) still compiles the dashboard's utilities exactly as it did
285
+ in 0.1.x, so the dashboard stays styled after upgrading. Both paths were
286
+ rendered side by side and compared across 491 elements with no visible
287
+ difference.
288
+
289
+ Moving to the shipped stylesheet is recommended but optional, and **the order
290
+ matters**:
291
+
292
+ 1. Add `import "@polishd/next/dashboard.css";` to your dashboard page.
293
+ 2. Check the dashboard still looks right.
294
+ 3. *Then* remove the `@source` line or `content` glob.
295
+
296
+ Removing the old wiring before adding the import is the one sequence that
297
+ leaves the dashboard unstyled, which is why the steps are in this order.
298
+
299
+ The payoff is that Tailwind stops being a requirement for your app at all, and
300
+ your build stops compiling utilities that only the dashboard uses.
301
+ `npx @polishd/next doctor` reports which of the two setups it finds and will not
302
+ report a working 0.1.x install as broken.
169
303
 
170
304
  ## 5. Protecting the dashboard
171
305
 
172
- By default `/polishd` is **public**, with a development-only console warning.
173
- Before deploying, gate it:
306
+ The dashboard is not a read-only report: its settings change the configured
307
+ model and **API base URL**, so whoever can reach it can point your requests at
308
+ their own endpoint and spend your key.
309
+
310
+ Locally it is open, with one console warning. **In production it refuses to
311
+ render** until you've chosen one of three options.
312
+
313
+ ### Option 1 — the built-in token (no auth of your own required)
314
+
315
+ ```bash
316
+ POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)
317
+ ```
318
+
319
+ That's the whole setup. The locked screen shows a password field, and the token
320
+ is exchanged for an httpOnly, path-scoped cookie by a server action — so it
321
+ never appears in a URL, in browser history, in server logs, or in a `Referer`
322
+ header. The cookie carries an HMAC of the token rather than the token itself,
323
+ comparison is constant-time, expiry is enforced server-side, and the unlock form
324
+ is rate-limited per IP.
325
+
326
+ If your dashboard isn't at `/polishd`, say so, since that's what scopes the
327
+ cookie:
328
+
329
+ ```tsx
330
+ import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";
331
+
332
+ export default createPolishdPage({
333
+ authenticate: polishdTokenAuth({
334
+ basePath: "/admin/polishd",
335
+ maxAgeSeconds: 60 * 60 * 24, // default is one week
336
+ }),
337
+ });
338
+ ```
339
+
340
+ ### Option 2 — your app's own auth
174
341
 
175
342
  ```tsx
176
343
  import { createPolishdPage } from "@polishd/next/dashboard";
@@ -186,25 +353,46 @@ export default createPolishdPage({
186
353
  });
187
354
  ```
188
355
 
189
- `authenticate` returns a boolean or a promise of one. When it returns false the
190
- `unauthorized` node renders, or a minimal built-in screen if you didn't pass
191
- one. Polishd ships no auth code, so it never pulls an auth library into your
192
- dependency tree.
356
+ `authenticate` returns a boolean or a promise of one, and receives the request's
357
+ `cookies` and `headers`:
193
358
 
194
- **This also guards the AI server actions.** That matters more than it might
195
- sound: a Next server action is an independently addressable POST endpoint whose
196
- id ships inside your public client bundle. Gating only the page render would
197
- leave the actions callable by anyone — able to change the configured model and
198
- API base URL, and spend your model tokens. The callback re-runs against the
199
- caller's cookies on every action call.
359
+ ```tsx
360
+ authenticate: (ctx) => ctx.cookies.get("admin_session")?.value === expected,
361
+ ```
200
362
 
201
- To stay open locally but protected in production:
363
+ Taking them as arguments rather than calling `cookies()` inside means the
364
+ callback is testable without standing up a request. A zero-argument callback
365
+ still works unchanged. When it returns false the `unauthorized` node renders, or
366
+ a minimal built-in screen if you didn't pass one. Polishd ships no auth library,
367
+ so it never pulls one into your dependency tree.
202
368
 
203
- ```tsx
204
- authenticate: async () =>
205
- process.env.NODE_ENV !== "production" || (await isAuthenticated()),
369
+ ### Option 3 — public on purpose
370
+
371
+ ```bash
372
+ POLISHD_DASHBOARD_PUBLIC=true
206
373
  ```
207
374
 
375
+ An explicit opt-in, greppable in your deployment config. You'll still get one
376
+ warning per boot in production, because that's the only place it's actionable.
377
+
378
+ ### Why this is stricter than it looks
379
+
380
+ **The gate covers the AI server actions, not just the page.** A Next server
381
+ action is an independently addressable POST endpoint whose id ships inside your
382
+ public client bundle. Gating only the page render would leave the actions
383
+ callable by anyone — able to change the configured model and API base URL, and
384
+ spend your model tokens. Your check re-runs against the caller's cookies on
385
+ every action call.
386
+
387
+ That's also why production fails shut at the *policy* level rather than merely
388
+ rendering a locked page: a page that showed a lock while still registering
389
+ "open" would leave every action reachable behind a locked-looking door.
390
+
391
+ One consequence worth knowing: if you delete `app/polishd/page.tsx` but keep the
392
+ package installed, the actions remain routable and always deny. That's correct,
393
+ but the symptom — actions failing with an opaque digest — is puzzling if you
394
+ meet it cold.
395
+
208
396
  ## 6. Verify it works
209
397
 
210
398
  ```bash
@@ -213,6 +401,20 @@ npm run build && npm start
213
401
 
214
402
  Then, in order:
215
403
 
404
+ The fastest check is the doctor, which inspects the whole wiring and, with
405
+ `--url`, confirms the live path too:
406
+
407
+ ```bash
408
+ npx @polishd/next doctor --url http://localhost:3000
409
+ ```
410
+
411
+ It catches the failures that produce no error: a proxy named for the wrong Next
412
+ major, `config.matcher` that isn't an inline literal, route segment config
413
+ re-exported rather than declared, the dashboard stylesheet not imported, and events
414
+ being dropped for want of a session cookie.
415
+
416
+ By hand:
417
+
216
418
  1. **Cookie.** `curl -sI http://localhost:3000/ | grep -i set-cookie` should
217
419
  show `polishd_session=<uuid>` with `HttpOnly`.
218
420
  2. **Dashboard.** Visit `/polishd` — expect HTTP 200, styled, with a "no events
@@ -273,9 +475,18 @@ export default definePolishdConfig({
273
475
  sampleRate: 1, // fraction of sessions captured
274
476
  rageClick: { count: 3, windowMs: 500 }, // N clicks on one element
275
477
  flushIntervalMs: 10_000,
478
+ dashboardRoute: "/polishd", // never captured; see below
276
479
  });
277
480
  ```
278
481
 
482
+ The dashboard's own traffic is **never captured**. Reading your analytics is
483
+ browsing too, and left alone it would rank "Refresh summary" as your site's
484
+ most-used feature. Nothing on `dashboardRoute` (or below it) is collected, and
485
+ anything captured before you upgraded is filtered out of the dashboard's
486
+ figures. Set `dashboardRoute` only if you mounted `createPolishdPage()`
487
+ somewhere other than `/polishd` — and pass the config to
488
+ `createPolishdRoute(config)` as well, so the server drops it too.
489
+
279
490
  ```ts
280
491
  // src/instrumentation-client.ts
281
492
  import { initPolishd } from "@polishd/next/client";
@@ -284,20 +495,66 @@ import config from "../polishd.config";
284
495
  initPolishd(config);
285
496
  ```
286
497
 
287
- Pass the same object to `createPolishdRoute(config)` and `createPolishdProxy(config)`
288
- if you change `apiRoute` or `sessionCookie`, so all three agree.
498
+ > **This file is not loaded for you.** It can't be: the three places that read
499
+ > config run in three different runtimes — the browser, the Edge proxy, and the
500
+ > Node route handler — and the Edge one has no filesystem to read it from.
501
+
502
+ So pass the same object to all three:
503
+
504
+ ```ts
505
+ initPolishd(config); // instrumentation-client.ts
506
+ export const POST = createPolishdRoute(config); // app/api/polishd/route.ts
507
+ const { proxy } = createPolishdProxy(config); // proxy.ts
508
+ ```
509
+
510
+ Thread it into one but not another and they disagree silently. A proxy minting
511
+ cookie `a` while ingest reads `b` produces exactly the "beacons return 200,
512
+ dashboard stays empty" failure below.
513
+
514
+ For `sessionCookie` specifically, use the environment variable instead — it's
515
+ the one channel all three runtimes can read, so it can't go half-applied:
516
+
517
+ ```bash
518
+ POLISHD_SESSION_COOKIE=my_session
519
+ ```
289
520
 
290
521
  ## Troubleshooting
291
522
 
523
+ Try `npx @polishd/next doctor` first — it names most of what follows directly,
524
+ and `--url http://localhost:3000` checks the live path as well.
525
+
526
+ **Beacons return 200 but the dashboard stays empty**
527
+ The commonest failure, and the quietest. Ingest deliberately answers a missing
528
+ session cookie with `{"ok":true,"stored":0,"reason":"no_session"}` so beacons
529
+ never retry, and `sendBeacon` can't read a response anyway — so nothing in the
530
+ browser can tell you. The dashboard shows a banner when it detects this, and the
531
+ dev console warns on the first flush. Causes, in order of likelihood: the proxy
532
+ file is named for the wrong Next major; its matcher doesn't cover the pages
533
+ you're visiting; a merge with an existing proxy went wrong; or the proxy and the
534
+ route disagree about the cookie name (set `POLISHD_SESSION_COOKIE`).
535
+
292
536
  **Build fails: "can't recognize the exported `runtime` field in route"**
293
537
  Route segment config was re-exported. Declare `runtime` and `dynamic` inline in
294
538
  the route and page modules.
295
539
 
540
+ **`ExperimentalWarning: SQLite is an experimental feature and might change at
541
+ any time`**
542
+ Expected and harmless. That's Node warning about its own built-in `node:sqlite`
543
+ module, which the dev store uses — it isn't a sign Polishd is unstable, and it
544
+ doesn't appear in production, where Postgres is used instead. Nothing to fix.
545
+
546
+ **The dashboard is cut off — content below the fold is unreachable**
547
+ Your `<body>` doesn't scroll (`overflow: hidden`), which is normal for app
548
+ shells. Polishd renders into its own fixed, scrolling container by default, so
549
+ this should be handled; if it persists, an ancestor with `transform`, `filter`,
550
+ `contain` or `will-change` is defeating `position: fixed`. See
551
+ [The dashboard's own shell](#the-dashboards-own-shell).
552
+
296
553
  **Every `/api/*` route suddenly 404s**
297
554
  Your proxy matcher is matching `/api`. Restore the `(?!api|...)` exclusion.
298
555
 
299
556
  **The dashboard is unstyled**
300
- Tailwind isn't scanning the package. See [Styling](#4-styling).
557
+ The page is missing `import "@polishd/next/dashboard.css";`. See [Styling](#4-styling).
301
558
 
302
559
  **`ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`**
303
560
  Node is older than 22.5. Upgrade, or configure Postgres.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@polishd/next",
3
- "version": "0.1.0",
4
- "description": "Drop-in product analytics for Next.js \u2014 capture real user behavioral signals (rage/dead clicks, scroll depth, errors, web vitals) and explore them on a built-in dashboard. What gets measured gets improved.",
3
+ "version": "0.2.1-dev.0",
4
+ "description": "Drop-in product analytics for Next.js — capture real user behavioral signals (rage/dead clicks, scroll depth, errors, web vitals) and explore them on a built-in dashboard. What gets measured gets improved.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "publishConfig": {
@@ -28,15 +28,17 @@
28
28
  "url": "https://github.com/muskanvaswan/polishd/issues"
29
29
  },
30
30
  "files": [
31
+ "bin",
31
32
  "dist",
32
33
  "docs",
33
34
  "AGENTS.md",
34
35
  "DATABASE.md",
35
36
  "README.md",
36
- "LICENSE"
37
+ "LICENSE",
38
+ "CHANGELOG.md"
37
39
  ],
38
40
  "bin": {
39
- "polishd": "./bin/init.mjs"
41
+ "polishd": "./bin/polishd.mjs"
40
42
  },
41
43
  "exports": {
42
44
  ".": {
@@ -63,13 +65,15 @@
63
65
  "types": "./dist/dashboard/index.d.ts",
64
66
  "default": "./dist/dashboard/index.js"
65
67
  },
66
- "./package.json": "./package.json"
68
+ "./package.json": "./package.json",
69
+ "./dashboard.css": "./dist/dashboard.css"
67
70
  },
68
71
  "scripts": {
69
- "build": "tsc -p tsconfig.build.json",
72
+ "build": "tsc -p tsconfig.build.json && npm run build:css",
70
73
  "typecheck": "tsc -p tsconfig.json",
71
74
  "clean": "rm -rf dist",
72
- "prepublishOnly": "npm run clean && npm run build"
75
+ "prepublishOnly": "npm run clean && npm run build",
76
+ "build:css": "node scripts/build-css.mjs"
73
77
  },
74
78
  "peerDependencies": {
75
79
  "next": ">=15",
@@ -80,13 +84,16 @@
80
84
  "pg": "^8.0.0"
81
85
  },
82
86
  "devDependencies": {
87
+ "@tailwindcss/cli": "^4.3.3",
83
88
  "@types/node": "^20.14.8",
84
89
  "@types/pg": "^8.11.0",
85
90
  "@types/react": "^19",
86
91
  "@types/react-dom": "^19",
87
92
  "next": "^16",
93
+ "postcss": "^8.5.26",
88
94
  "react": "^19",
89
95
  "react-dom": "^19",
96
+ "tailwindcss": "^4.3.3",
90
97
  "typescript": "^5.5.2"
91
98
  }
92
99
  }