stacks/bench-review
publicClone
Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.
Like Glassdoor but for the judiciary. Written by clerks, so users can get a glimpse into the inner workings of the court system.
- .config
- .github
- .maestro
- app
- cloud
- config
- database
- docs
- middleware
- plugins
- public
- resources
- routes
- scripts
- storage
- tests
- types
- .env.example 6.5 KB
- .gitignore 5.5 KB
- .stacks-channel 6 B
- .stacks-version 47 B
- bootstrap 3.9 KB
- buddy 6.5 KB
- build.ts 13.4 KB
- bun.lock 169.2 KB
- bunfig.toml 888 B
- CHANGELOG.md 380.3 KB
- CLAUDE.md 1.0 KB
- DEPLOY.md 13.9 KB
- deps.yaml 32 B
- gate.ts 55.2 KB
- LICENSE.md 1.1 KB
- package.json 2.6 KB
- pantry.lock 51.2 KB
- pkgx.yaml 739 B
- preview.ts 8.2 KB
- README.md 5.9 KB
- site.config.ts 2.8 KB
- stx.config.ts 2.1 KB
- tsconfig.app.json 3.1 KB
- tsconfig.json 1.3 KB
Bench Review
A public directory of judges, with first-hand reviews from the attorneys, clerks, and court staff who appear before them. Think Yelp, for the bench.
Built as a Stacks app: defineModel()
for schema, stx for views, Crosswind for
styling, and buddy for everything else.
[!WARNING] Pre-launch. The application surface is substantially complete, but the directory has no production content yet — building against an empty database produces zero judge and courthouse pages. Content ingestion is the open blocker. See Status.
What it does
The directory — judges and courthouses, each with a profile, a reviews tab, and (for judges) recent rulings. Courthouses carry real building-level coordinates and render on a Leaflet/OSM map.
Reviews — a rich-text editor with server-side draft autosave (so a draft
survives a device change), photo upload that strips EXIF and emits a
three-size resize ladder, 1–5 star ratings, threaded comments, and a
"helpful" vote with a denormalised counter so public feeds stay COUNT-free.
Reviews can be posted anonymously; the reviewer label is derived through a
helper built to avoid de-anonymisation.
Moderation — an admin surface with queues for flagged reviews, judge submissions, and credential claims, plus user suspension (reversible) and deletion, and an append-only moderation audit log recording who did what.
Trust — reviewers can submit a credential claim (role + state) that an admin verifies out of band. Judges can claim their own profile and post an official response to a review of them; the same right-of-reply is also available admin-mediated.
Accounts — registration, login, email verification (with resend), password reset, password change, and sign-out-everywhere. Plus data export and self-serve account deletion with a full cascade.
Public hygiene — a DB-derived sitemap and robots.txt, per-page canonical / Open Graph / Twitter tags, a JSON-LD Organization + WebSite + WebPage graph, and a Content-Security-Policy with every inline script hashed. Rate limits sit on every public and mutating endpoint.
Stack
| Layer | What |
|---|---|
| Framework | Stacks |
| Views | stx — build-time rendered, no frontend render server |
| Styling | Crosswind (Tailwind-compatible utilities) |
| Data | defineModel() + bun-query-builder; SQLite in dev, Postgres in production |
| Search | Meilisearch configured; the UI still filters client-side |
| Images | sharp (EXIF strip + resize) |
| Storage | app/Storage/disk.ts — local disk or Bun-native S3 via FILESYSTEM_DISK |
| Lint | pickier (never eslint directly) |
Getting started
bun install
cp .env.example .env
./buddy key:generate
./buddy migrate
./buddy seed
./buddy devViews come up on :4000, the API on :4008.
./buddy seedloads 6 courthouses, 24 judges, and a handful of reviews — enough to exercise every page, not enough to be a product.
Layout
app/
Actions/ one file per endpoint, grouped by surface (Reviews, Judges, Admin, Me, …)
Models/ defineModel() schema; `useApi.routes` is OPT-OUT — always state it
Helpers/ SEO head injection, sitemap, CSP, review photos, storage disk
Support/ health endpoint
resources/
views/ one file per route; dynamic routes enumerate via getStaticPaths
components/ Bench/* — the real UI lives here
stores/ all data access goes through a store, never a fetch in a component
layouts/ page shells
routes/api.ts hand-written API routes (auto-prefixed with /api)
database/ plain-SQL migrations + idempotent seeders
scripts/ the validation suite (see below)
site.config.ts single source of truth for name / url / SEO
gate.ts 17-check build + SEO conformance gateQuality gates
bun run lint # pickier
bun run typecheck:app # app-owned TypeScript
bun run gate # build + 17 build/SEO integrity checks
bun run ui:validate # accessibility lint over resources/
bun run visual:validate # headless-Chrome contrast / overflow / focus audit
bun run release:validate # route text contracts, assets, metadata, migrations
bun run deployment:smoke # post-deploy checks against the live domainrelease:validate only passes on a build made with a production APP_URL —
that is the check that catches a dev artifact being shipped.
Deploying
See DEPLOY.md. The short version: the build produces a static
dist/ plus an API process, and the host must provide two things the artifact
cannot — an /api/* proxy and a /verify-email/:id/:token rewrite.
Status
Working and verified: the full review lifecycle, moderation, auth, privacy
export/delete, SEO surface, and rate limiting. lint, typecheck:app,
ui:validate and release:validate are green; gate is 16/17.
Open before launch:
- Content ingestion — the directory is empty, so no entity pages render. This is the one blocker that makes the rest moot.
- A host —
config/cloud.tsis still stock Stacks and describesstacksjs.org, not this app. - Production SMTP — email verification and password reset depend on it.
routes: []onapp/Models/Request.ts— it currently declaresuseApi: truewith noroutes, which the ORM generator turns into unauthenticated full CRUD at/api/requests.HttpOnlyon the auth cookie — the token is readable from JS today.- Tests —
tests/is still placeholder stubs. docs/— still the inherited Stacks framework documentation; either curate it for this app or remove it.