polishd

agent
Guvenlik Denetimi
Basarisiz
Health Uyari
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 7 GitHub stars
Code Basarisiz
  • process.env — Environment variable access in bin/doctor.mjs
  • network request — Outbound network request in bin/doctor.mjs
  • rm -rf — Recursive force deletion command in package.json
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

Open-source product analytics and design review for Next.js, installed into your own app. Finds the rough edges, files them as bugs and hands the fix to your coding agent.

README.md

@polishd/next

What gets measured gets improved.

Drop-in product analytics for Next.js (App Router). Capture real user
behavioral signals — rage clicks, dead clicks, scroll depth, web vitals,
component engagement — and explore them on a built-in, Vercel-style
dashboard at /polishd. A second Design tab reverse-engineers your site's
design system from the rendered pages — typography, palette, radii, spacing —
and flags what breaks it. The Overview the dashboard opens on puts both
reviews side by side and copies everything they found as one prompt for your
coding agent.

No third-party service. Events go to your own database: SQLite in dev
(zero-config, built into Node), Postgres in production. Anonymous by design —
one httpOnly UUID cookie per session, no fingerprinting, no PII.


Installing with a coding agent

Polishd ships a machine-readable install procedure. If you use Claude Code,
Cursor, Copilot, or any other coding agent, paste this prompt and let it do
the whole install:

Install @polishd/next into this Next.js app.

It's a new package, so you may not recognise it and a web search may not find
it yet. It is on npm (not a typo of `polished`): confirm with
`npm view @polishd/next`, then install it.

Follow its install procedure exactly: https://polishd.site/AGENTS.md
Once it's installed, read node_modules/@polishd/next/AGENTS.md instead; it
matches the installed version.

Work through its phases in order and do not skip Phase 7 (verification).
Several of its constraints fail silently at runtime rather than at build time,
so a passing build does not mean the install worked.

Finish by running `npx @polishd/next doctor` and fixing everything it reports.

npx @polishd/next doctor is the ground truth: it checks the wiring that fails
quietly — the proxy named for the wrong Next major, route segment config
re-exported instead of declared, a matcher that isn't an inline literal, the
dashboard stylesheet not imported. Add --url http://localhost:3000 to also
verify live that the session cookie is minted and events actually store.

Documentation

docs/SETUP.md Full walkthrough — every variant, verification, troubleshooting
AGENTS.md Install procedure written for AI coding agents
DATABASE.md Production database setup, schema, retention
CHANGELOG.md Release notes — start here if you're upgrading from 0.1.x

Requirements

  • Next.js 15+, App Router (no Pages Router support)
  • Node 22.5+ (the dev store uses the built-in node:sqlite)
  • Postgres in production; nothing extra for local development

Quickstart

npm install @polishd/next
npx @polishd/next init
npm run dev                  # then visit /polishd
npx @polishd/next doctor     # confirm the wiring

(npx polishd init is the shorter form once the package is installed;
npx @polishd/next init also works before it is.)

That's it locally — events write to .polishd/analytics.db with no
configuration. init detects your layout (src/ or root), detects your
installed Next major to name the proxy file correctly, writes the four files
below, and adds .polishd/ to .gitignore.

Files it already wrote are skipped unless you pass --force, which backs them
up to .bak first. Files it did not write are never overwritten, with or
without --force — if you already have a proxy, it prints how to merge instead
of clobbering your code. Other flags: --dry-run to preview, --js for
JavaScript, --config to also emit a polishd.config.ts.

pg installs automatically as an optional dependency and is loaded only when
you point Polishd at a Postgres URL.

What gets wired up

// src/proxy.ts        (middleware.ts on Next 15) — sets the session cookie
import type { NextRequest } from "next/server";
import { withPolishdSession } from "@polishd/next/proxy";

export function proxy(request: NextRequest) {
  return withPolishdSession(request);
}
// Must be an inline literal — Next can't import config.matcher.
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
};

// src/instrumentation-client.ts — starts capture before hydration
import { initPolishd } from "@polishd/next/client";
initPolishd();

// src/app/api/polishd/route.ts — the ingest endpoint
import { createPolishdRoute } from "@polishd/next/route";
export const runtime = "nodejs";        // must be declared here, not re-exported
export const dynamic = "force-dynamic";
export const POST = createPolishdRoute();

// src/app/polishd/page.tsx — the dashboard
import "@polishd/next/dashboard.css";   // self-contained; no Tailwind needed
import { createPolishdPage } from "@polishd/next/dashboard";
export const runtime = "nodejs";       // Next requires these inline in the page
export const dynamic = "force-dynamic";
export default createPolishdPage();    // see Protecting the dashboard

Route segment config can't be re-exported. Next statically parses
runtime and dynamic, so export { POST, runtime, dynamic } from "@polishd/next/route" fails the build — declare them inline as above. The same
applies to the dashboard page.

Get the matcher right. Next statically parses config.matcher, so it must
be an inline literal in your proxy file — it can't be imported. npx @polishd/next init writes the correct one for you. Excluding all of /api is load-bearing:
under Next 16 + Turbopack, a proxy matcher that touches any /api/* route
breaks resolution for the entire /api segment.

Composing with an existing proxy

Next allows exactly one proxy module per app (proxy.ts on Next 16+,
middleware.ts on Next 15), so if you already have one, Polishd has to go
inside it. init detects this and prints instructions rather than overwriting.

The subtle part is the matcher. One file means one config.matcher, so adding
Polishd means widening it to the union — and Polishd's matcher is near-total.
Your handler, which you deliberately scoped to a couple of routes, would
silently start running on every request in the app. Nothing errors; your
redirects and rewrites just begin firing where they never did before.

Declare your original scope and composePolishd re-applies it as an internal
guard:

// proxy.ts        (middleware.ts on Next 15, with the function named `middleware`)
import { composePolishd } from "@polishd/next/proxy";
import type { NextRequest } from "next/server";

function myProxy(request: NextRequest) {
  if (!hasSession(request)) return NextResponse.redirect(new URL("/login", request.url));
}

// Your handler still only runs on /admin/*, even though the matcher below
// now feeds Polishd every page request.
export const proxy = composePolishd(myProxy, { matcher: "/admin/:path*" });

// Written by hand: Next statically parses this and it must be an inline
// literal, so no helper can generate it. The union of your paths and Polishd's.
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon|assets).*)"],
};

Your handler runs first and its response is threaded through, so a redirect or
rewrite you return carries the analytics cookie rather than losing it.
Returning nothing is fine. matcher accepts Next's matcher strings
("/admin/:path*", "/((?!api).*)") or a RegExp when you want to be exact;
omit it only if your handler genuinely did run on everything.

Widening the matcher is safe from Polishd's side: withPolishdSession returns
early once the cookie is present, so the extra paths cost a cookie lookup and
nothing else.

Styling

The dashboard ships its own stylesheet and loads it itself, so it always
renders in the Polishd palette, whatever your Tailwind preset defines. init
still writes the import into the page, which is harmless and keeps the
dependency visible:

// app/polishd/page.tsx
import "@polishd/next/dashboard.css";

Tailwind is not required in your app. The stylesheet is compiled at publish
time, so it doesn't depend on your build setup, your Tailwind version, or
whether you use Tailwind at all.

It is built not to interfere with the app it lands in:

  • No global preflight. Tailwind's reset is what would restyle every element
    on your page; it's left out. A scoped equivalent applies to the dashboard's
    own subtree instead, so your headings, lists and buttons are untouched.
  • Every rule is scoped to .polishd-root, verified at build time — nothing
    in the file can match your markup.
  • The scope is doubled (.polishd-root.polishd-root) so the dashboard wins
    specificity ties against a host that happens to have its own .flex,
    .hidden or .container, rather than the winner depending on stylesheet
    order.
  • No cascade layers, because unlayered host CSS beats layered CSS outright —
    a plain h1 { color: … } in your app would otherwise override the dashboard's
    own typography.

The one thing it can't win is a host !important rule aimed at the same
property. Nothing short of !important on our side would, and a package
shouting over its host is worse than a rare collision.

Upgrading from 0.1.x? Nothing to do — your existing @source "…/@polishd/next/dist" keeps compiling the dashboard's utilities exactly as
before, so the dashboard stays styled whether or not you add the import.

To move over at your own pace, add the import first, then delete the
@source line — in that order, so the dashboard is never unstyled in between.
polishd doctor reports which setup it finds and won't call a working 0.1.x
install broken.

Protecting the dashboard

The dashboard is not a read-only report. Its settings change the configured
model and API base URL, so anyone who can reach it can point your requests
at their own endpoint and spend your key. Treat it as a credential-adjacent
surface.

In development it is open and says so once in the console. In
production Polishd refuses to serve it until you have made a decision.
There are three ways to make one.

1. A token — no code, works with no auth of your own

POLISHD_DASHBOARD_TOKEN=$(openssl rand -hex 32)

That is the whole integration. The locked screen shows a password field; the
token is exchanged for an httpOnly, path-scoped cookie by a server action, so
it never appears in a URL, in browser history, in server logs, or in a
Referer header. The cookie holds an HMAC of the token rather than the token
itself, comparison is constant-time, expiry is enforced server-side, and the
unlock form is rate-limited per IP.

Tune it if you moved the dashboard off /polishd:

import { createPolishdPage, polishdTokenAuth } from "@polishd/next/dashboard";

export default createPolishdPage({
  authenticate: polishdTokenAuth({ basePath: "/admin/polishd" }),
});

2. Your app's own auth

import { createPolishdPage } from "@polishd/next/dashboard";
import { isAuthenticated } from "@/lib/auth";
import { MyLogin } from "./login";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export default createPolishdPage({
  authenticate: isAuthenticated,   // (ctx) => boolean | Promise<boolean>
  unauthorized: <MyLogin />,       // optional; a minimal screen otherwise
});

authenticate receives the request's cookies and headers, so it is an
ordinary function of its inputs and can be unit tested without a live request:

const authenticate = (ctx) => ctx.cookies.get("session")?.value === expected;

A zero-argument callback still works unchanged.

3. Public on purpose

POLISHD_DASHBOARD_PUBLIC=true

An explicit, grep-able opt-in. You will still get one console warning per boot,
in production, because that is the only place it is worth acting on.

The gate covers the server actions, not just the page. The AI actions
(generate summary, save settings, scan codebase, file GitHub issues) are
independently addressable POST endpoints whose ids ship in the public client
bundle, so gating only the page would leave them callable by anyone. They
re-run your check against the caller's cookies on every call, and deny when
no dashboard page has registered a policy — including when you delete
app/polishd/page.tsx but keep the package, in which case the actions stay
routable and always deny.

Overview

The dashboard opens on the Overview, laid out as a board of the site:

  • The site and its brand, in one card with no headings. On the left, the
    newest snapshot as a small bento of the site's top pages:
    the homepage hero largest, the homepage on a phone beside it, and the two
    most visited other pages under them, with no device or theme toggles in
    front. A site with fewer pages lends their other renderings to the
    remaining tiles. A click opens the full-height shot, and the rest of the
    capture is one link away on the Design tab. On the right, the system the pages
    actually ship, curated the way the brand kit is (one-offs left out), as two
    hierarchies. The palette: the color the site paints its buttons and calls
    to action in as Primary — a white site's white, a blue site's blue, never a
    status red — the page itself as Secondary, the next distinct color as
    Accent, then the next six most used as one scale from light to dark; not
    every color the site paints. The type: "Title",
    "Subtitle", "Body" and "Caption", each word set in its own face at the size
    the site really uses that style.
  • Overview — one paragraph, written by the model from the
    AI summary and the design review: the
    overall picture, the one or two things most worth fixing, and what to
    protect, saying nothing the two reviews don't. It's cached, goes stale when
    either review changes, and is rewritten when the Overview opens on a
    missing or stale paragraph; one refresh regenerates both reviews and then
    the paragraph. The problems both reviews found are on the
    Actionables tab, with the file-bug and ignore
    controls that act on them.
  • Visitors — sessions, page views and the share on mobile, stacked in a
    narrow column beside the paragraph, each tile opening its chart over time.

Copy fix prompt turns every problem the two reviews found into one message
for the coding agent working in your repo — Claude Code, Cursor, or anything
else that reads a prompt. A prompt carries:

  • the site description and audience from Settings, and the traffic so far;
  • a warning up front when the two reviews are far apart in time (two weeks or
    more), when one is a month old, or when the design measurements have changed
    since the review. A review that's out of date for another reason (a
    dismissal, a settings change) gets a neutral note, never a claim of new
    data;
  • each review's summary, and what's working, so the agent doesn't regress it;
  • every problem, labelled A1, D1…, with the evidence it was found from,
    the source file it was matched to, the suggested fix, and the GitHub issue
    already filed for it. An analytics problem also carries the volume behind it
    when the summary recorded one (23 dead clicks across 9 sessions on
    /pricing
    ), and handles to search the source for, read off its citation:
    a data-component name, the element's visible text, its meaningful class
    names, or the route's page file. A design problem names its values in px
    and rem, says which values its fix would introduce, and says when Polishd
    withheld the model's fix (see Design review);
  • one line per kind of finding on how it was detected and what that detector
    can't see. A dead click, for example, is a click on something with no
    interactive tag or role, so a div with a React onClick counts as one; the
    agent is told to check for a handler first;
  • instructions to find the responsible code before changing anything, skip a
    finding the code shows isn't real, keep each change scoped, and finish with
    a JSON verdict block: fixed, not_real or deliberate per problem, with a
    reason and the files involved.

The prompt is built in the browser from the latest summary and review the
page has, so a review you've just regenerated is already in it. It sends
nothing anywhere: it's text on your clipboard.

Paste verdicts reads the agent's verdict block back. Paste its reply (or
just the block) and each verdict is shown next to the finding it refers to.
Findings it marked not_real or deliberate are then ignored with its
reasons, exactly as if you had clicked Ignore on each card, so the next
review doesn't report them again. Ids are matched against the prompt as you
last copied it, since ignoring or regenerating in between renumbers the
problems, and a verdict only counts for a finding still in the current
summary or review. The paste is treated as untrusted: only the id, verdict,
reason and files are read, the reason is flattened to one line and clipped,
the problem's wording comes from Polishd's own copy, and later prompts quote
the reason as a note about the site, never as an instruction.

AI summary

The Analytics tab's top card (also on the Overview) turns the captured signals into a plain-English story
of how people are actually using your site, followed by a wins list (what's
working). Its losses (specific problems) are actionables: they're listed on
the Actionables tab alongside the design review's
issues, and the card ends with how many are waiting there. Losses are held to a higher
bar: each must cite the exact page, selector, or component from the data —
citations that don't appear in the analytics are discarded as hallucinations,
and the rest are matched to your source files so every loss shows the file it
lives in (or an honest "not matched to source" tag for dynamic content). It's
bring-your-own-key. The Settings tab in the sidebar walks a fresh
install through a four-step setup — connect a model (Anthropic, OpenAI, any
OpenAI-compatible endpoint, or Google), describe your site, connect GitHub and
choose what it's used for (scanning your codebase, filing issues automatically
— each its own toggle), pick a refresh cadence — and holds all of it
afterwards. The
card itself is only ever the story: the summary, when it was generated, a
refresh icon to force a regenerate, and a gear that jumps to Settings.
Nothing is sent to a model until you ask.

It's built to spend as few tokens as possible:

  • The model never sees raw events — only a compact, server-side digest of the
    already-aggregated numbers (a few hundred tokens for an entire site).
  • The digest is fingerprinted and the summary is cached. Re-opening the
    dashboard costs nothing, and a regenerate with no new data returns the cached
    text without a model call.
  • The output is capped to a single tight paragraph.

Auto-refresh. Pick a cadence (manual / daily / weekly) during setup or in
Settings. When you open the dashboard past the cadence and the data has
actually changed, the summary regenerates in the background after the page is
served — no external cron, serverless-friendly. Unchanged data never triggers
a model call, whatever the cadence.

The API key is stored server-side (in your database) and is never sent back
to the browser — the UI only ever shows whether one is set. You can also preset
everything from the environment (handy for CI / shared deploys); dashboard
settings take precedence.

Where to get a key:

Provider Get a key Notes
Anthropic (Claude) console.anthropic.com/settings/keys default model claude-opus-4-8
OpenAI platform.openai.com/api-keys default model gpt-4o-mini
OpenAI-compatible e.g. openrouter.ai/keys, console.groq.com/keys also set Base URL
Google (Gemini) aistudio.google.com/app/apikey default model gemini-2.5-flash

Project profile — the one-time setup scan

To make the summary understand your code — not just the numbers — run the
one-time Scan codebase step from Settings → Codebase. Polishd
reads your app's source (pages and layouts first, then component files, under
a hard token budget) — from disk where it's there, otherwise from the
connected GitHub repo's default branch — and asks the model to write a compact
profile: what the site is for, a map of its routes, and every interactive
component by its exact identifier. Optionally tell it your target audience
and ideology / values in Settings so the analysis judges the site by your
goals.

The profile is cached in your database and injected into every summary as
authoritative context, so:

  • Summaries never re-read source. One scan, then pure digest + profile.
  • The codebase is only touched again when analytics mention a component the
    profile doesn't cover — a tiny targeted read of just the files naming it —
    or when you explicitly re-scan (the summary card shows a hint when new
    components appear).
  • On serverless hosts, where the source isn't on disk, the scan reads the
    connected GitHub repo instead: one tree listing, then only the files the
    budget picks. In a monorepo it finds the app's folder itself — the one whose
    package.json installs @polishd/next — and Source folders are relative
    to that folder.

What the model sees. A compact, server-side digest of the aggregated
analytics (page paths, element selectors, data-component names, sample
text), the cached project profile, and the description you
provide. Your source is read only during an explicit scan (or a targeted
gap-fill), and always under strict size budgets.

Three verdicts on an actionable: file it, mark it fixed, or ignore it

Every actionable — a loss from the summary, an issue from the design review —
carries all three, on its row and in its drawer.

File bug (with GitHub connected) verifies the report against your repo's
source before anything is filed: confirmed reports become an issue with the
technical analysis and suggested fixes, and a report the code disproves comes
back as "checked the source — not an actual bug" with the reasoning, filing
nothing. The issue is linked on the actionable from the moment it's filed, and
on the next load the row shows the state your tracker gives it.

A design issue filed this way carries where it renders: for each value it
cites, how much of the UI the value covers, on which pages, how it was declared
(and whether it was hand-typed as a Tailwind arbitrary value), and — once a
site snapshot has pictured the flagged value — the picture of
the element carrying it, embedded in the issue body when the image lives in a
public Vercel Blob store that GitHub can fetch. A picture in a private store or
on local disk is named in the body rather than embedded. The dashboard shows
the same pictures on the actionable itself.

Mark fixed says the opposite of a bug report: done, the site changed here.
It works exactly like ignoring — the finding leaves every later review and the
dismissal is replayed to the model — except that the model is told it was
fixed, and on which day, so the data it still sees from before the fix reads
as history rather than a live problem. A note on what changed is optional,
asked for in the same dialog.

Ignore is the other answer: this isn't a problem, stop showing it to me. It
asks once, in a dialog with an optional reason, and happens on its second
Ignore. The finding drops out of every later review — including the cached
one, immediately — and the model is told about the dismissal on every
subsequent call, so it doesn't spend a slot rediscovering it. Dismissing one
finding doesn't shrink the list; the next real problem takes its place.

A reason is optional and offered after the fact — "the second CTA is
deliberate"
, "that page is staff-only". It's worth adding: reasons are
replayed to the model verbatim as context it can't get from the numbers, which
is what stops the same finding coming back in different words. You can edit a
reason later, and Undo puts the loss back where the model can report it
again.

Ignoring a loss marks the summary stale, exactly like new analytics does, so
the next refresh actually re-asks the model with the dismissal in hand.

The Actionables tab

The Actionables tab is the one list of everything worth fixing: the
summary's losses, the design review's issues, and every issue polishd has
filed for any of them, whether you clicked File bug yourself or auto-filing
did it for you. What still needs a decision comes first — losses, then design
issues, each in its review's order — then what's open in the tracker, newest
first. Issues closed on GitHub, and ones GitHub no longer returns (deleted,
transferred, or beyond what the token covers), leave it for a separate
Closed list below, collapsed until opened, rows and drawer intact. A filed issue whose finding is no longer in the
current summary or review (fixed, dismissed, or re-worded away) stays listed on
its own, so filing is never the last you hear of it. The tab is always there;
without a GitHub connection it lists the findings and lets them be ignored, and
says that filing needs a repository.

Each row is the finding, the icon of the tab it came from, and one status tag
— fix suggested, verifying…, filed, in progress, closed, not
planned
, not a bug, unreadable — with the issue it's tracked in linked
beneath. Hovering a row brings up File bug, Mark fixed and Ignore;
the last two each ask once, with an optional note. Everything else is in the drawer a click opens:
the evidence — the selector, component or path the analytics cited, or the
color, size or page the design review cited — the volume behind it, the source
file it was matched to, the suggested fix, the pictures of where a design value
renders, and for a filed issue the title, state, labels, assignees and comment
count read live from GitHub on every render (so a bug someone closed an hour
ago reads as closed here), the issue body rendered as the markdown it is, and —
where no coding agent can be asked to fix it (none set up, dispatch paused, or a
workflow that only hears new issues) and no pull request already carries a fix —
Suggest a fix, a plan from polishd's own model; where one can, Create fix
takes its place. An issue that can no longer be read — deleted, transferred, or
beyond what the token now covers — keeps its row and says exactly that.
?tab=issues, the tab's old name, still lands here.

Opening the tab costs a single GitHub API call in the ordinary case, not one
per issue: the repo's issue list hands back a hundred whole issues at a time,
which is more than polishd is ever likely to have filed.

Connect a coding agent

Filing an issue hands the problem to your tracker; a coding agent can take it
from there. The Claude Code GitHub Action answers an issue that mentions it,
and the Copilot coding agent answers one it's assigned. polishd hands filed
issues to whichever of those the repo already runs, follows the work to a pull
request linked to the issue, and shows what happens on the issue on the
Actionables tab — whoever does it.

Filing and fixing are two steps. Issues polishd files never carry the
trigger phrase, so filing alone never starts a run. On the Actionables tab a
filed, open issue offers Create fix: polishd labels it (polishd-fix by
default) and posts a comment that opens with the trigger phrase and carries
the brief — or, for Copilot, posts the brief and assigns the issue. The brief
asks the agent to work in four steps and stop at the first that fails: scope
it
(find the code that renders the evidence), confirm it's real (with
the blind spot of the detector that found it — a dead click can't see React
onClick handlers, say), check it's feasible as one focused pull request,
and only then fix it. The pull request's first line is Fixes #N, so
GitHub links it to the issue and closes the issue on merge, and the agent ends
with a small JSON verdict — fixed, not_real, deliberate, infeasible or
needs_info, with a reason — that polishd reads back onto the row.

That takes a workflow that hears comments (issue_comment), which the stock
one does. A workflow that hears new issues only can be reached just once, as
the issue is filed, so there File bug comes with File and fix, which puts
the brief in the issue body. A workflow in automation mode (a prompt input)
runs on every new issue regardless, so every filing is a fix starting, and the
tab says so.

Following it to a pull request. The agent works on GitHub whether or not
the dashboard is open; polishd reads where it got to when you open the tab or
the issue. The Action pushes a branch and leaves a "Create PR" link rather than
opening a pull request, so once that link is in its comment the actionable
offers Create PR in place of Create fix: it opens the pull request from that
branch with Fixes #N first, so it's linked to the issue. When an agent opened
its own pull request without the closing keyword, the button reads Link PR
and adds it. polishd writes nothing to a pull request without that click. While
the agent is still working and the tab is open, it reads again every
half-minute so the button shows up without a reload.

What happened on the issue, whoever did it, comes from one GraphQL query
per forty filed issues: the pull requests that reference each one — linked or
merely mentioning it, opened by the agent, a teammate or polishd — with their
state, and the issue's recent comments, labels, assignments and closes. Rows
show where a fix stands (fix requested, agent working, PR open, fix
merged
, agent: not real…) and the drawer shows the pull requests, the
agent's verdict and the activity.

Send fixes automatically (off by default) sends each issue the source
confirmed as soon as it's filed; unverified filings wait for you. A daily cap
(5) counts every fix sent, by hand or not, and Pause dispatch stops them
all. No new token permissions are needed: Issues and Pull requests (write)
already cover the label, the comment, the pull request and the edit.

What follows is the Fix dispatch card at the bottom of the Settings tab,
shown once a repository is connected, that says what the repo has and how a
fix starts there, and proves it works.

What polishd found. The card reads every workflow under
.github/workflows/ on the default branch (with the same token, no extra
permissions) and reports: which agent is there — a workflow using
anthropics/claude-code-action, or Copilot's agent if it's assignable, or
neither; how it fires — a workflow that listens for comments lets a fix start
on an issue already filed, with Create fix (the stock workflow listens for both
comments and new issues, and polishd prefers the comment), one that listens for
new issues only means the phrase has to go in the body at filing; the trigger phrase (from a trigger_phrase
input or the job's if: condition, @claude otherwise); the billing path —
whether the step passes claude_code_oauth_token (runs draw on your Claude
plan's usage limits) or anthropic_api_key (metered API usage, covered by a
Max or Team plan's monthly credit when linked), read from which input is
named, never from a value; and whether the token's user has write access,
which the Action requires of whoever triggers it. It warns when a prompt
input puts the workflow in automation mode (every issue starts a run), and when
the workflow sits in an unmerged pull request, since GitHub runs issue-event
workflows only from the default branch. Two things can't be read with any
token polishd holds: whether the Claude GitHub App is installed and whether
the secret exists.

Live test. That's what Send test issue is for. It files an issue titled
"polishd dispatch test" the way a dispatch would — the phrase in the body or
in a comment after, per the workflow; an assignment, for Copilot — waits up to
three minutes for the agent to answer, then closes the issue with a note. A
reply proves the App and the secret; no reply lists the likely causes in order
(App not installed, secret missing, workflow on another branch, no write
access). The issue number is stored the moment it exists, so Check again
picks the test back up after the page is gone, and a test issue is never left
open silently.

Setting one up when nothing is found, least effort first: if you use
Claude Code, claude in a terminal in the repo, then /install-github-app there (it only
runs in the Claude Code terminal), does the App, the
secret and a workflow pull request in one go. Otherwise three links: install
the Claude GitHub App; add
the repository secret — a radio picks between a subscription token
(CLAUDE_CODE_OAUTH_TOKEN, made with claude setup-token, uses your plan's
limits) and an API key (ANTHROPIC_API_KEY from the Console, metered,
covered by a Max or Team monthly credit when linked), and the card names the
secret to create; then add the workflow through a link that opens GitHub's
new-file editor with polishd's recommended claude.yml already in it — the
stock example, with the secret line matching your choice and a --max-turns
cap — plus a copy button. A third row points at the repository's Copilot
setting. polishd could open that workflow pull request itself if its token
were granted Contents (write) and Workflows (write); it doesn't today.

Your choices are the only editable fields, saved with the rest of the
settings: send fixes automatically (off by default), the label put on
dispatched issues (polishd-fix), a daily cap (5), and a pause switch. Until
the card says Ready, the Actionables tab carries a one-line pointer to it
instead of Create fix.

Design review

The dashboard's Design tab (in the sidebar) is your site's brand guideline
reverse-engineered from what actually shipped. Every page measures its own
rendered design once per visitor session and build — the computed
typography, colors, corner radii and padding, reduced to a compact tally of a
few KB — and the tab assembles the site-wide picture:

  • Typography — every font family / size / weight combination in use,
    rendered as specimens with real text from your pages.
  • Color palette — swatches weighted by how much of the UI each color
    covers, split by role (text / background / border).
  • Contrast — each text-on-background pairing checked against WCAG AA.
  • Corner radii & spacing — the rounding and padding scales in use.

The first layer of review is deterministic — rule-based checks, no model:
too many font sizes to be a type scale, extra font families, near-duplicate
colors (#fefefe living next to #ffffff), contrast failures with the exact
ratio, one-off values that appear on a single page, padding off the 4px grid,
and Tailwind arbitrary values (text-[#555], p-[13px]) standing in for a
theme.

The checks count decisions, not copies. Each value carries both its uses
(elements rendering it) and its declarations (the distinct class strings
behind them), so a badge class repeated down a 38-row table is one decision
and a shared button-class constant is one decision. A one-off is a value
that's rare in what the page renders, on one page, and declared once; a
value chosen in two places is a pattern. On a Tailwind site, its spacing scale's half steps
(2px, 6px, 10px, 14px) count as on-grid. Loading skeletons, aria-busy
regions and closed overlays are left out of the scan, which waits a few
seconds for them to clear first.

The second layer is the Aesthetic review card: the same model you
configured in Settings reads the measured tokens and findings and judges the
system the site already has, on its own terms. Values that recur are treated
as decisions, even when they don't follow a textbook scale; a problem is a
value that breaks the pattern, or a measured failure such as low contrast. It
raises at most three issues, and none when the system is coherent. Each must
cite a token that actually appears in the metrics (a hex value, a px size, a
page path), and its fix should move the outlier onto a value the site already
uses.

The model also sees the pages. Generating a review first runs a small
capture of its own, through the same machinery as site snapshots
but separate from the gallery: the first screen of the three most-visited
pages (ranked like the Top pages table: distinct sessions, then page views),
desktop, in light and dark — a site with no dark mode drops the dark shots —
plus each flagged value pictured in place: the element carrying it, ringed,
with its surroundings, from the light rendering only. Failing contrast pairs
are looked for first, then one-off type styles, colors and radii in turn, and
off-grid padding only to fill the last of at most six pictures. The shots are
context, not evidence: an issue still has to cite a measured token, and the
model is told they're scaled down. The card says which pages and how many
values were shown, and why the review went without pictures when it did. The
screenshots travel to your model provider with the metrics, on a regenerate
only; a model or endpoint that doesn't take images gets the metrics alone. In
production the capture uses Polishd's screenshot service, so the first review
there asks the same consent question the Site snapshots card does, and a
declined service or a missing Blob store means reviews from the metrics alone.

Its fixes are then checked against the review itself. A fix that clearly
removes a value the same review praised is withheld, and the card says why:
one that replaces the value with a different one of the same kind
(collapse 22px into 20px), drops it, or comes from a review that admires
22px and 28px headings and then proposes a 12/14/16/20/24px type scale. A fix
that only changes something about a praised value (its color, its
line-height, a token in place of the literal) is left alone, and nothing
else is withheld. Any value a fix introduces that the site doesn't render yet
is labelled new. Each issue is an actionable, listed with the summary's losses
on the Actionables tab: it shows where its cited
values render — how much of the UI each covers, on which pages, how it was
declared — and, once a site snapshot has pictured the flagged value, the
picture of the element in place; filing it puts the same into the GitHub
issue. Issues can be ignored like the summary's losses, with a
reason the model reads on every later review. The dismissal holds when a
regenerate rewords the issue, as long as it's the same kind (a contrast pair,
a color, or a type size, radius or padding: named outright, or read from the
measured design) citing the same values. When it's unclear, Polishd lets a
dismissed issue come back rather than risk hiding a real one: a color's role
must agree (one stated and one not is a different issue), and a contrast
failure's sizes must be the same when both state them, so dismissing it at
11px doesn't hide it failing at 16px. A rewording that only drops the size
still matches, and of two issues citing identical evidence, a dismissal hides
only the one worded closest to it. It
follows the summary's token-thrift rules: the
model sees a few hundred tokens of aggregated metrics plus a dozen small
screenshots at most, the result is cached and fingerprinted, and regenerating
over unchanged metrics is free. Refresh buttons
for both the metrics and the analysis are on the tab.

Brand kit. The Brand kit button in the tab's header downloads the
measured system as a guideline PDF: palette, passing color pairings,
typefaces, type scale, radii, spacing, and the same tokens as CSS custom
properties. It's curated rather than copied. One-off values are left out,
near-duplicate colors fold together, and every value gets a name (Red 600,
Heading 1, radius-md). The PDF is built in the browser, so specimens are
drawn in the site's own fonts.

Nothing extra to install or wire: the scan ships with the existing client,
flows through the existing ingest route, and the dashboard itself is never
scanned.

Site snapshots

The Design tab can also screenshot the site itself — every scanned route,
phone and desktop, light and dark — and render the shots as a gallery, so the
measured tokens sit next to what the pages actually look like. Each snapshot
is stamped with the design-metrics fingerprint at capture time, so a snapshot
taken before a round of design fixes can be held against one taken after. The
last 6 are kept.

Each capture also pictures the flagged values in place: for every one-off type
style, color and radius, and every off-grid padding (up to 64 values), it finds the
element carrying the value on its page and saves a ringed crop of it. Hover a
flagged value on the Design tab to see that picture and the page it's on.

Who takes the shots. It depends on where the dashboard is running:

  1. Local development (next dev, or a dashboard opened on localhost or
    a private address): a browser on your own machine. That's your installed
    Chrome or Edge, or Playwright's Chromium (npx playwright install chromium once if you have neither). Nothing leaves the machine and
    there's nothing to agree to.

  2. Production: your server doesn't run a browser. The dashboard, in your
    browser, sends each page to Polishd's screenshot service
    (shots.polishd.muskanvaswan.xyz, source in service/) one
    at a time. The service loads that page on the site the dashboard is
    running on and sends back WebP images, which appear in the gallery as they
    arrive and are then stored by your install. The first time you press
    Capture, a dialog asks you once whether snapshots may use the
    service, and says what it does and doesn't see. Allowing it starts the
    capture. The answer is kept per install; until you've been asked, the
    section stays where it is.

  3. Production without a place for images or without your agreement: the
    Site snapshots section moves to the bottom of the Design tab, turned
    off
    , and says why. The reasons are: no Blob store connected on Vercel,
    or the service wasn't allowed (with a button to review it). Some reasons
    only show up during a capture, and the message names the fix:

    • your site's CSP blocked the call;
    • the service can't reach the site, such as an intranet address or a
      preview behind Vercel's login;
    • the service is rate-limiting;
    • the service is unreachable.

    A block found during a capture is remembered: the section stays off at
    the bottom, across reloads, until "Try again" gets a capture through.

What the service does, plainly:

  • Page URLs and pixels pass through Polishd's service. It sees the paths
    being shot and renders them like any visitor would. It returns the images
    and doesn't store or log them. Its rate limiter keeps the site's hostname
    (and a hash of your IP) for an hour at most.
  • It only shoots the site that calls it. The site comes from your
    browser's Origin header; the request body just lists paths. It only
    shoots public pages
    : it refuses private, loopback, link-local and cloud
    metadata addresses, checks every request and redirect the page makes after
    DNS resolution, and keeps navigation on your origin.
  • It's rate-limited per site, per caller and in total. If you hit a limit,
    the section says which one and when to try again, and keeps the pages it
    had already shot.
  • You can withdraw the agreement any time from the section ("Turn off").
    POLISHD_SNAPSHOT_SERVICE=off stops the service from ever being called;
    snapshots are then only taken in local development.

Polishd's own dashboard pages. An install that reads dashboard telemetry
(POLISHD_ANALYTICS_SOURCE=telemetry, which is Polishd's own site) measures
the dashboard's tabs, labelled /~polishd/<tab>. Production captures never
shoot those, because the service would need your dashboard's credentials. A
capture in local development does: your own Chrome opens each tab of the
development dashboard (where it's open without a login). Run the site locally
with the production environment (vercel env pull) and the pictures upload
to the production Blob store, so the live Design tab shows them. Telemetry the
captured dashboard would send is answered locally, so it never counts as
usage.

Where the images go. On disk under .polishd/snapshots/ locally, and on
a self-hosted server with a writable disk (POLISHD_SNAPSHOT_DIR overrides).
On Vercel, connect a
Blob store to the project; that's the
whole setup. Whichever variable the connection injects switches storage to
Blob: BLOB_STORE_ID for a store connected over OIDC, or the classic
BLOB_READ_WRITE_TOKEN. The dashboard uploads each image straight to the
store with a short-lived URL your server signs for exactly that file, so
images never have to fit through a server action or a function response.
Public and private stores both work; either way the dashboard serves the
images itself, behind its own access check. BLOB_WEBHOOK_PUBLIC_KEY isn't
needed. Without a Blob store on Vercel, the section is off and says what's
missing.

No next.config changes and no extra packages are needed. If your site
sends a strict Content-Security-Policy
, allow the service and the Blob API
in connect-src:

connect-src 'self' https://shots.polishd.muskanvaswan.xyz https://vercel.com;

The section recognises a CSP block and names the directive to change. It
also tells you when a preview deployment is behind Vercel's Deployment
Protection: the service can only see the login page there, so capture from
your production domain instead.

Builds

Every captured event is stamped with the build of the site it came from —
a commit sha where one is known, otherwise whatever label the deployment
carries. That is what lets the dashboard answer "did the rage clicks start
with Tuesday's deploy?" and what tells the Design tab which scans describe the
site as it ships now.

Where the label comes from. The client bundle sends its own build with
every batch when it knows it: NEXT_PUBLIC_POLISHD_BUILD, inlined at build
time, or Vercel's NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA (set when system
environment variables are exposed, the default for new projects), or a
build passed to initPolishd(). A batch that names none is stamped by the
server with its own: POLISHD_BUILD, else the host's commit variable
(VERCEL_GIT_COMMIT_SHA, Netlify's COMMIT_REF, Cloudflare's
CF_PAGES_COMMIT_SHA, Railway, Render, Heroku), and in development the
checked-out commit from git rev-parse HEAD. The client's label is preferred
because it is exact: a tab left open across a deploy keeps posting from the
old bundle, and only the bundle knows that. On a host without a public commit
variable, set it in the build command:

NEXT_PUBLIC_POLISHD_BUILD=$COMMIT_REF next build

A label is letters, digits and . _ - / + : @, up to 80 characters; anything
else is dropped rather than guessed at. Events stored before builds were
tracked have none.

On the Analytics tab, a range control in the header scopes every section
at once — the tiles and their charts, top pages, device sizes, journeys,
elements — to the last 24 hours, 7, 30 or 90 days, all time (the
default), or This build: the one currently shipping. The range is in the
URL (?tab=analytics&range=7d), so a view is a link. Windows are measured on
the server's receive time, so a visitor with a wrong clock can't move an event
out of the week it arrived in. A Builds table lists every build events
have landed on, newest first, with its sessions, views and friction; clicking
one scopes the tab to it. A build is "new" the first time it is seen — a
lingering tab reporting from the previous build can't make that one current
again. The AI summary and its staleness are always written from the whole
history, whatever the range, so changing the range never marks it stale.

On the Design tab, scans are chosen per build. Taking each page's newest
scan is right while the site is on one build and wrong the day after a deploy:
a page nobody has revisited still contributes the old build's type and colors,
and a page the new build removed never stops contributing at all. So once
every page the current build has served has also been scanned on it, only its
scans are merged and the previous build's drop out — removed pages with them.
Until then the newest scan per page is still used, whichever build took it, and
a note under the header says how many pages have been re-scanned and which are
still waiting; open each and refresh the metrics to finish the picture. Pages
re-scan once per session per build (and on every load in development), so a
deploy is picked up as visitors return. The Scanned pages list shows the build
each scan came from, marking any carried over from the previous one.

Component-level tracking

Wrap any element to track it explicitly. Interactive controls get hover + click
attribution; pass content for articles/regions to also measure viewport time,
scroll depth, and rendered size.

import { PolishdMonitor } from "@polishd/next/client";

<PolishdMonitor name="listen-button">
  <ListenButton />
</PolishdMonitor>

<PolishdMonitor name={slug} content className="block">
  <Article />
</PolishdMonitor>

Dashboard telemetry

Polishd can report anonymous usage of its own dashboard back to the polishd
project — the same rage-click/dead-click/page-view signals it captures for
you, applied to itself, so the dashboard is improved by what it preaches.

Opt-in, and off by default. The first time you open the dashboard it asks
once; nothing is sent unless you say yes. What's shared when you do:

  • Clicks and tab views inside the dashboard only (paths are namespaced
    /~polishd/… and capture suspends the moment you navigate back to your
    site), plus one viewport-size sample per session.
  • Your site's domain — the collecting side reads it from the request's
    Origin header, so installations are named by where the dashboard actually
    runs — and an anonymous per-browser-session id. No cookies, no
    fingerprinting, no URLs, page content, or user data from your site — your
    analytics never leave your database.
  • Once per session, whether a model is connected and which provider/model
    it is — never the key, never any setting beyond those two names — and the
    polishd version you run. This is how the project learns which AI setups
    to test against, and which release the friction it sees came from.

Say no and it never asks again. Change your mind, or disable it fleet-wide,
with POLISHD_TELEMETRY=off; navigator.doNotTrack is honored regardless of
consent. The receiving end is an ordinary @polishd/next/telemetry route on
the polishd site's own installation — the same package, dogfooding itself.

On the collecting side, telemetry stays fully separated from the host's own
analytics: tagged rows are excluded from every first-party query (stats,
pages, features, journeys, the AI digest) and appear only in an Installs
dashboard tab — per-domain sessions, tab views, rage/dead clicks, last seen —
which only shows up on installations whose database actually holds telemetry.

A collector that cares about the dashboard-usage story more than its own
traffic can flip the whole thing: POLISHD_ANALYTICS_SOURCE=telemetry points
every dashboard feature — Analytics, journeys, elements, device sizes, the
Design tab (fed by the emitter's design scans of the dashboard itself), and
the AI summary — at the telemetry dataset instead. First-party events are
still captured and stored, so flipping back loses nothing, and the Analytics
tab can show them meanwhile: on a collector it has a Site / Telemetry
switch beside the range (?source=). The switch changes only that tab; the
AI summary and the other tabs keep reading what the variable picks.

Configuration

Defaults suit a low-traffic site. Override via polishd.config.ts:

// polishd.config.ts
import { definePolishdConfig } from "@polishd/next";
export default definePolishdConfig({
  sampleRate: 1,
  rageClick: { count: 3, windowMs: 500 },
});

The dashboard never measures itself: nothing on /polishd (or below it) is
captured, so reading your analytics can't show up as your site's most-used
feature. Moved the dashboard elsewhere? Set dashboardRoute in your
config, thread the config into the client and route as shown below, and also
pass it to the page — the dashboard's own queries have no other way to learn
where they're mounted:

export default createPolishdPage({ config: polishdConfig });

(POLISHD_DASHBOARD_ROUTE is the no-code alternative: ingest and the
dashboard's queries both read it.)

Nothing imports this file for you. It cannot be auto-loaded: the three
places that read config run in three different runtimes — the browser, the
Edge proxy, and the Node route handler — and the Edge one has no filesystem.
Pass it to each explicitly:

import config from "../polishd.config";

initPolishd(config);                             // instrumentation-client.ts
export const POST = createPolishdRoute(config);  // app/api/polishd/route.ts
const { proxy } = createPolishdProxy(config);    // proxy.ts

Thread it into one but not another and the layers disagree silently — a proxy
minting cookie a while ingest looks for b produces exactly the empty
dashboard described below.

For sessionCookie specifically, prefer the environment variable. It is the
one channel every runtime can read, so it cannot go half-applied:

POLISHD_SESSION_COOKIE=my_session

Environment variables

Var When Purpose
POLISHD_DATABASE_URL production Pooled Postgres connection string — enables capture
POLISHD_DB_PATH dev (optional) Custom SQLite path (default .polishd/analytics.db)
NEXT_PUBLIC_POLISHD_BUILD optional The build label stamped on every event, inlined into the client bundle at build time — a commit sha, version or date. Vercel's NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA is read when this isn't set; see Builds
POLISHD_BUILD optional The server's build, stamped on batches whose bundle names none. Hosts' own commit variables (VERCEL_GIT_COMMIT_SHA, Netlify's COMMIT_REF, Cloudflare's CF_PAGES_COMMIT_SHA, Railway, Render, Heroku) are read when this isn't set; in development it's git rev-parse HEAD
POLISHD_DASHBOARD_TOKEN production Protects /polishd with the built-in gate. Generate with openssl rand -hex 32
POLISHD_DASHBOARD_PUBLIC production (optional) true — serve the dashboard unguarded on purpose
POLISHD_SESSION_COOKIE optional Rename the anonymous session cookie. Read by both the proxy and ingest, so they cannot disagree
POLISHD_DASHBOARD_ROUTE optional Where the dashboard is mounted, if not /polishd. Read by ingest and the dashboard's queries so its own traffic is excluded; createPolishdPage({ config }) is the code-side equivalent
POLISHD_AI_PROVIDER AI (optional) anthropic | openai | openai-compatible | google
POLISHD_AI_MODEL AI (optional) Model id (defaults per provider, e.g. claude-opus-4-8)
POLISHD_AI_API_KEY AI (optional) Model API key — preset instead of using the dashboard
POLISHD_AI_BASE_URL AI (optional) Base URL for openai-compatible providers
POLISHD_AI_INSTRUCTIONS / POLISHD_AI_CONTEXT AI (optional) Default instructions / site description
POLISHD_AI_AUDIENCE / POLISHD_AI_IDEOLOGY AI (optional) Target audience / product values for the profile scan
POLISHD_AI_SOURCE_DIRS AI (optional) Comma-separated folders to scan (default src/app/components/pages/lib)
POLISHD_AI_REFRESH_CADENCE AI (optional) manual | daily | weekly — summary auto-refresh cadence
POLISHD_GITHUB_REPO GitHub (optional) Repository as owner/repo — enables filing bugs from AI-found losses
POLISHD_GITHUB_TOKEN GitHub (optional) Fine-grained PAT: Contents (read), Issues & Pull requests (write)
POLISHD_GITHUB_API GitHub (optional) REST base for a GitHub Enterprise Server, e.g. https://ghe.example.com/api/v3; defaults to https://api.github.com
POLISHD_GITHUB_AUTO_ISSUES GitHub (optional) true — file a GitHub issue automatically for each new problem a summary finds
POLISHD_SNAPSHOT_SERVICE optional off — never call Polishd's screenshot service: site snapshots are then only taken in local development, and the section is off on a public deployment. Or a URL, to use your own deployment of service/ (also in development, for testing it)
POLISHD_SNAPSHOT_DIR optional Where disk-stored snapshot images go (default .polishd/snapshots)
POLISHD_SITE_ORIGIN optional The origin snapshots are taken of, if not the one the dashboard is opened on (proxies, tunnels)
POLISHD_TELEMETRY optional off (or 0/false/no) — disable dashboard telemetry entirely, consent prompt included
POLISHD_TELEMETRY_ENDPOINT optional Where dashboard telemetry is sent, if not the polishd project's own endpoint
POLISHD_ANALYTICS_SOURCE collector only telemetry — point the whole dashboard (every tab, the AI summary included) at collected cross-install telemetry instead of the site's own traffic. The Analytics tab's Site / Telemetry switch still shows either

AI settings are optional — the dashboard's Settings tab configures the same
fields, and what you save there overrides these env defaults.

Local dev needs nothing. If no writable store is available the package degrades
to a safe no-op (a console warning, dashboard notice) — your app never breaks.
See DATABASE.md for production setup.

Troubleshooting

Run npx @polishd/next doctor first — it catches most of these by name, and
--url http://localhost:3000 checks the live path as well.

Symptom Cause Fix
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
Every /api/* route 404s The proxy matcher touches /api Restore the (?!api…) exclusion. Under Next 16 + Turbopack this breaks the whole /api segment
Build fails: "can't recognize the exported runtime field" Route segment config re-exported Declare runtime/dynamic inline in the module
Dashboard has white borders and no greys, or is unstyled A package older than 0.2.3-dev.23 without the dashboard.css import, styled by the host's Tailwind and palette Upgrade; see Styling
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
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
ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite Node < 22.5 Upgrade Node, or configure Postgres
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
Snapshot capture: "Content-Security-Policy blocked…" Your CSP's connect-src doesn't allow the screenshot service or the Blob API Add https://shots.polishd.muskanvaswan.xyz and https://vercel.com to connect-src
Snapshot capture: "behind Vercel's Deployment Protection" A protected preview deployment; the service only sees Vercel's login Capture from the production domain, or a deployment without protection
Snapshot capture: "can't reach …" The site isn't on the public internet Run the dashboard locally (next dev), where a browser on your machine shoots it
Site snapshots section is at the bottom of the Design tab, marked Off Production without a Blob store (on Vercel), or the screenshot service wasn't allowed, or a capture found it can't be used from here The section's message names which; "Review and allow…" reopens the consent dialog

Entry points

Import Contents
@polishd/next config, definePolishdConfig, resolveSessionCookie, event types (isomorphic)
@polishd/next/client initPolishd, PolishdMonitor
@polishd/next/server store, ingest, queries, withPolishdSession (Node only)
@polishd/next/route POST, createPolishdRoute
@polishd/next/telemetry POST, OPTIONS, createPolishdTelemetryRoute — CORS-enabled ingest for telemetry from other polishd installs
@polishd/next/proxy proxy, config, withPolishdSession, polishdMatcher, composePolishd
@polishd/next/dashboard createPolishdPage, polishdTokenAuth, PolishdDashboard, loadPolishdDashboardData

CLI

Command Purpose
npx @polishd/next init Scaffold the glue files. --dry-run, --force, --js, --config
npx @polishd/next doctor Check an install. --url <origin> adds live checks

Development

This repository holds the package, its landing site (site/) and the screenshot service (service/). To work on the package:

npm install
npm run build        # tsc -> dist (ESM + .d.ts)
npm run typecheck

To test a change against a real app, npm pack and install the tarball into a
scratch Next.js project — that exercises the published exports map and the
"use client"/"use server" boundaries the way a consumer will.

service/ is the screenshot service site snapshots use — a
separate Next.js app deployed as its own Vercel project. See its README.

License

MIT — see LICENSE.

Yorumlar (0)

Sonuc bulunamadi