CRRT π₯ is visual feedback capture for React sites, plus a private reviewer console and a Proof-style agent bridge.
This repo is no longer just "a widget that stores comments."
It now has five product surfaces:
- Public widget in
src/- capture feedback on any page
- submit comments to the public ingestion API
- Reviewer dashboard in
apps/dashboard/- triage comments
- accept or reject feedback
- create agent shares
- copy prompts for Codex or Claude Code
- Public landing/demo in
apps/landing/- CRRT marketing page
- live widget demo mounted against the same published widget source
- Agent bridge API in
api/v1/- expose page-scoped or selection-scoped feedback to coding agents
- track presence, events, and implementation updates
- Storybook addon and CLI in
packages/crrt-storybook/- connect a Storybook 10 project to CRRT Pro with one command
- capture component feedback and failed checks with authenticated story context
The old /api/comments route still exists as a compatibility path and for the smoke workflow. The new product flow uses /api/v1/....
src/- published widget package
apps/dashboard/- private reviewer UI
apps/landing/- public CRRT landing/demo site for the widget
api/- Vercel serverless API routes
packages/crrt-storybook/- published
@thedesignproject/crrt-storybookaddon and connection CLI
- published
db/- Drizzle schema, migrations, seed
supabase/- legacy SQL (
supabase/legacy/) and storage policies (supabase/policies/) β schema management has moved todb/
- legacy SQL (
Install:
bun add @thedesignproject/crrtUsage:
import { FeedbackWidget } from '@thedesignproject/crrt'
export default function App() {
return (
<FeedbackWidget projectId="demo-project" />
)
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
projectId |
string |
yes | Project public key used for comment capture. |
apiBase |
string |
no | Base URL of the backend that serves the widget API. Defaults to https://crrt.ai/api. Override to point at a self-hosted deployment. |
theme |
'light' | 'dark' | 'system' |
no | Widget appearance. Defaults to dark; system follows prefers-color-scheme. |
disabled |
boolean |
no | When true, the widget renders nothing β no UI, no listeners, no API calls. Defaults to false. Useful for turning the widget off per environment (e.g. production). |
The identity dialog offers Continue as guest (with a display name) or Log in to CRRT.
Login opens the hosted dashboard and reuses an existing CRRT session. No additional widget props
or Supabase credentials are needed. Self-hosted deployments must serve /dashboard/widget-auth
on the same origin as apiBase and apply the generated widget-session migration.
The website receives an opaque, one-hour credential scoped to the signed-in user, project, and exact website origin. It stays in memory; reloading requires another handoff. The credential can create feedback and read, edit, or delete that user's widget comments on that origin. It cannot access dashboard or account APIs. The signed-in widget shows My feedback; the avatar opens account controls and Sign out of this widget, which revokes that widget session.
Public projects continue to offer guest feedback. In project settings, admins can enable Private project, which requires login and limits the website widget to the commenter's own feedback. Admins choose whether the entire project team or only the owner and admins can access all feedback in CRRT. Existing anonymous comments cannot be claimed by signing in.
New widget screenshots use private storage with five-minute signed URLs. Enabling privacy protects existing widget screenshots and removes their public objects before saving the setting. Automatic public agent links stop working for private projects; explicitly created feedback shares still grant access to their selected feedback. Apply the generated project-privacy migration before deploying this feature.
Sending feedback to GitHub explicitly exports its screenshot through a durable, screenshot-only link. That link stops working if the comment, screenshot, or linked GitHub issue is removed from CRRT. Privacy changes also cancel pending activity emails; each delivery rechecks recipient access.
The handoff uses a five-minute, one-time code bound to a proof generated by the widget. Only code
and token hashes are stored in widget_auth_sessions, with RLS enabled. Expired rows are removed
when a new handoff is created. No new signing-secret environment variable is required.
Widget authentication endpoints:
POST /v1/widget/auth/handoffrequires a CRRT dashboard session and creates the one-time code.POST /v1/widget/auth/exchangeexchanges the code and proof for the restricted credential.DELETE /v1/widget/auth/exchangerevokes the presented widget credential.GET /v1/widget/commentslists the credential owner's shared widget feedback on its origin.PATCH /v1/widget/comments?id=...edits an owned comment's body.DELETE /v1/widget/comments?id=...deletes an owned comment.POST /v1/public/commentsaccepts the widget credential to record verified account ownership.
The current widget calls these HTTP endpoints at apiBase:
GET /v1/public/comments?projectKey=...returns page comments for compatibility with the in-page sidebar.POST /v1/public/commentscreates a public comment.PATCH /v1/public/commentsupdates review status for current widget compatibility.
Create comment request:
{
"projectKey": "demo-project",
"pageUrl": "https://example.com/pricing",
"selector": "main > section:nth-of-type(2) button",
"x": 540,
"y": 220,
"body": "This CTA feels too weak"
}Patch status request:
{
"id": "comment-id",
"reviewStatus": "accepted"
}Protected by REVIEWER_API_TOKEN.
GET /api/v1/projects
GET /api/v1/projects/:projectId/comments
PATCH /api/v1/comments/:commentId/review-status
POST /api/v1/feedback-shares
GET /api/v1/feedback-shares/:shareId/prompt
Protected by per-share bearer token.
GET /api/v1/agent/shares/:slug/state
GET /api/v1/agent/shares/:slug/events
POST /api/v1/agent/shares/:slug/presence
POST /api/v1/agent/shares/:slug/ops
Product Audit runs a durable Explorer β Critic β Verifier workflow for a public URL. Explorer runs in an isolated Vercel Sandbox, model output is schema-validated through the existing OpenAI-compatible configuration, and deterministic admission can return zero to five Open findings.
GET /api/v1/audits/capabilities
POST /api/v1/audits
GET /api/v1/audits/:auditId
GET /api/v1/audits/:auditId/events?after=0&limit=50
POST /api/v1/audits/:auditId/cancel
Creation requires Idempotency-Key. Project runs use a Supabase bearer token and projectKey. Anonymous runs use the returned signed X-Audit-Session value for subsequent creation and the per-audit X-Audit-Token for reads or cancellation; tokens are never placed in URLs. The server stores hashes only.
Set AUDIT_FEATURE_ENABLED=true, AUDIT_ANONYMOUS_ENABLED=true, and a random AUDIT_TOKEN_SECRET of at least 32 characters. The exact demo host demo.crrt.ai uses deterministic evidence without DNS or AI; every other URL must resolve publicly and uses the Vercel Workflow/Sandbox environment plus AI_API_BASE_URL, AI_API_KEY, and AI_MODEL. Set AI_CRITIC_MODEL to use a separate, faster Critic model; it falls back to AI_MODEL when omitted. Gateway 429 responses release the stage lease, back off durably, and finish as a partial zero-finding report if capacity remains unavailable. Never commit real secrets.
bun run local:dev runs live audit stages inline because its custom Bun API server does not expose Workflow SDK queue handlers. URL exploration still runs in an isolated node:24-alpine Docker container; pull that image before the first live local audit. The server binds only to loopback and lets unexpired anonymous reports reopen across local browser profiles without a capability token. Production always requires capability headers and uses Vercel Workflow and Vercel Sandbox.
Schema is managed with Drizzle ORM. The source of truth is db/schema.ts, which defines:
projectsproject_repo_configscommentsfeedback_sharesfeedback_share_itemsfeedback_eventsagent_presencefeedback_operation_keys
- Development:
bun db:pushβ syncs the schema directly to your database. Fast iteration, skips migration history. - Production:
bun db:migrateβ applies versioned migrations fromdb/migrations/.deploy-buildruns this on every deploy.
bun db:generate # writes a new migration file under db/migrations/
bun db:migrate # applies itbun db:seed runs db/seed.ts, which inserts a demo-project row and a matching repo config for local development.
The hand-managed SQL files used before Drizzle live under
supabase/legacy/for reference only β do not run them against a current database.
For the default-off, test-only Stripe integration, see Stripe billing setup.
Example values live in .env.example.
Required server variables:
SUPABASE_URLSUPABASE_KEYβ the anon/publishable key; the dashboard includes it in the browser bundleSUPABASE_SECRET_KEYβ the privileged server-only key; never expose it to client code- During the key cutover,
SUPABASE_SERVICE_ROLE_KEYis accepted as a fallback. SetSUPABASE_SECRET_KEYto a newly generated Supabasesb_secret_*key in each environment, deploy, and verify privileged operations before removing the old environment variable. Renaming the variable while keeping its legacy JWT value does not migrate the credential. Before deactivating legacy keys in Supabase, verify that no other apps, scripts, or public anon-key consumers still depend on them. REVIEWER_API_TOKENSHARE_TOKEN_SECRET- encrypts persisted share and native integration tokensWIDGET_AUTH_SECRET- signs native integration OAuth state
Product Audit server variables (when the feature is enabled):
AUDIT_FEATURE_ENABLEDandAUDIT_ANONYMOUS_ENABLED- server-controlled availabilityAUDIT_TOKEN_SECRET- signs anonymous browser sessions and capabilitiesAUDIT_LOCAL_EXECUTION- local-only inline workflow adapter; ignored in production and set automatically bylocal:devAI_API_BASE_URL,AI_API_KEY, andAI_MODEL- OpenAI-compatible gateway configuration and Verifier/default modelAI_CRITIC_MODEL- optional Critic model override, for exampleopenai/gpt-oss-20b; falls back toAI_MODELAUDIT_MAX_ROUTES,AUDIT_MAX_ACTIONS,AUDIT_WALL_CLOCK_MS,AUDIT_MODEL_TOKENS, andAUDIT_MAX_ARTIFACTS- optional bounded execution budgetsAUDIT_MODEL_TIMEOUT_MSandAUDIT_MODEL_ATTEMPTS- optional bounded provider retry controls
Optional server variables:
-
APP_URL- canonical app URL for generated links and notification emails -
RESEND_API_KEY- enables comment activity emails through Resend -
COMMENT_ACTIVITY_EMAIL_FROM- sender for comment activity emails, defaults toCRRT <activity@mail.crrt.ai> -
COMMENT_ACTIVITY_EMAIL_COOLDOWN_HOURS- per-project email cooldown window, defaults to5 -
COMMENT_ACTIVITY_EMAIL_TIMEOUT_MS- Resend request timeout, defaults to5000 -
LINEAR_CLIENT_IDandLINEAR_CLIENT_SECRET- enable native Linear OAuth and issue creation -
LINEAR_REDIRECT_URI- optional fixed Linear callback URL; defaults toAPP_URL/v1/integrations/linear/callback -
JIRA_CLIENT_IDandJIRA_CLIENT_SECRET- enable native Jira Cloud OAuth and issue creation -
JIRA_REDIRECT_URI- optional fixed Jira callback URL; defaults toAPP_URL/v1/integrations/jira/callback
The secret key also lets the API resolve project member emails from Supabase Auth for comment activity notifications. Without it, that email path stays disabled because there are no resolved recipients.
Comment activity notifications are persisted in the server-only comment_email_batches table before delivery. Each recipient gets a private message; the sender is never added as an extra recipient. Successful batches are checkpointed, and pending batches retry with the same frozen payload and Resend idempotency key. Retries stop after eight attempts or 20 hours, before Resend's 24-hour idempotency window ends; terminal failures retain a safe error code for inspection. The worker removes email content and recipient addresses older than seven days on successful runs.
The authenticated worker at GET /api/v1/internal/comment-email-deliveries runs every minute through Vercel Cron and requires Authorization: Bearer <CRON_SECRET>. Configure CRON_SECRET and use a Vercel plan supporting minute-level schedules. Self-hosted deployments must schedule the same authenticated request. Immediate delivery is attempted after enqueueing, but the scheduler is required for recovery after failures or process restarts. Apply the generated database migration before deploying this code.
Comment activity notifications are persisted in the server-only comment_email_batches table before delivery. Each recipient gets a private message; the sender is never added as an extra recipient. Successful batches are checkpointed, and pending batches retry with the same frozen payload and Resend idempotency key. Retries stop after eight attempts or 20 hours, before Resend's 24-hour idempotency window ends; terminal failures retain a safe error code for inspection. The worker removes email content and recipient addresses older than seven days on successful runs.
The authenticated worker at GET /api/v1/internal/comment-email-deliveries runs every minute through Vercel Cron and requires Authorization: Bearer <CRON_SECRET>. Configure CRON_SECRET and use a Vercel plan supporting minute-level schedules. Self-hosted deployments must schedule the same authenticated request. Immediate delivery is attempted after enqueueing, but the scheduler is required for recovery after failures or process restarts. Apply the generated database migration before deploying this code.
Useful client variables:
VITE_API_BASEVITE_PROJECT_IDVITE_REVIEWER_TOKEN
- React 18 or 19 on the consuming app.
- A backend implementing the endpoints above.
Install:
bun installRun the public demo:
bun run devRun the reviewer dashboard:
bun run dev:dashboardDocker Desktop and the Supabase CLI can run Postgres, Auth, Storage, Realtime, Studio, and local email without using a shared cloud project.
bun install
bun run local:up
bun run local:devlocal:up starts the containers, writes the local service credentials to the
ignored .env.docker.local, applies every Drizzle migration, seeds demo-project,
creates the public feedback-images bucket, and runs service smoke checks.
- Dashboard:
http://127.0.0.1:5173/dashboard/ - API:
http://127.0.0.1:3001 - Supabase Studio:
http://127.0.0.1:54323 - Local email inbox:
http://127.0.0.1:54324
Useful lifecycle commands:
bun run local:status
bun run local:down # keeps local data
bun run local:reset # deletes local data, then migrates and seeds againThe local Supabase credentials are development defaults and must never be used in production. Docker may publish the development ports beyond localhost, so run this stack only on a trusted network.
Build the widget package:
bun run buildBuild the dashboard:
bun run build:dashboardRun tests:
bun run testTypecheck everything:
bun run typecheckThe legacy route remains available:
GET /api/comments?projectId=...
POST /api/comments
PATCH /api/comments
DELETE /api/comments
That path is kept for backward compatibility and smoke validation. New product work should target /api/v1/....
CRRT is OSS-first. crrt.ai is the easiest path β a managed hosted instance with auth, storage, and the agent bridge all wired up. But the same code in this repo runs on your own infra in under twenty minutes if you'd rather own the stack.
- Postgres for the data layer. Any provider works; we recommend Supabase so you get auth + storage out of the box β that's what the hosted instance runs on.
- A runtime that can serve the Vercel-style serverless functions in
api/plus the static builds inapps/landing/andapps/dashboard/. We deploy to Vercel; Fly, Render, Cloudflare Workers, or a Node container behind a reverse proxy all work as long as your function runtime is compatible with@vercel/node. - Bun β₯ 1.4 for the build (
bun install,bun run build).
git clone https://github.com/thedesignproject/CRRT.git
cd CRRT
bun installCopy .env.example to .env:
cp .env.example .envFill in the required variables (see Environment variables):
SUPABASE_URL,SUPABASE_KEYβ your Supabase URL and public anon/publishable keySUPABASE_SECRET_KEYβ your privileged server-only key; never expose it to client codeREVIEWER_API_TOKENβ long random string; gates the reviewer API endpointsSHARE_TOKEN_SECRETβ long random string; signs per-share bearer tokens
The dashboard reads SUPABASE_URL and SUPABASE_KEY at build time. Only the public key is included in the browser bundle.
bun run db:migratedeploy-build does this on every deploy, but the first time you'll want to run it locally so the schema is in place before you push.
If you're targeting Vercel:
vercel deployThe vercel.json at the repo root sets buildCommand to bun run deploy-build, which typechecks, builds the landing app to apps/landing/dist, builds the dashboard into apps/landing/dist/dashboard, and runs pending migrations. Both surfaces ship in one deploy.
If you're targeting another runtime, the relevant outputs are:
apps/landing/dist/β static landing pageapps/landing/dist/dashboard/β static dashboard SPA (build withbun run build:dashboard)api/β Vercel-style serverless handlers; adapt them to your platform if needed
curl -s "$APP_URL/api/v1/public/comments?projectKey=demo-project"You should get a JSON response (empty array on a fresh DB, or the seeded demo project's comments if you ran bun db:seed).
We tag widget releases on npm but the OSS server-side moves on trunk. Pull periodically; bun run db:migrate is idempotent so re-running it after a fetch is safe.
crrt.ai runs whatever is on trunk. If you self-host, you can pin to a tag (git checkout v0.x.y) for stability. We aim to keep the public API surface backward-compatible across minor versions.
Licensed under the Apache License, Version 2.0. See LICENSE.