@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/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
  Drop-in **product analytics** for Next.js (App Router). Capture real user
6
6
  behavioral signals — rage clicks, dead clicks, scroll depth, JS errors, web
7
7
  vitals, component engagement — and explore them on a built-in, Vercel-style
8
- dashboard at `/polishd`.
8
+ dashboard at `/polishd`. A second **Design** tab reverse-engineers your site's
9
+ design system from the rendered pages — typography, palette, radii, spacing —
10
+ and flags what breaks it.
9
11
 
10
12
  No third-party service. Events go to your own database: **SQLite in dev**
11
13
  (zero-config, built into Node), **Postgres in production**. Anonymous by design —
@@ -13,6 +15,32 @@ one httpOnly UUID cookie per session, no fingerprinting, no PII.
13
15
 
14
16
  ---
15
17
 
18
+ ## Installing with a coding agent
19
+
20
+ Polishd ships a machine-readable install procedure. If you use Claude Code,
21
+ Cursor, Copilot, or any other coding agent, **paste this prompt** and let it do
22
+ the whole install:
23
+
24
+ ```text
25
+ Install @polishd/next into this Next.js app.
26
+
27
+ Follow the procedure in AGENTS.md from the package exactly — after installing,
28
+ read node_modules/@polishd/next/AGENTS.md. If the package isn't installed yet,
29
+ read it at https://github.com/muskanvaswan/polishd/blob/main/AGENTS.md
30
+
31
+ Work through its phases in order and do not skip Phase 7 (verification).
32
+ Several of its constraints fail silently at runtime rather than at build time,
33
+ so a passing build does not mean the install worked.
34
+
35
+ Finish by running `npx @polishd/next doctor` and fixing everything it reports.
36
+ ```
37
+
38
+ `npx @polishd/next doctor` is the ground truth: it checks the wiring that fails
39
+ quietly — the proxy named for the wrong Next major, route segment config
40
+ re-exported instead of declared, a matcher that isn't an inline literal, the
41
+ dashboard stylesheet not imported. Add `--url http://localhost:3000` to also
42
+ verify live that the session cookie is minted and events actually store.
43
+
16
44
  ## Documentation
17
45
 
18
46
  | | |
@@ -20,27 +48,36 @@ one httpOnly UUID cookie per session, no fingerprinting, no PII.
20
48
  | **[docs/SETUP.md](./docs/SETUP.md)** | Full walkthrough — every variant, verification, troubleshooting |
21
49
  | **[AGENTS.md](./AGENTS.md)** | Install procedure written for AI coding agents |
22
50
  | **[DATABASE.md](./DATABASE.md)** | Production database setup, schema, retention |
51
+ | **[CHANGELOG.md](./CHANGELOG.md)** | Release notes — **start here if you're upgrading from 0.1.x** |
23
52
 
24
53
  ## Requirements
25
54
 
26
55
  - Next.js **15+**, App Router (no Pages Router support)
27
56
  - Node **22.5+** (the dev store uses the built-in `node:sqlite`)
28
- - Tailwind CSS v3 or v4, if you want the dashboard styled — see [Styling](#styling-required)
29
57
  - Postgres in production; nothing extra for local development
30
58
 
31
59
  ## Quickstart
32
60
 
33
61
  ```bash
34
62
  npm install @polishd/next
35
- npx polishd init
36
- npm run dev # then visit /polishd
63
+ npx @polishd/next init
64
+ npm run dev # then visit /polishd
65
+ npx @polishd/next doctor # confirm the wiring
37
66
  ```
38
67
 
68
+ (`npx polishd init` is the shorter form once the package is installed;
69
+ `npx @polishd/next init` also works before it is.)
70
+
39
71
  That's it locally — events write to `.polishd/analytics.db` with no
40
- configuration. `init` detects your layout (`src/` or root), writes the four
41
- files below, adds `.polishd/` to `.gitignore`, wires Tailwind, and skips
42
- anything that already exists (`--force` to overwrite, `--dry-run` to preview,
43
- `--js` for JavaScript, `--config` to also emit a `polishd.config.ts`).
72
+ configuration. `init` detects your layout (`src/` or root), detects your
73
+ installed Next major to name the proxy file correctly, writes the four files
74
+ below, and adds `.polishd/` to `.gitignore`.
75
+
76
+ Files it already wrote are skipped unless you pass `--force`, which backs them
77
+ up to `.bak` first. **Files it did not write are never overwritten**, with or
78
+ without `--force` — if you already have a proxy, it prints how to merge instead
79
+ of clobbering your code. Other flags: `--dry-run` to preview, `--js` for
80
+ JavaScript, `--config` to also emit a `polishd.config.ts`.
44
81
 
45
82
  `pg` installs automatically as an optional dependency and is loaded only when
46
83
  you point Polishd at a Postgres URL.
@@ -70,11 +107,12 @@ export const runtime = "nodejs"; // must be declared here, not re-exporte
70
107
  export const dynamic = "force-dynamic";
71
108
  export const POST = createPolishdRoute();
72
109
 
73
- // src/app/polishd/page.tsx — the dashboard (unguarded by default)
110
+ // src/app/polishd/page.tsx — the dashboard
111
+ import "@polishd/next/dashboard.css"; // self-contained; no Tailwind needed
74
112
  import { createPolishdPage } from "@polishd/next/dashboard";
75
113
  export const runtime = "nodejs"; // Next requires these inline in the page
76
114
  export const dynamic = "force-dynamic";
77
- export default createPolishdPage();
115
+ export default createPolishdPage(); // see Protecting the dashboard
78
116
  ```
79
117
 
80
118
  > **Route segment config can't be re-exported.** Next statically parses
@@ -88,35 +126,127 @@ export default createPolishdPage();
88
126
  > under Next 16 + Turbopack, a proxy matcher that touches any `/api/*` route
89
127
  > breaks resolution for the entire `/api` segment.
90
128
 
91
- ## Styling (required)
129
+ ## Composing with an existing proxy
92
130
 
93
- The dashboard is built from Tailwind utility classes and ships **no stylesheet
94
- of its own**, so Tailwind has to scan the package's `dist` — it won't find it by
95
- default. `init` wires this up for you; if you're doing it by hand:
131
+ Next allows exactly **one** proxy module per app (`proxy.ts` on Next 16+,
132
+ `middleware.ts` on Next 15), so if you already have one, Polishd has to go
133
+ inside it. `init` detects this and prints instructions rather than overwriting.
96
134
 
97
- ```css
98
- /* Tailwind v4 — in the CSS file that imports tailwind */
99
- @import "tailwindcss";
100
- @source "../../node_modules/@polishd/next/dist"; /* relative to this file */
101
- ```
135
+ The subtle part is the matcher. One file means one `config.matcher`, so adding
136
+ Polishd means widening it to the union — and Polishd's matcher is near-total.
137
+ Your handler, which you deliberately scoped to a couple of routes, would
138
+ silently start running on every request in the app. Nothing errors; your
139
+ redirects and rewrites just begin firing where they never did before.
140
+
141
+ Declare your original scope and `composePolishd` re-applies it as an internal
142
+ guard:
102
143
 
103
144
  ```ts
104
- /* Tailwind v3 — in tailwind.config.ts */
105
- content: [
106
- "./src/**/*.{ts,tsx}",
107
- "./node_modules/@polishd/next/dist/**/*.js",
108
- ],
145
+ // proxy.ts (middleware.ts on Next 15, with the function named `middleware`)
146
+ import { composePolishd } from "@polishd/next/proxy";
147
+ import type { NextRequest } from "next/server";
148
+
149
+ function myProxy(request: NextRequest) {
150
+ if (!hasSession(request)) return NextResponse.redirect(new URL("/login", request.url));
151
+ }
152
+
153
+ // Your handler still only runs on /admin/*, even though the matcher below
154
+ // now feeds Polishd every page request.
155
+ export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });
156
+
157
+ // Written by hand: Next statically parses this and it must be an inline
158
+ // literal, so no helper can generate it. The union of your paths and Polishd's.
159
+ export const config = {
160
+ matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
161
+ };
109
162
  ```
110
163
 
111
- Skip this and `/polishd` renders as unstyled HTML. Tailwind v4's automatic content
112
- detection deliberately ignores `node_modules`, so the `@source` line is not
113
- optional.
164
+ Your handler runs first and its response is threaded through, so a redirect or
165
+ rewrite you return **carries the analytics cookie** rather than losing it.
166
+ Returning nothing is fine. `matcher` accepts Next's matcher strings
167
+ (`"/admin/:path*"`, `"/((?!api).*)"`) or a `RegExp` when you want to be exact;
168
+ omit it only if your handler genuinely did run on everything.
169
+
170
+ Widening the matcher is safe from Polishd's side: `withPolishdSession` returns
171
+ early once the cookie is present, so the extra paths cost a cookie lookup and
172
+ nothing else.
173
+
174
+ ## Styling
175
+
176
+ The dashboard ships its own stylesheet. One import, no configuration:
177
+
178
+ ```tsx
179
+ // app/polishd/page.tsx — `init` writes this for you
180
+ import "@polishd/next/dashboard.css";
181
+ ```
182
+
183
+ **Tailwind is not required in your app.** The stylesheet is compiled at publish
184
+ time, so it doesn't depend on your build setup, your Tailwind version, or
185
+ whether you use Tailwind at all.
186
+
187
+ It is built not to interfere with the app it lands in:
188
+
189
+ - **No global preflight.** Tailwind's reset is what would restyle every element
190
+ on your page; it's left out. A scoped equivalent applies to the dashboard's
191
+ own subtree instead, so your headings, lists and buttons are untouched.
192
+ - **Every rule is scoped to `.polishd-root`**, verified at build time — nothing
193
+ in the file can match your markup.
194
+ - **The scope is doubled (`.polishd-root.polishd-root`)** so the dashboard wins
195
+ specificity ties against a host that happens to have its own `.flex`,
196
+ `.hidden` or `.container`, rather than the winner depending on stylesheet
197
+ order.
198
+ - **No cascade layers**, because unlayered host CSS beats layered CSS outright —
199
+ a plain `h1 { color: … }` in your app would otherwise override the dashboard's
200
+ own typography.
201
+
202
+ The one thing it can't win is a host `!important` rule aimed at the same
203
+ property. Nothing short of `!important` on our side would, and a package
204
+ shouting over its host is worse than a rare collision.
205
+
206
+ > **Upgrading from 0.1.x?** Nothing to do — your existing `@source
207
+ > "…/@polishd/next/dist"` keeps compiling the dashboard's utilities exactly as
208
+ > before, so the dashboard stays styled whether or not you add the import.
209
+ >
210
+ > To move over at your own pace, **add the import first, then** delete the
211
+ > `@source` line — in that order, so the dashboard is never unstyled in between.
212
+ > `polishd doctor` reports which setup it finds and won't call a working 0.1.x
213
+ > install broken.
114
214
 
115
215
  ## Protecting the dashboard
116
216
 
117
- By default `/polishd` is **public** (a dev-only console warning reminds you).
118
- Gate it with an `authenticate` callback, and optionally render your own sign-in
119
- UI when it fails:
217
+ The dashboard is not a read-only report. Its settings change the configured
218
+ model and **API base URL**, so anyone who can reach it can point your requests
219
+ at their own endpoint and spend your key. Treat it as a credential-adjacent
220
+ surface.
221
+
222
+ In **development** it is open and says so once in the console. In
223
+ **production** Polishd refuses to serve it until you have made a decision.
224
+ There are three ways to make one.
225
+
226
+ ### 1. A token — no code, works with no auth of your own
227
+
228
+ ```bash
229
+ POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)
230
+ ```
231
+
232
+ That is the whole integration. The locked screen shows a password field; the
233
+ token is exchanged for an httpOnly, path-scoped cookie by a server action, so
234
+ it never appears in a URL, in browser history, in server logs, or in a
235
+ `Referer` header. The cookie holds an HMAC of the token rather than the token
236
+ itself, comparison is constant-time, expiry is enforced server-side, and the
237
+ unlock form is rate-limited per IP.
238
+
239
+ Tune it if you moved the dashboard off `/polishd`:
240
+
241
+ ```tsx
242
+ import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";
243
+
244
+ export default createPolishdPage({
245
+ authenticate: polishdTokenAuth({ basePath: "/admin/polishd" }),
246
+ });
247
+ ```
248
+
249
+ ### 2. Your app's own auth
120
250
 
121
251
  ```tsx
122
252
  import { createPolishdPage } from "@polishd/next/dashboard";
@@ -127,21 +257,37 @@ export const runtime = "nodejs";
127
257
  export const dynamic = "force-dynamic";
128
258
 
129
259
  export default createPolishdPage({
130
- authenticate: isAuthenticated, // () => boolean | Promise<boolean>
131
- unauthorized: <MyLogin />, // optional; a minimal screen otherwise
260
+ authenticate: isAuthenticated, // (ctx) => boolean | Promise<boolean>
261
+ unauthorized: <MyLogin />, // optional; a minimal screen otherwise
132
262
  });
133
263
  ```
134
264
 
135
- The same callback also guards the AI **server actions** (generate summary, save
136
- settings, scan codebase, file GitHub issues). That matters: a server action is
137
- an addressable POST endpoint whose id ships in the public client bundle, so
138
- gating only the page would leave those actions callable by anyone. They re-run
139
- `authenticate` against the caller's cookies on every call, and deny when no
140
- dashboard page has registered a policy.
265
+ `authenticate` receives the request's `cookies` and `headers`, so it is an
266
+ ordinary function of its inputs and can be unit tested without a live request:
141
267
 
142
- If you pass no `authenticate`, the dashboard **and** its actions are public —
143
- anyone who finds the URL can read your analytics, change the configured model
144
- and API base URL, and spend your model tokens. Always gate it in production.
268
+ ```ts
269
+ const authenticate = (ctx) => ctx.cookies.get("session")?.value === expected;
270
+ ```
271
+
272
+ A zero-argument callback still works unchanged.
273
+
274
+ ### 3. Public on purpose
275
+
276
+ ```bash
277
+ POLISHD_DASHBOARD_PUBLIC=true
278
+ ```
279
+
280
+ An explicit, grep-able opt-in. You will still get one console warning per boot,
281
+ in production, because that is the only place it is worth acting on.
282
+
283
+ > **The gate covers the server actions, not just the page.** The AI actions
284
+ > (generate summary, save settings, scan codebase, file GitHub issues) are
285
+ > independently addressable POST endpoints whose ids ship in the public client
286
+ > bundle, so gating only the page would leave them callable by anyone. They
287
+ > re-run your check against the caller's cookies on every call, and deny when
288
+ > no dashboard page has registered a policy — including when you delete
289
+ > `app/polishd/page.tsx` but keep the package, in which case the actions stay
290
+ > routable and always deny.
145
291
 
146
292
  ## AI summary
147
293
 
@@ -216,6 +362,40 @@ authoritative context, so:
216
362
  > provide. Your source is read only during an explicit scan (or a targeted
217
363
  > gap-fill), and always under strict size budgets.
218
364
 
365
+ ## Design review
366
+
367
+ The dashboard's **Design** tab (in the sidebar) is your site's brand guideline
368
+ reverse-engineered from what actually shipped. Every page measures its own
369
+ rendered design once per visitor session — the computed typography, colors,
370
+ corner radii and padding, reduced to a compact tally of a few KB — and the tab
371
+ assembles the site-wide picture:
372
+
373
+ - **Typography** — every font family / size / weight combination in use,
374
+ rendered as specimens with real text from your pages.
375
+ - **Color palette** — swatches weighted by how much of the UI each color
376
+ covers, split by role (text / background / border).
377
+ - **Contrast** — each text-on-background pairing checked against WCAG AA.
378
+ - **Corner radii & spacing** — the rounding and padding scales in use.
379
+
380
+ The first layer of review is **deterministic** — rule-based checks, no model:
381
+ too many font sizes to be a type scale, extra font families, near-duplicate
382
+ colors (`#fefefe` living next to `#ffffff`), contrast failures with the exact
383
+ ratio, one-off values that appear on a single page, padding off the 4px grid.
384
+
385
+ The second layer is the **Aesthetic review** card: the same model you
386
+ configured for the analytics summary reads the measured tokens and findings
387
+ and answers the real question — does this read as one deliberate design, or an
388
+ accumulation of one-off decisions? Issues it raises must cite a token that
389
+ actually appears in the metrics (a hex value, a px size, a page path), and each
390
+ comes with a concrete fix. It follows the summary's token-thrift rules: the
391
+ model sees a few hundred tokens of aggregated metrics, the result is cached and
392
+ fingerprinted, and regenerating over unchanged metrics is free. Refresh buttons
393
+ for both the metrics and the analysis are on the tab.
394
+
395
+ Nothing extra to install or wire: the scan ships with the existing client,
396
+ flows through the existing ingest route, and the dashboard itself is never
397
+ scanned.
398
+
219
399
  ## Component-level tracking
220
400
 
221
401
  Wrap any element to track it explicitly. Interactive controls get hover + click
@@ -236,8 +416,7 @@ import { PolishdMonitor } from "@polishd/next/client";
236
416
 
237
417
  ## Configuration
238
418
 
239
- Defaults suit a low-traffic site. Override via `polishd.config.ts` and pass it to
240
- `initPolishd`:
419
+ Defaults suit a low-traffic site. Override via `polishd.config.ts`:
241
420
 
242
421
  ```ts
243
422
  // polishd.config.ts
@@ -248,11 +427,32 @@ export default definePolishdConfig({
248
427
  });
249
428
  ```
250
429
 
430
+ The dashboard never measures itself: nothing on `/polishd` (or below it) is
431
+ captured, so reading your analytics can't show up as your site's most-used
432
+ feature. Moved the dashboard elsewhere? Set `dashboardRoute`.
433
+
434
+ > **Nothing imports this file for you.** It cannot be auto-loaded: the three
435
+ > places that read config run in three different runtimes — the browser, the
436
+ > Edge proxy, and the Node route handler — and the Edge one has no filesystem.
437
+ > Pass it to each explicitly:
438
+
251
439
  ```ts
252
- // src/instrumentation-client.ts
253
- import { initPolishd } from "@polishd/next/client";
254
440
  import config from "../polishd.config";
255
- initPolishd(config);
441
+
442
+ initPolishd(config); // instrumentation-client.ts
443
+ export const POST = createPolishdRoute(config); // app/api/polishd/route.ts
444
+ const { proxy } = createPolishdProxy(config); // proxy.ts
445
+ ```
446
+
447
+ Thread it into one but not another and the layers disagree silently — a proxy
448
+ minting cookie `a` while ingest looks for `b` produces exactly the empty
449
+ dashboard described below.
450
+
451
+ For **`sessionCookie` specifically, prefer the environment variable**. It is the
452
+ one channel every runtime can read, so it cannot go half-applied:
453
+
454
+ ```bash
455
+ POLISHD_SESSION_COOKIE=my_session
256
456
  ```
257
457
 
258
458
  ## Environment variables
@@ -261,6 +461,9 @@ initPolishd(config);
261
461
  |---|---|---|
262
462
  | `POLISHD_DATABASE_URL` | production | Pooled Postgres connection string — enables capture |
263
463
  | `POLISHD_DB_PATH` | dev (optional) | Custom SQLite path (default `.polishd/analytics.db`) |
464
+ | `POLISHD_DASHBOARD_TOKEN` | production | Protects `/polishd` with the built-in gate. Generate with `openssl rand -hex 32` |
465
+ | `POLISHD_DASHBOARD_PUBLIC` | production (optional) | `true` — serve the dashboard unguarded on purpose |
466
+ | `POLISHD_SESSION_COOKIE` | optional | Rename the anonymous session cookie. Read by both the proxy and ingest, so they cannot disagree |
264
467
  | `POLISHD_AI_PROVIDER` | AI (optional) | `anthropic` \| `openai` \| `openai-compatible` \| `google` |
265
468
  | `POLISHD_AI_MODEL` | AI (optional) | Model id (defaults per provider, e.g. `claude-opus-4-8`) |
266
469
  | `POLISHD_AI_API_KEY` | AI (optional) | Model API key — preset instead of using the dashboard |
@@ -280,16 +483,39 @@ Local dev needs nothing. If no writable store is available the package degrades
280
483
  to a safe no-op (a console warning, dashboard notice) — your app never breaks.
281
484
  See [DATABASE.md](./DATABASE.md) for production setup.
282
485
 
486
+ ## Troubleshooting
487
+
488
+ Run `npx @polishd/next doctor` first — it catches most of these by name, and
489
+ `--url http://localhost:3000` checks the live path as well.
490
+
491
+ | Symptom | Cause | Fix |
492
+ |---|---|---|
493
+ | Dashboard empty, beacons all return 200 | The proxy isn't running, so no session cookie is minted and ingest drops every batch | Check the file is named for your Next major — `proxy.ts` on 16+, `middleware.ts` on 15 — and exports a function of the same name. The dashboard shows a banner when it detects this |
494
+ | Every `/api/*` route 404s | The proxy matcher touches `/api` | Restore the `(?!api…)` exclusion. Under Next 16 + Turbopack this breaks the whole `/api` segment |
495
+ | Build fails: *"can't recognize the exported `runtime` field"* | Route segment config re-exported | Declare `runtime`/`dynamic` inline in the module |
496
+ | Dashboard renders unstyled | `@polishd/next/dashboard.css` isn't imported | See [Styling](#styling) |
497
+ | Dashboard content cut off below the fold | A host `body` that doesn't scroll | Fixed by default; if an ancestor sets `transform`/`filter`/`contain`, pass `shell: false` and see [docs/SETUP.md](./docs/SETUP.md) |
498
+ | `ExperimentalWarning: SQLite is an experimental feature` | Node's built-in `node:sqlite` | **Expected and harmless.** Node prints this for its own SQLite module on 22.x. Not a sign the package is unstable, and it disappears in production where Postgres is used |
499
+ | `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite` | Node < 22.5 | Upgrade Node, or configure Postgres |
500
+ | Server actions fail with an opaque digest | No dashboard page registered a policy | Expected if you deleted `app/polishd/page.tsx` — the actions fail closed by design |
501
+
283
502
  ## Entry points
284
503
 
285
504
  | Import | Contents |
286
505
  |---|---|
287
- | `@polishd/next` | config, `definePolishdConfig`, event types (isomorphic) |
506
+ | `@polishd/next` | config, `definePolishdConfig`, `resolveSessionCookie`, event types (isomorphic) |
288
507
  | `@polishd/next/client` | `initPolishd`, `PolishdMonitor` |
289
508
  | `@polishd/next/server` | store, ingest, queries, `withPolishdSession` (Node only) |
290
509
  | `@polishd/next/route` | `POST`, `createPolishdRoute` |
291
- | `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher` |
292
- | `@polishd/next/dashboard` | `createPolishdPage`, `PolishdDashboard`, `loadPolishdDashboardData` |
510
+ | `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher`, `composePolishd` |
511
+ | `@polishd/next/dashboard` | `createPolishdPage`, `polishdTokenAuth`, `PolishdDashboard`, `loadPolishdDashboardData` |
512
+
513
+ ## CLI
514
+
515
+ | Command | Purpose |
516
+ |---|---|
517
+ | `npx @polishd/next init` | Scaffold the glue files. `--dry-run`, `--force`, `--js`, `--config` |
518
+ | `npx @polishd/next doctor` | Check an install. `--url <origin>` adds live checks |
293
519
 
294
520
  ## Development
295
521