Rai (राई) is the Hindi word for mustard seed — a nod to Jira (जीरा, cumin). Tiny things that pile up and need gathering.
A lightweight, hostable request queue. People submit what they need and watch a live public queue; you (the admin) triage, annotate, progress, and clear — with permissions enforced in the database, not just hidden in the UI.
- Public board — frictionless submission (name, text, urgency), live queue, sort by urgency/newest, urgency filter, search, shareable URL filters.
- Live updates — granular realtime: inserts/updates/deletes patch the list in place (no full refetch per event).
- Admin layer — email/password login, per-card status + private notes, delete-with-confirm, bulk select → mark done / delete, clear all done.
- Undo — deletes (single, bulk, and clear-done) show an Undo toast that restores the row(s) verbatim.
- Keyboard shortcuts (admin) —
j/kmove,xselect,ecycle status,ddelete,escclear selection. - Permalinks — every request has a shareable read-only page at
/r/:id. - Private notes — admin notes live in a separate, admin-only table; anonymous visitors can't read them even via the raw API.
- Quality of life — dark mode, optimistic submit with rollback, soft
duplicate detection, client-side rate limiting, "queue is busy" banner,
incremental rendering ("Show more"), skeletons, empty/error states, a
top-level error boundary, and accessible (
aria-live, keyboard-operable) components.
npm install
npm run dev # http://localhost:5173With no Supabase credentials, the app runs in demo mode: data lives in your
browser's localStorage and the RLS rules are faithfully simulated (anon can only
insert clean pending rows; edit/delete require an admin session). Admin login
in demo mode accepts any email with the password demo.
-
Create a Supabase project. In the SQL editor, run
supabase/schema.sql— this creates the tables (requests+ privaterequest_notes), the Row Level Security policies, theupdated_attriggers, and enables Realtime. -
Authentication → Users → Add user: create your one admin (email + password).
-
Authentication → Sign In / Providers: turn off "Allow new signups". Since the only authenticated user is you, authenticated = admin by construction.
-
Copy the Project URL and anon public key into
.env:cp .env.example .env # fill in VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
The anon key is safe to ship to the browser — RLS is the gate. Restart npm run dev after editing .env (Vite reads it at startup); the demo banner disappears
once you're connected.
If your project predates the private-notes change (it had an admin_notes
column on requests), run the data-preserving migration once:
supabase/migrate_2_private_notes.sql.
Open the deployed site as an anonymous visitor, open dev tools, and try a direct mutation:
import { createClient } from '@supabase/supabase-js'
const sb = createClient(URL, ANON_KEY)
await sb.from('requests').delete().eq('id', '<some-id>') // → rejected by RLS
await sb.from('requests').update({ status: 'done' }).eq('id', '<id>') // → rejected
await sb.from('request_notes').select('*') // → returns [] for anonBoth mutations should fail, and request_notes returns nothing for anon. Only
inserts of clean pending rows succeed.
npm test # Vitest unit tests (sort/filter, duplicates, rate limit)
npm run test:watchEnd-to-end (Playwright) is scaffolded in e2e/; enable with:
npm i -D @playwright/test && npx playwright install chromium
npm run test:e2e- Build command:
npm run build - Publish directory:
dist - Add
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEYunder Site settings → Environment variables. netlify.tomlalready includes the SPA redirect so/adminand/r/:idsurvive a hard refresh.- The build is code-split (vendor chunks + lazy admin/detail routes) so the public board loads a lean initial bundle.
A few knobs live in src/lib/config.ts (WIP banner
threshold, page size) and src/lib/rateLimit.ts
(submission cooldown/quota).
src/
lib/ supabase client, demo store, unified repo (events/notes/restore),
sort/filter, status helpers, duplicates, rate limit, config, utils
hooks/ useRequests (realtime + optimistic), useAuth, useTheme,
useNow, useQueueFilters (URL state)
components/ Header, SubmitCard, FilterBar, RequestCard, AdminControls,
EmptyState, ThemeToggle, Badges, DemoBanner, ErrorBoundary,
ui/ (button, dialog, dropdown)
pages/ Board ("/"), Admin ("/admin"), RequestDetail ("/r/:id")
App.tsx router + lazy routes + LazyMotion + error boundary + toaster
supabase/ schema.sql, migrate_2_private_notes.sql, cleanup_test_rows.sql
e2e/ Playwright happy-path spec (scaffold)