Serein
Rust로 만든 가볍고 성능 좋은 Discord 전용 네이티브 클라이언트
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Family decisions, one round at a time. Polls grouped around an event (a trip, a birthday, Friday dinner), narrowed down in rounds, with a running record of what was decided and one link to share back into the family chat.
• Product brief: docs/BRAINSTORM.md (concepts, critiques and landscape scan in docs/BRAINSTORM-APPENDIX.md)
• Screen designs (source for the design canvas): design/README 전문 보기 ↗Family decisions, one round at a time. Polls grouped around an event (a trip, a birthday, Friday dinner), narrowed down in rounds, with a running record of what was decided and one link to share back into the family chat.
docs/BRAINSTORM.md (concepts, critiques and landscape scan in docs/BRAINSTORM-APPENDIX.md)design/| Piece | Choice |
|---|---|
| App | Next.js 16 (App Router, server actions), React 19, TypeScript |
| Styling | Tailwind CSS 4 with the palette in src/app/globals.css |
| Auth | Clerk (Google, Apple, Facebook sign-in; prebuilt components) |
| Data | Postgres via Drizzle ORM (postgres driver, works with Neon, Supabase, Railway, local) |
| Hosting | Vercel, linked to this repo |
The app deploys green with no secrets set: every protected route redirects to /setup, which shows which environment variables are still missing (presence only, never values). Once the variables exist and the schema is pushed, the same deployment comes alive.
npm install
cp .env.example .env.local # fill in Clerk keys and a DATABASE_URL
npm run db:migrate # creates the tables (the build does this too)
npm run dev # http://localhost:3000
Other scripts: npm test (unit tests: the rounds engine, format helpers, and feed mappers), npm run typecheck, npm run lint, npm run db:generate (writes SQL to drizzle/ after a schema change; the next build applies it), npm run db:studio.
Checkout Session smoke only (no paywall, no subscriptions). Paths match the Stack Ballot convention:
| Piece | Path |
|---|---|
| Stripe client | src/lib/stripe.ts |
| Create session | POST /api/stripe/checkout |
| Webhook stub | POST /api/stripe/webhook |
| Smoke UI | /app/billing/smoke (not linked from home/nav) |
vercel env pull (or copy placeholders from .env.example into .env.local). Needed: STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, optionally STRIPE_WEBHOOK_SECRET, and STRIPE_SMOKE_ENABLED=1 to enable the UI (keys alone are not enough — flag off returns 404).npm run dev, sign in as an organizer, open http://localhost:3000/app/billing/smoke.stripe listen --forward-to localhost:3000/api/stripe/webhook and put the printed whsec_… into STRIPE_WEBHOOK_SECRET.Leave STRIPE_SMOKE_ENABLED unset in production. This path is for Stack launch verification only.
Clerk (sign-in): in the Vercel project, Integrations → Browse Marketplace → Clerk → Install, which creates the Clerk application and adds NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY for you. (Without the marketplace: create an app at dashboard.clerk.com and paste those two keys under Settings → Environment Variables.) Then in the Clerk dashboard, SSO connections → Add connection, pick Google, Apple and Facebook. In a development instance all three work immediately on Clerk's shared credentials; nobody needs a Google Cloud, Apple Developer or Meta account to try the app. Going to production later means a pk_live_ / sk_live_ instance with your own provider credentials (Apple needs an Apple Developer membership, Facebook needs a Meta app switched to Live).
Database: in the Vercel project, Storage → Create → Neon Postgres. Vercel adds DATABASE_URL for you. The build runs pending migrations itself (scripts/migrate.mjs), so there is nothing to run by hand. Keep migrations backward-compatible with the previous deployment (see CLAUDE.md).
App URL (optional): NEXT_PUBLIC_APP_URL=https://your-domain so share links are stable.
Brand name (optional): NEXT_PUBLIC_BRAND_NAME and NEXT_PUBLIC_BRAND_TAGLINE rename the product everywhere it shows (src/lib/brand.ts holds the defaults). "FamPoll" is a working name.
Famdash feed (optional): set FAMPOLL_API_KEY to a long random secret. Famdash (or any FeedClient) calls GET /api/feed/recent?limit=10 with Authorization: Bearer <FAMPOLL_API_KEY> and receives a FeedItem[] of open (needs-vote) decisions first, then recently decided ones (≤14 days). Without the key the route returns 401. Example:
curl -H "Authorization: Bearer $FAMPOLL_API_KEY" \
"https://your-domain/api/feed/recent?limit=10"
Redeploy. /setup should show every step ticked.
src/app/ routes (mobile-first, server components + server actions)
page.tsx landing and sign-in
app/ the signed-in app: home, family, events, decisions
join/[code] invite link
s/[token] public read-only "what we've decided" summary
setup integration checklist
src/lib/engine/rounds.ts the rounds engine: pure functions, unit-tested
src/lib/lifecycle.ts closing a round and what follows (advance, decide, tie)
src/lib/actions/ server actions (family, events, decisions and votes)
src/lib/queries.ts read models for each screen
src/lib/db/schema.ts Drizzle schema
src/proxy.ts Clerk route protection (Next 16 proxy, was middleware)
design/ .dc.html artboards for the design canvas
docs/ product brief
A decision belongs to an event and is described by three choices. Its format says what an option is: a short line of text, a longer paragraph, or a date range (exactly one per decision). Its vote type says how each voting round is voted: A or B (exactly two options, pick one; always a single round, and nobody can add or remove an option), multiple choice (pick one of several), or pick several (each person picks up to N, never all of them). Its plan says how many rounds it takes: a quick vote (one final round), a shortlist then a final, or ideas then shortlist then final. In an ideas round people add options and nobody votes. In a shortlist round everyone votes and the top two advance (everyone tied at the cut line advances; options with zero votes drop). In a final the option with the most votes wins. A round closes when its deadline passes or as soon as every seat in the family has voted, and the next round opens by itself. A tied final asks the organizer to run a tiebreak between the tied options or just call it. The organizer can always close a round early, give it more time, reopen the last round, decide by hand, or set a decision aside. Kids and relatives without a phone get a proxy seat that an adult votes from. While a round is open nobody sees anyone else's picks, organizers included; only who has voted so far. Results appear when the round closes, by name unless someone hid their ballot, in which case that round shows counts only. Anyone can hide a ballot (per ballot, or as a standing preference on their seat) and can ask a question or suggest an idea anonymously; every vote is still recorded under its seat.