@polishd/next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +262 -0
- package/DATABASE.md +147 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/bin/init.mjs +224 -0
- package/dist/ai/actions.d.ts +23 -0
- package/dist/ai/actions.d.ts.map +1 -0
- package/dist/ai/actions.js +58 -0
- package/dist/ai/actions.js.map +1 -0
- package/dist/ai/digest.d.ts +9 -0
- package/dist/ai/digest.d.ts.map +1 -0
- package/dist/ai/digest.js +108 -0
- package/dist/ai/digest.js.map +1 -0
- package/dist/ai/github.d.ts +51 -0
- package/dist/ai/github.d.ts.map +1 -0
- package/dist/ai/github.js +175 -0
- package/dist/ai/github.js.map +1 -0
- package/dist/ai/guard.d.ts +43 -0
- package/dist/ai/guard.d.ts.map +1 -0
- package/dist/ai/guard.js +57 -0
- package/dist/ai/guard.js.map +1 -0
- package/dist/ai/issues.d.ts +21 -0
- package/dist/ai/issues.d.ts.map +1 -0
- package/dist/ai/issues.js +291 -0
- package/dist/ai/issues.js.map +1 -0
- package/dist/ai/profile.d.ts +26 -0
- package/dist/ai/profile.d.ts.map +1 -0
- package/dist/ai/profile.js +160 -0
- package/dist/ai/profile.js.map +1 -0
- package/dist/ai/providers.d.ts +40 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +142 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/scan.d.ts +38 -0
- package/dist/ai/scan.d.ts.map +1 -0
- package/dist/ai/scan.js +274 -0
- package/dist/ai/scan.js.map +1 -0
- package/dist/ai/settings.d.ts +31 -0
- package/dist/ai/settings.d.ts.map +1 -0
- package/dist/ai/settings.js +182 -0
- package/dist/ai/settings.js.map +1 -0
- package/dist/ai/summary.d.ts +26 -0
- package/dist/ai/summary.d.ts.map +1 -0
- package/dist/ai/summary.js +260 -0
- package/dist/ai/summary.js.map +1 -0
- package/dist/ai/types.d.ts +188 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +13 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/client/init.d.ts +14 -0
- package/dist/client/init.d.ts.map +1 -0
- package/dist/client/init.js +274 -0
- package/dist/client/init.js.map +1 -0
- package/dist/client/monitor.d.ts +23 -0
- package/dist/client/monitor.d.ts.map +1 -0
- package/dist/client/monitor.js +176 -0
- package/dist/client/monitor.js.map +1 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/elements.d.ts +6 -0
- package/dist/dashboard/elements.d.ts.map +1 -0
- package/dist/dashboard/elements.js +35 -0
- package/dist/dashboard/elements.js.map +1 -0
- package/dist/dashboard/features.d.ts +6 -0
- package/dist/dashboard/features.d.ts.map +1 -0
- package/dist/dashboard/features.js +28 -0
- package/dist/dashboard/features.js.map +1 -0
- package/dist/dashboard/index.d.ts +54 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +140 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/journeys.d.ts +5 -0
- package/dist/dashboard/journeys.d.ts.map +1 -0
- package/dist/dashboard/journeys.js +158 -0
- package/dist/dashboard/journeys.js.map +1 -0
- package/dist/dashboard/pages.d.ts +5 -0
- package/dist/dashboard/pages.d.ts.map +1 -0
- package/dist/dashboard/pages.js +104 -0
- package/dist/dashboard/pages.js.map +1 -0
- package/dist/dashboard/summary.d.ts +13 -0
- package/dist/dashboard/summary.d.ts.map +1 -0
- package/dist/dashboard/summary.js +333 -0
- package/dist/dashboard/summary.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +29 -0
- package/dist/proxy.js.map +1 -0
- package/dist/route.d.ts +24 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +37 -0
- package/dist/route.js.map +1 -0
- package/dist/server/ingest.d.ts +14 -0
- package/dist/server/ingest.d.ts.map +1 -0
- package/dist/server/ingest.js +73 -0
- package/dist/server/ingest.js.map +1 -0
- package/dist/server/queries.d.ts +245 -0
- package/dist/server/queries.d.ts.map +1 -0
- package/dist/server/queries.js +547 -0
- package/dist/server/queries.js.map +1 -0
- package/dist/server/store.d.ts +29 -0
- package/dist/server/store.d.ts.map +1 -0
- package/dist/server/store.js +359 -0
- package/dist/server/store.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/shared/types.d.ts +56 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +27 -0
- package/dist/shared/types.js.map +1 -0
- package/docs/SETUP.md +315 -0
- package/package.json +92 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# AGENTS.md — installing `@polishd/next` into an app
|
|
2
|
+
|
|
3
|
+
Instructions for a coding agent asked to add Polishd to a Next.js application.
|
|
4
|
+
Human-oriented prose lives in [README.md](./README.md) and
|
|
5
|
+
[docs/SETUP.md](./docs/SETUP.md); this file is the procedure.
|
|
6
|
+
|
|
7
|
+
Work through the phases in order. Do not skip verification — several of the
|
|
8
|
+
constraints below fail only at build time or, worse, silently at runtime.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Phase 1 — Check preconditions
|
|
13
|
+
|
|
14
|
+
Read these from the target app before changing anything.
|
|
15
|
+
|
|
16
|
+
| Check | How | If it fails |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Next.js App Router | an `app/` or `src/app/` directory exists | Stop. Polishd has no Pages Router support. |
|
|
19
|
+
| Next.js ≥ 15 | `next` version in `package.json` | Stop and report. |
|
|
20
|
+
| Node ≥ 22.5 | `node -v`, plus `engines`/`.nvmrc`/host setting | Capture silently no-ops on older Node. Warn the user and continue only if they accept that. |
|
|
21
|
+
| Tailwind CSS | `tailwindcss` in dependencies | The dashboard renders unstyled. See Phase 4. |
|
|
22
|
+
| Package manager | lockfile name | Use it consistently; don't mix. |
|
|
23
|
+
|
|
24
|
+
Record two facts you will need throughout:
|
|
25
|
+
|
|
26
|
+
- **`BASE`** — `src` if `src/app/` exists, otherwise `` (repo root).
|
|
27
|
+
- **`NEXT_MAJOR`** — 16+ uses `proxy.ts`; 15 uses `middleware.ts`.
|
|
28
|
+
|
|
29
|
+
## Phase 2 — Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @polishd/next
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`pg` arrives automatically as an optional dependency; it is only loaded when a
|
|
36
|
+
Postgres URL is configured. No separate install is needed.
|
|
37
|
+
|
|
38
|
+
## Phase 3 — Scaffold
|
|
39
|
+
|
|
40
|
+
Prefer the CLI. It handles every branch in Phase 1 and Phase 4 for you:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx polishd init
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Flags: `--dry-run` (preview), `--force` (overwrite existing), `--js` (JavaScript
|
|
47
|
+
output), `--config` (also emit `polishd.config.ts`).
|
|
48
|
+
|
|
49
|
+
It writes four files, appends `.polishd/` to `.gitignore`, and wires Tailwind.
|
|
50
|
+
If it reports that a file already exists, do not overwrite blindly — merge by
|
|
51
|
+
hand following Phase 3b.
|
|
52
|
+
|
|
53
|
+
### Phase 3b — Manual scaffold (only if the CLI could not run)
|
|
54
|
+
|
|
55
|
+
Create exactly these four files. Contents are load-bearing; see Phase 5.
|
|
56
|
+
|
|
57
|
+
**1. `{BASE}/proxy.ts`** — mints the anonymous session cookie.
|
|
58
|
+
On Next 15 name the file `middleware.ts` and the function `middleware`.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import type { NextRequest } from "next/server";
|
|
62
|
+
import { withPolishdSession } from "@polishd/next/proxy";
|
|
63
|
+
|
|
64
|
+
export function proxy(request: NextRequest) {
|
|
65
|
+
return withPolishdSession(request);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const config = {
|
|
69
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If the app already has a proxy/middleware, do **not** replace it. Call
|
|
74
|
+
`withPolishdSession(request, existingResponse)` in the existing handler and
|
|
75
|
+
merge the matcher, keeping the `api` exclusion.
|
|
76
|
+
|
|
77
|
+
**2. `{BASE}/instrumentation-client.ts`** — starts browser capture.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { initPolishd } from "@polishd/next/client";
|
|
81
|
+
|
|
82
|
+
initPolishd();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**3. `{BASE}/app/api/polishd/route.ts`** — the ingest endpoint.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createPolishdRoute } from "@polishd/next/route";
|
|
89
|
+
|
|
90
|
+
export const runtime = "nodejs";
|
|
91
|
+
export const dynamic = "force-dynamic";
|
|
92
|
+
|
|
93
|
+
export const POST = createPolishdRoute();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**4. `{BASE}/app/polishd/page.tsx`** — the dashboard.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
100
|
+
|
|
101
|
+
export const runtime = "nodejs";
|
|
102
|
+
export const dynamic = "force-dynamic";
|
|
103
|
+
|
|
104
|
+
export default createPolishdPage();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Then add `.polishd/` to `.gitignore`.
|
|
108
|
+
|
|
109
|
+
## Phase 4 — Wire Tailwind
|
|
110
|
+
|
|
111
|
+
The dashboard is built from Tailwind utility classes and ships **no stylesheet**.
|
|
112
|
+
Tailwind will not find it on its own — v4's content detection deliberately
|
|
113
|
+
ignores `node_modules`, and v3 only scans the globs you give it. Skipping this
|
|
114
|
+
step produces a dashboard that returns HTTP 200 and is completely unstyled.
|
|
115
|
+
|
|
116
|
+
**Tailwind v4** (no `tailwind.config.*`; CSS has `@import "tailwindcss"`) — add
|
|
117
|
+
to that CSS file, with the path relative to the file itself:
|
|
118
|
+
|
|
119
|
+
```css
|
|
120
|
+
@import "tailwindcss";
|
|
121
|
+
@source "../../node_modules/@polishd/next/dist";
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Tailwind v3** — add to `content` in `tailwind.config.*`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
content: ["./src/**/*.{ts,tsx}", "./node_modules/@polishd/next/dist/**/*.js"],
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**No Tailwind** — tell the user the dashboard will be unstyled. Do not add
|
|
131
|
+
Tailwind to their app to fix this unless they ask.
|
|
132
|
+
|
|
133
|
+
## Phase 5 — Invariants
|
|
134
|
+
|
|
135
|
+
Violating any of these produces a broken build or a security hole. They are not
|
|
136
|
+
style preferences.
|
|
137
|
+
|
|
138
|
+
1. **`config.matcher` must be an inline array literal.** Next parses it
|
|
139
|
+
statically; it cannot be imported, spread, or re-exported. It must exclude
|
|
140
|
+
all of `/api` — under Next 16 + Turbopack a matcher that touches any
|
|
141
|
+
`/api/*` route breaks resolution for the entire `/api` segment.
|
|
142
|
+
|
|
143
|
+
2. **`runtime` and `dynamic` must be declared inline** in both the route and the
|
|
144
|
+
page module. `export { POST, runtime, dynamic } from "@polishd/next/route"`
|
|
145
|
+
fails the build with *"Next.js can't recognize the exported `runtime` field
|
|
146
|
+
in route. It mustn't be reexported."*
|
|
147
|
+
|
|
148
|
+
3. **Gate the dashboard in production.** With no `authenticate` callback, the
|
|
149
|
+
dashboard *and its server actions* are public: anyone reaching the URL can
|
|
150
|
+
read the analytics, change the configured model and API base URL, and spend
|
|
151
|
+
the owner's model tokens. See Phase 6.
|
|
152
|
+
|
|
153
|
+
4. **Never import `@polishd/next/server` from a client component.** It pulls
|
|
154
|
+
database drivers into the browser bundle. Use `@polishd/next/client` there.
|
|
155
|
+
|
|
156
|
+
5. **`runtime` must be `"nodejs"`.** The store uses `node:sqlite`/`pg`; neither
|
|
157
|
+
runs on the Edge runtime.
|
|
158
|
+
|
|
159
|
+
## Phase 6 — Protect the dashboard
|
|
160
|
+
|
|
161
|
+
If the app has any auth helper, wire it up. Ask the user which one rather than
|
|
162
|
+
guessing.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { createPolishdPage } from "@polishd/next/dashboard";
|
|
166
|
+
import { isAuthenticated } from "@/lib/auth";
|
|
167
|
+
import { MyLogin } from "./login";
|
|
168
|
+
|
|
169
|
+
export const runtime = "nodejs";
|
|
170
|
+
export const dynamic = "force-dynamic";
|
|
171
|
+
|
|
172
|
+
export default createPolishdPage({
|
|
173
|
+
authenticate: isAuthenticated, // () => boolean | Promise<boolean>
|
|
174
|
+
unauthorized: <MyLogin />, // optional; a minimal screen otherwise
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The same callback guards the AI server actions, re-running per request against
|
|
179
|
+
the caller's cookies. Polishd ships no auth code of its own.
|
|
180
|
+
|
|
181
|
+
A common pattern is to stay open in development and require auth in production:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
authenticate: async () =>
|
|
185
|
+
process.env.NODE_ENV !== "production" || (await isAuthenticated()),
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Phase 7 — Verify
|
|
189
|
+
|
|
190
|
+
Run all of these. Report the results; do not declare success from the build
|
|
191
|
+
alone.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# 1. Build must pass.
|
|
195
|
+
npm run build
|
|
196
|
+
|
|
197
|
+
# 2. Serve it.
|
|
198
|
+
npm start &
|
|
199
|
+
|
|
200
|
+
# 3. The proxy mints a session cookie on a page request.
|
|
201
|
+
curl -sI http://localhost:3000/ | grep -i set-cookie
|
|
202
|
+
# expect: polishd_session=<uuid>; Path=/; HttpOnly; SameSite=lax
|
|
203
|
+
|
|
204
|
+
# 4. The dashboard responds.
|
|
205
|
+
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/polishd
|
|
206
|
+
# expect: 200
|
|
207
|
+
|
|
208
|
+
# 5. Ingest accepts a batch (reuse the cookie from step 3).
|
|
209
|
+
curl -s -X POST http://localhost:3000/api/polishd \
|
|
210
|
+
-H 'content-type: application/json' \
|
|
211
|
+
-H 'cookie: polishd_session=<uuid>' \
|
|
212
|
+
-d '{"events":[{"type":"page_view","ts":1,"path":"/"}]}'
|
|
213
|
+
# expect: {"ok":true,"stored":1}
|
|
214
|
+
|
|
215
|
+
# 6. Styling is present — the stylesheet must contain dashboard utilities.
|
|
216
|
+
# Fetch the dashboard's CSS chunk and grep for `tabular-nums`.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
In development the store writes `.polishd/analytics.db` (SQLite). Its presence
|
|
220
|
+
after step 5 confirms the write path end to end.
|
|
221
|
+
|
|
222
|
+
## Phase 8 — Production
|
|
223
|
+
|
|
224
|
+
Capture only persists in production once a Postgres URL is set — serverless
|
|
225
|
+
hosts have read-only, ephemeral filesystems, so the dev SQLite file cannot
|
|
226
|
+
survive there.
|
|
227
|
+
|
|
228
|
+
| Variable | Required | Purpose |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `POLISHD_DATABASE_URL` | for production capture | Pooled Postgres connection string |
|
|
231
|
+
| `POLISHD_DB_PATH` | no | Custom SQLite path in dev |
|
|
232
|
+
| `POLISHD_AI_*` | no | Preset AI provider/model/key instead of using the dashboard |
|
|
233
|
+
| `POLISHD_GITHUB_*` | no | Repo + token for filing issues from findings |
|
|
234
|
+
|
|
235
|
+
Set the host's Node version to 22.x or later. See [DATABASE.md](./DATABASE.md)
|
|
236
|
+
for provisioning and schema.
|
|
237
|
+
|
|
238
|
+
Without a database URL the package degrades to a safe no-op: a console warning,
|
|
239
|
+
a notice on the dashboard, and no impact on the host app.
|
|
240
|
+
|
|
241
|
+
## Troubleshooting
|
|
242
|
+
|
|
243
|
+
| Symptom | Cause | Fix |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| *"can't recognize the exported `runtime` field"* | Route segment config re-exported | Declare `runtime`/`dynamic` inline (Phase 3b) |
|
|
246
|
+
| Every `/api/*` route 404s | Proxy matcher matches `/api` | Restore the `api` exclusion in the matcher |
|
|
247
|
+
| Dashboard renders unstyled | Tailwind isn't scanning `dist` | Phase 4 |
|
|
248
|
+
| Dashboard is empty, warning about the store | No writable backend | Set `POLISHD_DATABASE_URL`, or check Node ≥ 22.5 in dev |
|
|
249
|
+
| `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite` | Node < 22.5 | Upgrade Node, or configure Postgres |
|
|
250
|
+
| No `polishd_session` cookie | Proxy file missing/misnamed, or matcher excludes the path | Check `proxy.ts` (Next 16) vs `middleware.ts` (Next 15) |
|
|
251
|
+
| Database drivers in the client bundle | `@polishd/next/server` imported client-side | Import from `@polishd/next/client` |
|
|
252
|
+
|
|
253
|
+
## Entry points
|
|
254
|
+
|
|
255
|
+
| Import | Contents | Environment |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `@polishd/next` | config, `definePolishdConfig`, event types | isomorphic |
|
|
258
|
+
| `@polishd/next/client` | `initPolishd`, `PolishdMonitor` | browser |
|
|
259
|
+
| `@polishd/next/server` | store, ingest, queries, `withPolishdSession` | Node only |
|
|
260
|
+
| `@polishd/next/route` | `POST`, `createPolishdRoute` | Node only |
|
|
261
|
+
| `@polishd/next/proxy` | `proxy`, `config`, `withPolishdSession`, `polishdMatcher` | Edge-safe |
|
|
262
|
+
| `@polishd/next/dashboard` | `createPolishdPage`, `PolishdDashboard` | Node only |
|
package/DATABASE.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Polishd — Database setup
|
|
2
|
+
|
|
3
|
+
Polishd captures a high-frequency stream of behavioral events. In **local
|
|
4
|
+
development** it writes them to a SQLite file with zero configuration. In
|
|
5
|
+
**production** you must point it at a writable, network-accessible database,
|
|
6
|
+
because most modern hosts (Vercel, Netlify, Cloudflare) run your app on a
|
|
7
|
+
**read-only, ephemeral filesystem** — a local SQLite file there either fails to
|
|
8
|
+
open or is wiped on every cold start.
|
|
9
|
+
|
|
10
|
+
Crucially, Polishd events must **not** go through the same GitHub-commit storage
|
|
11
|
+
the notes content uses: a commit (and redeploy) per event would be catastrophic.
|
|
12
|
+
Analytics needs a real database.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## How the store decides what to use
|
|
17
|
+
|
|
18
|
+
`src/server/store.ts` opens its backend lazily and picks it in this
|
|
19
|
+
order:
|
|
20
|
+
|
|
21
|
+
1. **`POLISHD_DATABASE_URL`** is set → use that Postgres database (production).
|
|
22
|
+
2. Otherwise → use SQLite at **`POLISHD_DB_PATH`** (defaults to
|
|
23
|
+
`.polishd/analytics.db`), good for local dev.
|
|
24
|
+
3. If neither can be opened (read-only FS, bad URL) → the store **latches to a
|
|
25
|
+
safe no-op**: capture silently drops, the `/polishd` dashboard shows a notice,
|
|
26
|
+
and the host app is never taken down.
|
|
27
|
+
|
|
28
|
+
> Both backends sit behind one async interface (`storeReady`, `insertEvents`,
|
|
29
|
+
> the `query` helper), so no caller knows which is live. Aggregation SQL is
|
|
30
|
+
> written in a portable subset — `?` placeholders (rewritten to `$1, $2, …` for
|
|
31
|
+
> Postgres) and `SUM(CASE WHEN … THEN 1 ELSE 0 END)` rather than SQLite's
|
|
32
|
+
> `SUM(x = y)` — so the same queries run on either backend.
|
|
33
|
+
>
|
|
34
|
+
> The Postgres backend uses the [`pg`](https://www.npmjs.com/package/pg) driver
|
|
35
|
+
> and requires the Node.js runtime (the ingest route and dashboard already pin
|
|
36
|
+
> `runtime = "nodejs"`). It connects over TLS with `rejectUnauthorized: false`,
|
|
37
|
+
> which every managed host (Neon, Supabase, RDS) needs.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Local development (default — nothing to do)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Events land in `.polishd/analytics.db` (gitignored). Inspect them directly:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
node -e "const {DatabaseSync}=require('node:sqlite'); \
|
|
51
|
+
const db=new DatabaseSync('.polishd/analytics.db'); \
|
|
52
|
+
console.log(db.prepare('SELECT type, count(*) c FROM events GROUP BY type').all())"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To use a custom path (e.g. a tmpfs-backed location), set `POLISHD_DB_PATH`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Production — Option A: Vercel Postgres / Neon (recommended)
|
|
60
|
+
|
|
61
|
+
Serverless Postgres with connection pooling, which matters because each
|
|
62
|
+
serverless invocation opens its own connection.
|
|
63
|
+
|
|
64
|
+
1. Create a database: **Vercel Dashboard → Storage → Create → Postgres**
|
|
65
|
+
(Vercel provisions a [Neon](https://neon.tech) database), or sign up at
|
|
66
|
+
neon.tech directly.
|
|
67
|
+
2. Copy the **pooled** connection string (host contains `-pooler`).
|
|
68
|
+
3. Add it to your environment:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Vercel: Project → Settings → Environment Variables
|
|
72
|
+
POLISHD_DATABASE_URL=postgres://user:password@ep-xxx-pooler.region.aws.neon.tech/polishd?sslmode=require
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
4. Redeploy. The store creates the `events` table on first write.
|
|
76
|
+
|
|
77
|
+
### Schema (created automatically; shown for reference)
|
|
78
|
+
|
|
79
|
+
```sql
|
|
80
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
81
|
+
id BIGSERIAL PRIMARY KEY,
|
|
82
|
+
session_id TEXT NOT NULL,
|
|
83
|
+
type TEXT NOT NULL,
|
|
84
|
+
ts BIGINT NOT NULL,
|
|
85
|
+
path TEXT NOT NULL,
|
|
86
|
+
selector TEXT,
|
|
87
|
+
component TEXT,
|
|
88
|
+
text TEXT,
|
|
89
|
+
value DOUBLE PRECISION,
|
|
90
|
+
meta JSONB,
|
|
91
|
+
received_at BIGINT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_events_path ON events(path);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Production — Option B: Turso (edge SQLite) — not yet wired up
|
|
101
|
+
|
|
102
|
+
> **Not implemented.** `POLISHD_DATABASE_URL` currently selects the **Postgres**
|
|
103
|
+
> backend only (via the `pg` driver). A `libsql://` URL will fail to connect and
|
|
104
|
+
> the store will latch to no-op. The libSQL adapter is a follow-up; until then,
|
|
105
|
+
> use Option A. The interface is backend-agnostic, so adding it is isolated to
|
|
106
|
+
> `openPostgres`'s sibling in `store.ts`.
|
|
107
|
+
|
|
108
|
+
If you prefer to keep SQLite semantics at the edge, the plan is to use
|
|
109
|
+
[Turso](https://turso.tech) (libSQL):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
turso db create polishd
|
|
113
|
+
turso db show polishd --url # → POLISHD_DATABASE_URL (libsql://...)
|
|
114
|
+
turso db tokens create polishd # → POLISHD_DATABASE_AUTH_TOKEN
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The same `events` schema applies (SQLite types, as in `store.ts`).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Environment variable reference
|
|
122
|
+
|
|
123
|
+
| Variable | Required | Purpose |
|
|
124
|
+
| ---------------------------- | ----------------- | ------------------------------------------------------------- |
|
|
125
|
+
| `POLISHD_DATABASE_URL` | Production | Postgres or libSQL connection string. Presence selects it. |
|
|
126
|
+
| `POLISHD_DATABASE_AUTH_TOKEN` | Turso only | Auth token for the libSQL database. |
|
|
127
|
+
| `POLISHD_DB_PATH` | No | Custom local SQLite path (dev). Default `.polishd/analytics.db`. |
|
|
128
|
+
|
|
129
|
+
No secrets ever live in `polishd.config.ts` — that file is imported by the
|
|
130
|
+
browser. Connection strings and tokens are read from `process.env` on the
|
|
131
|
+
server only.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Data retention & privacy
|
|
136
|
+
|
|
137
|
+
- Events carry **no PII**: sessions are random UUIDs (no fingerprinting), and
|
|
138
|
+
only element *labels* are stored — never user-typed input.
|
|
139
|
+
- Prune old rows on whatever cadence you like, e.g. a scheduled job:
|
|
140
|
+
|
|
141
|
+
```sql
|
|
142
|
+
DELETE FROM events WHERE received_at < (extract(epoch from now()) - 60*60*24*90) * 1000;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
(90-day retention; `received_at` is milliseconds since epoch.)
|
|
146
|
+
- The capture layer honors the browser's **Do Not Track** signal and the
|
|
147
|
+
`sampleRate` config, so you can dial volume down without code changes.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muskan Vaswan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|