Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기The agentic group trip planner. Caravan replaces the unpaid "trip mom" inside every group chat — the one person who DMs everyone for dates, aggregates them by hand, proposes destinations, absorbs 200 forwarded reels, and sends the reminders
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
The agentic group trip planner. Caravan replaces the unpaid "trip mom" inside every group chat — the one person who DMs everyone for dates, aggregates them by hand, proposes destinations, absorbs 200 forwarded reels, and sends the reminders — with a team of purpose-built AI agents living inside a shared trip room.README 전문 보기 ↗The agentic group trip planner. Caravan replaces the unpaid "trip mom" inside every group chat — the one person who DMs everyone for dates, aggregates them by hand, proposes destinations, absorbs 200 forwarded reels, and sends the reminders — with a team of purpose-built AI agents living inside a shared trip room.
One admin creates a trip and shares a code. Everyone else joins with Google, in about twenty seconds. The agents extract constraints from what people say, run the date math, force decisions to close, propose destinations with real reasoning, build the itinerary, and chase the stragglers — so the group chat converges on a plan instead of dying in a pile of "so are we doing this or not?"
Live Deployment: https://caravan.amitpile.com/
Group trips don't fail because chats don't capture information — they capture plenty. They fail because a group chat has no mechanism to close a question. "Anyone free the 2nd week of Dec?" gets four replies, two jokes, and then dies. Three weeks later someone asks "so are we doing this or not?" and the cycle restarts. Meanwhile prices go up and the window closes.
Caravan's product thesis rests on three bets:
Every screen below is a render of docs/design/screens.html — the hi-fi design spec this app was built from, organized into the same seven flows (A–G) the spec itself uses. These are the design reference, not live-app screenshots — a few details have since diverged as the build evolved (invite codes are plain 6-character codes now, not hyphenated; joining is Google OAuth for everyone, not a name/email form; Your Trips gained avatar stacks and richer status styling; and several screens below — the destination page, the per-member facts page, settings' danger zone — were redesigned past what's pictured here). Cross-reference against Feature tour for what's actually implemented today.
/join/[code]); if you're already signed in, clicking the link drops you straight into the lobby.postgres_changes, which under RLS only ever reaches the trip's admin — see Architecture), with a polling safety net so nobody's screen can get permanently stuck.The core thing this layer removes: an admin having to notice a vote closed, or a decision stalled, and manually tap a button.
The other half of the cost estimate actually meaning something: once someone's flagged, they get asked, not just counted.
Hotel and transport mentions get their own richer homes instead of sitting in a flat idea inbox.
trips.id), gated behind a destructive confirm.loading.tsx skeletons on every route segment that fetches before first paint.window.confirm).| Layer | Choice | Notes |
|---|---|---|
| Framework | Next.js 16 (App Router) | Server Components for data-heavy pages, Route Handlers for mutations |
| Language | TypeScript 5 (strict) | |
| UI | React 19, Tailwind CSS v4 (@theme tokens), tw-animate-css, lucide-react | Warm paper/plum/signal-green design system, hand-drawn SVG illustrations, no external UI kit |
| Backend / DB | Supabase — Postgres, Auth (Google OAuth), Realtime | Row-Level Security everywhere; a custom broadcast layer covers what RLS-gated postgres_changes can't reach for members (see Architecture) |
| AI | Vercel AI SDK v7 + Google Gemini 3.6 Flash (@ai-sdk/google) | One model across every agent; generateObject for structured output, generateText for the Q&A surface |
| Search | Tavily API (optional) | Live web context for Scout's destination research, and for Accommodations/Travel options' price/area/mode enrichment (lib/search/tavily.ts, one shared client); the app degrades gracefully without it |
| Background jobs | Inngest | Cron-driven Chaser sweep (deadline nudges/auto-lock), every 30 minutes |
| Validation | Zod 4 | Every mutating API route parses its body through a schema in lib/validation.ts |
| Testing | Vitest | 383 tests across 70 files — pure logic and API routes are unit-tested; no component/E2E layer yet |
| Containerization | Docker (multi-stage, standalone output) | Optional — Vercel needs none of this; see Docker |
flowchart TB
subgraph Client["Browser"]
RSC["Server-rendered pages<br/>(React Server Components)"]
CC["Client components<br/>(chat feed, calendars, sheets)"]
end
subgraph Next["Next.js 16 (Vercel)"]
Pages["App Router pages<br/>app/trip/[tripId]/..."]
API["Route Handlers<br/>app/api/trips/[tripId]/..."]
end
subgraph Supabase["Supabase"]
Auth["Auth<br/>Google OAuth — admin & members alike"]
PG[("Postgres<br/>23 tables, RLS on every one")]
RT["Realtime<br/>custom broadcast channel"]
end
subgraph Agents["AI agent layer (lib/agents/*)"]
Scribe["Scribe — gate + extract facts/availability/ideas from every message"]
Scout["Scout — destination generation"]
Planner["Planner — itinerary"]
QM["Quartermaster — cost estimate + prep checklist"]
Chaser["Chaser — deadline + quorum auto-lock, auto-generation triggers (no LLM)"]
DateOutreach["Date Outreach — private nudge for date-minority members"]
Digest["Digest — periodic recap + suggestion"]
Concierge["Concierge — kickoff & lifecycle announcements"]
end
subgraph External["External services"]
Gemini["Google Gemini 3.6 Flash<br/>via Vercel AI SDK"]
Tavily["Tavily<br/>(optional web search)"]
Inngest["Inngest<br/>cron: */30 * * * *"]
end
CC -->|fetch| API
RSC -->|server-side query| PG
API -->|service-role or RLS-bound client| PG
API --> Agents
Agents --> Gemini
Scout -.-> Tavily
API -->|postAgentMessage + broadcastTripChange| PG
PG --> RT
RT -->|"broadcast('change')"| CC
CC -.->|polling fallback| API
Inngest -->|sweep| Chaser
Chaser -->|quorum/deadline lock| API
Chaser -->|auto-trigger| Scout
Chaser -->|majority fits, 1-2 don't| DateOutreach
Chaser -->|3h + volume gate| Digest
API -->|decision just locked| Planner
API -->|decision just locked| QM
Auth --- PG
Pages --> RSC
Why a custom broadcast layer instead of just postgres_changes: every RLS policy on messages, decisions, votes, etc. grants SELECT to a trip's own admin_user_id — never to a plain member, even though members now authenticate through real Supabase Auth sessions too. Rather than loosen RLS, mutating routes explicitly broadcast() a {type, ...payload} event on a per-trip channel after every write, and every page that needs to feel "live" (the room, both lobbies, Plan via RealtimeRefresh) subscribes to it — with an interval-based polling fallback wherever a missed broadcast would otherwise leave a screen stuck (both lobbies).
Auth model: a single getAuthUser() (Supabase Auth session) answers "who's signed in" for both audiences. resolveCaller(tripId) does one lookup — auth user → that trip's members row by email — and returns { id, status, role }; role is the one source of truth for admin-only UI/actions inside any page both audiences can reach. requireTripOwner(tripId) is the separate, stricter check for service-role routes that bypass RLS entirely (member management, trip deletion, decision deletion, Scout/Planner/Quartermaster generation) — it independently verifies trips.admin_user_id === auth.uid() since the service client gets no RLS protection for free.
Every agent follows the same deterministic core, LLM shell principle: anything with a right answer (date math, quorum, who's over budget, when to auto-lock) is plain arithmetic; the LLM is only ever the one deciding what to say or what to propose, never the one deciding who's right.
| Agent | Posts as | Trigger | What it does | LLM? |
|---|---|---|---|---|
| Concierge | concierge | Room opens, decision opened/locked/reopened/deleted, intake submitted | Narrates lifecycle events into the room/thread with a receipt — nothing happens silently | No — templated |
| Scribe | scribe | Every new message | Gates ("does this contain extractable info?"), then extracts facts/availability/ideas attributed to the author only and files them — a stay/travel idea routes straight to Accommodations/Travel options instead, AI-enriched with price/area/mode on the spot; retries once with its own invalid output + the exact validation error echoed back if extraction fails, and normalizes known schema-shape mistakes before Zod validates rather than trusting a retry alone; posts a receipt — or an honest "couldn't pin it down" reply if it still can't parse the message | Yes — gate + extraction |
| Scout | scout | Admin requests destination options, or auto-fires once every active member has answered intake and no destination decision exists | Deterministic budget-ceiling gate → optional Tavily search → 3 destination options with cost/travel/reasoning, weighing the trip's own name/pitch alongside aggregated facts | Yes — generateObject |
| Planner | planner | Admin requests an itinerary, or auto-fires the instant the destination decision locks | Day-by-day itinerary paced to party size, vibe, hard constraints | Yes — generateObject |
| Quartermaster | quartermaster | Admin requests a cost estimate/checklist, or auto-fires on decision lock (estimate: destination locks; checklist: destination + dates both locked) | LLM estimates a per-head range; deterministic arithmetic (not the model) decides who's over budget, comparing against the range's midpoint. Also generates the prep checklist, and triggers a private budget-check ask for anyone newly flagged | Yes (estimate) + No (flagging) |
| Date Outreach | chaser | Chaser's sweep — a majority of active members fit the trip's leading date window but 1-2 don't; fires once per member per decision | Privately asks each member outside the window if they could shift, grounded in their own filed availability — posted to their own thread, never the group |
23 tables, every one RLS-enabled, pulled directly from the live Supabase schema.
erDiagram
trips ||--o{ members : "has"
trips ||--o{ messages : "has"
trips ||--o{ facts : "has"
trips ||--o{ availability : "has"
trips ||--o{ decisions : "has"
trips ||--o{ agent_runs : "logs"
trips ||--o| itineraries : "has"
trips ||--o{ ideas : "has"
trips ||--o{ tasks : "has"
trips ||--o{ bookings : "has"
trips ||--o| cost_estimates : "has"
trips ||--o{ threads : "has"
trips ||--o{ budget_checks : "has"
trips ||--o{ accommodations : "has"
trips ||--o{ travel_options : "has"
members ||--o{ facts : "files"
members ||--o{ availability : "submits"
members ||--o{ votes : "casts"
members ||--o{ idea_votes : "casts"
members ||--o{ tasks : "assigned"
members ||--o{ threads : "owns"
members ||--o{ messages : "authors"
members |o--o{ decisions : "locked_by"
members ||--o{ ideas : "posts"
members ||--o{ booking_status : "marks"
members ||--o{ budget_checks : "asked"
members ||--o{ accommodations : "suggests"
members ||--o{ accommodation_votes : "casts"
members ||--o{ travel_options : "suggests"
members ||--o{ travel_option_votes : "casts"
members ||--o{ travel_option_members : "joins"
decisions ||--o{ votes : "receives"
decisions ||--o{ date_outreach_nudges : "gates"
members ||--o{ date_outreach_nudges : "nudged"
ideas ||--o{ idea_votes : "receives"
accommodations ||--o{ accommodation_votes : "receives"
travel_options ||--o{ travel_option_votes : "receives"
travel_options ||--o{ travel_option_members : "carries"
bookings ||--o{ booking_status : "tracked_by"
threads ||--o{ messages : "contains"
messages |o--o{ facts : "source_message_id"
facts |o--o{ facts : "superseded_by"
trips {
uuid id PK
text name
text rough_intent
uuid admin_user_id "auth.users(id)"
text invite_code UK
text status "lobby | active | closed"
bool joining_open
text[] vibe
text budget_hint
text agent_tone "efficient | warm | dry"
int preferred_trip_days "2|3|5|7|10|14, default 7"
}
members {
uuid id PK
uuid trip_id FK
text display_name
text email
text role "member | admin"
text status "active | removed"
int nudge_tier "0-3"
timestamptz flagged_at
}
messages {
uuid id PK
uuid trip_id FK
uuid thread_id FK "nullable — null = group lane"
text lane "group | thread"
text author_type "member | agent"
uuid author_id FK "nullable for agent"
text agent_name "concierge|scribe|chaser|scout|planner|quartermaster"
text body
jsonb metadata
}
facts {
uuid id PK
uuid trip_id FK
uuid member_id FK
text category "budget|departure_city|vibe|hard_no"
text type "HARD | SOFT"
jsonb value
numeric confidence
text source "intake|manual|extract"
uuid source_message_id FK
uuid superseded_by FK "self-ref"
}
availability {
uuid id PK
uuid trip_id FK
uuid member_id FK
date start_date
date end_date
text strength "free|partial|blocked"
}
decisions {
uuid id PK
uuid trip_id FK
text type "DATES|DESTINATION|BUDGET|STAY|ACTIVITY|CUSTOM"
text state "DRAFT|OPEN|VOTING|LOCKED|REOPENED"
jsonb options
text quorum_rule
timestamptz deadline
text default_on_silence
text locked_option
text rationale
uuid locked_by FK
timestamptz reminded_at
}
votes {
uuid id PK
uuid decision_id FK
uuid member_id FK
text option_id
bool is_veto
}
date_outreach_nudges {
uuid id PK
uuid decision_id FK
uuid member_id FK "unique(decision_id, member_id) — never nudged twice"
}
budget_checks {
uuid id PK
uuid trip_id FK
uuid member_id FK
int threshold_amount
text status "pending | yes | no"
text reason "the hard-no reason, if any"
timestamptz answered_at
}
accommodations {
uuid id PK
uuid trip_id FK
uuid member_id FK
text name
text url
text price "AI-derived or manual, free text"
text area "AI-derived city/area"
text image_url
text source "chat | manual"
bool locked
date start_date "set when locked"
date end_date "set when locked"
}
accommodation_votes {
uuid id PK
uuid accommodation_id FK
uuid member_id FK
}
travel_options {
uuid id PK
uuid trip_id FK
uuid member_id FK
text name
text mode "air | road | water"
text timing
text price
text url
text source "chat | manual"
bool locked
}
travel_option_votes {
uuid id PK
uuid travel_option_id FK
uuid member_id FK
}
travel_option_members {
uuid id PK
uuid travel_option_id FK
uuid member_id FK "who's actually on this leg"
}
threads {
uuid id PK
uuid trip_id FK
uuid member_id FK
}
ideas {
uuid id PK
uuid trip_id FK
uuid member_id FK
text url "nullable — a text-only idea has no link"
text title
text note
text image_url
text category "activity | stay | travel"
}
idea_votes {
uuid id PK
uuid idea_id FK
uuid member_id FK
}
tasks {
uuid id PK
uuid trip_id FK
uuid member_id FK "nullable — null = shared"
text title
text category "docs|booking|packing|other"
date due_date
bool done
}
bookings {
uuid id PK
uuid trip_id FK
text item
date deadline
}
booking_status {
uuid id PK
uuid booking_id FK
uuid member_id FK
bool booked
}
cost_estimates {
uuid id PK
uuid trip_id FK "unique"
text destination
int min_per_head
int max_per_head
text currency
text assumptions
}
itineraries {
uuid id PK
uuid trip_id FK "unique"
text destination
jsonb days
}
agent_runs {
uuid id PK
uuid trip_id FK
text agent
text trigger
int input_tokens
int output_tokens
numeric cost
int latency_ms
text outcome "success|error|skipped"
}
Notable design choices in the schema itself:
facts.superseded_by is a self-referencing FK — a chat correction to a "current answer" category (budget, departure city) supersedes the prior fact rather than sitting alongside it; additive categories (vibe, hard_no) are left alone on purpose.messages.lane + thread_id is how the group feed and every member's private 1:1 thread share one table without ever leaking into each other.agent_runs is a full LLM observability log — tokens, estimated cost, latency, outcome — per call, per agent, per trip.trip_id (on delete cascade), which is what makes trip deletion a genuine one-shot teardown with zero orphaned rows.date_outreach_nudges has no trip_id of its own (same shape as votes — reachable only through decision_id); its unique(decision_id, member_id) constraint is the entire "never nudge the same person twice about the same decision" guarantee, enforced by the database rather than application logic.budget_checks has a partial unique index (unique(trip_id, member_id) where status = 'pending') — at most one open ask per member at a time, enforced by the database; a later re-estimate can ask again once they've answered, never piles a second ask on an unanswered one.travel_option_members is a plain many-to-many join with no extra columns — unlike accommodations (everyone's assumed together), travel genuinely splits, so a member can appear against more than one option (flight there, train back).ideas.category still technically allows 'stay'/'travel' at the database level, but nothing files into those values anymore now that both have their own dedicated tables — left alone rather than tightened, since relaxing a constraint later is free and tightening one on a live table isn't.git clone <this-repo>
cd caravan
npm install
cp .env.example .env.local
supabase/migrations/ against it, in filename order — either supabase db push via the Supabase CLI, or paste each one into the dashboard's SQL editor.anon public key, and the service_role key into .env.local (below). The service-role key is server-only — it bypasses RLS and must never reach the client.They don't go in this codebase at all — not in a file, not in an env var. Supabase Auth owns the entire OAuth handshake with Google; the app only ever calls supabase.auth.signInWithOAuth({ provider: "google" }) and lets Supabase redirect, exchange the code, and hand back a session. That's why a grep for client_id/clientId across this repo turns up nothing — it's not a gap, that credential simply has no reason to ever touch application code.
/auth/callback route:
https://<your-project-ref>.supabase.co/auth/v1/callback
app/auth/callback/route.ts):
http://localhost:3000/auth/callback # dev
https://<your-domain>/auth/callback # production
Fill in .env.local:
NEXT_PUBLIC_SUPABASE_URL= # your Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Supabase public anon key
SUPABASE_SERVICE_ROLE_KEY= # Supabase service role key (server-only, never exposed to the client)
GOOGLE_GENERATIVE_AI_API_KEY= # Gemini key from https://aistudio.google.com/apikey
TAVILY_API_KEY= # optional — Scout works without it, just with less live context
Nothing Inngest- or Google-OAuth-client-related belongs in this file — see §3 above for Google, and §5 below for Inngest.
The whole integration is two files: lib/inngest/client.ts (defines the client + the trip ID) and app/api/inngest/route.ts (the single HTTP endpoint — serve() from the inngest/next package turns it into GET/POST/PUT handlers). lib/inngest/functions.ts defines the actual cron job (chaser-sweep, */30 * * * *), which per active trip runs five sweeps in sequence: deadline/quorum decision lock, auto-generation (Scout/DATES), date-minority outreach, the periodic digest, and intake nudges.
next dev — it auto-discovers /api/inngest with zero config, no env vars needed:
npx inngest-cli@latest dev
/api/inngest URL, and set these two on your host (Vercel dashboard, etc.) — the SDK picks them up from process.env automatically, there's no code change:
INNGEST_EVENT_KEY=
INNGEST_SIGNING_KEY=
Without these set in production, the endpoint still exists but Inngest Cloud can't authenticate to it, so the Chaser sweep silently never fires — decisions with deadlines just won't auto-lock or nudge.npm run dev # start the dev server
npm test # run the Vitest suite
npm run lint # ESLint
npm run build # production build
Only the Next.js app itself is containerized — Supabase (Postgres/Auth/Realtime) stays an external hosted project either way, Docker or not, so there's no local Postgres container to wire up here. Everything under Getting started still applies; you still need a real Supabase project, a Gemini key, .env.local filled in, and migrations run against it.
The Dockerfile is a 3-stage build (deps → builder → runner) that ends in a node server.js runtime, not a full node_modules + next start — next.config.ts sets output: "standalone", which traces exactly which dependencies each route needs and copies only those into the final image. Verified locally: ~300MB, serves traffic immediately on start.
One real gotcha, handled for you: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY get inlined into the client JS bundle at build time — unlike every other env var this app uses (service role key, Gemini, Tavily, Inngest), which are read at runtime. So those two specifically have to be passed as Docker build args, not just runtime -e/env_file vars, or the client bundle silently ships with empty values. Both the Dockerfile and docker-compose.yml are already wired for this — you don't need to do anything differently, just make sure they're set in whichever env file you point at.
npm run docker:up # build + run, reading .env.local for both build args and runtime env
# or directly:
docker compose --env-file .env.local up --build
The app is then up at http://localhost:3000.
docker build \
--build-arg NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY \
-t caravan .
docker run -p 3000:3000 --env-file .env.local caravan
Push the built image to any registry (Docker Hub, GHCR, ECR, ...) and run it on any container host (Fly.io, Railway, Cloud Run, ECS, a plain VM with docker run, ...). Set the same server-only env vars from Getting started on the host at runtime, and rebuild whenever NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEY change (they're baked in, not reconfigurable post-build). If you deploy this way instead of to Vercel, you're also responsible for pointing an Inngest app at wherever /api/inngest ends up publicly reachable — that part doesn't change based on how the container itself is hosted.
app/
trips/ # my-trips list, new-trip wizard, admin lobby
trip/[tripId]/
intake/ # the 5 private questions
member-lobby/ # member's pre-open waiting screen
(main)/ # everything behind the trip shell (tab bar + sidebar)
room/ plan/ you/ you/scrapbook/ settings/
destination/ facts/ bookings/ ideas/ cost/
accommodations/ travel-options/
decisions/[decisionId]/
join/[code]/ # invite landing → OAuth → auto-join → redirect
api/trips/[tripId]/... # every mutation: decisions, votes, intake, ideas,
# bookings, accommodations, travel-options, budget-check,
# member removal, scout/planner/quartermaster
components/caravan/ # the whole design system, organized by domain —
# primitives/ layout/ auth/ intake/ decisions/
# plan/ room/ generate/ sharing/ settings/ shared/
lib/
agents/ # Scribe, Scout, Planner, Quartermaster, Chaser,
# Date Outreach, Digest, runtime/ (model, logging, posting)
dates/ # date-solver.ts — the deterministic, duration-aware date-window core
budget/ # budget.ts, cost-flags.ts, trigger-budget-checks.ts
decisions/ # tally-votes.ts, on-decision-locked.ts (the lock → generation
# cascade), refresh-dates-decision.ts, opened-message.ts, decision-titles.ts
accommodations/, travel/ # extract-accommodation.ts, extract-travel-option.ts — Tavily
# + model enrichment (price/area, mode)
search/ # tavily.ts — the one shared Tavily client
facts/, ideas/, bookings/, threads/, trips/ # small domain-scoped helpers
auth/ # session + caller resolution
supabase/ # browser / server / service-role clients
realtime/ # the custom broadcast layer
inngest/ # client, functions (the cron sweep)
validation.ts # every Zod schema
database.types.ts # hand-maintained row types for every table
supabase/migrations/ # 18 migrations, schema history
docs/design/ # PRD, UI spec, and a static HTML hi-fi mockup (screens.html)
npm test
383 tests across 70 files. The test strategy follows the deterministic-core/LLM-shell split: pure logic (lib/dates/date-solver.ts, lib/budget/budget.ts, lib/budget/cost-flags.ts, lib/decisions/tally-votes.ts, lib/agents/chaser-rules.ts, lib/decisions/on-decision-locked.ts, ...) and every API route are unit-tested with mocked Supabase/AI-SDK clients; agent LLM calls are tested for their deterministic surrounding logic (gating, prompt construction, fact filing, error handling), not for model output itself. Verified live against a real trip via Supabase MCP is the additional bar used for every agentic-coordination phase — see the commit history for lib/agents/chaser.ts and lib/agents/scribe.ts. There's no component or end-to-end browser test layer yet — see Known limitations.
Two supported paths — same underlying app either way, same Supabase project, same env vars.
Vercel (no Docker involved — this is what output: "standalone" in next.config.ts is orthogonal to; Vercel builds the app its own way regardless):
NEXT_PUBLIC_SUPABASE_URL / keys at your production Supabase project (run migrations there too)./auth/callback (https://<your-domain>/auth/callback) to Supabase's Authentication → URL Configuration allowed redirects — the Google Cloud Console side doesn't change per-deployment, it always points at Supabase's own callback (see §3 above).INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY and point an Inngest app at /api/inngest (see §5 above) so the Chaser sweep actually fires every 30 minutes in production.Any container host (Fly.io, Railway, Cloud Run, ECS, a bare VM, ...) — build the image per Docker, then follow the same 4 steps above; nothing about Supabase, Google OAuth, or Inngest setup changes based on how the app itself is hosted.
These held across the whole build and are worth knowing before extending it:
lobby until an explicit admin action, and that action itself is gated (3+ people) — no slow trickle-in, no half-empty room making decisions.Kept here on purpose so this stays an honest README, not a marketing page:
nudge_tier), there's no actual email delivery wired up.null rather than blocking the entry — the name and link (if any) always save regardless.MIT — do what you want with it, including in production, with attribution and no warranty.
Yes — generateText |
| Digest | concierge | Chaser's sweep — ≥3h since the last digest and ≥5 new member messages in the group | Recaps recent group discussion and, only when it clearly points to one, adds a concrete suggestion | Yes — generateText |
| Chaser | (system — triggers the agents above) | Cron, every 30 minutes | Pure rule sweep: deadline reminders/auto-lock inside a 24h window, quorum auto-lock the instant everyone's voted, auto-runs Scout / auto-opens the DATES decision once everyone's answered, nudge tiers for non-responders, and decides when Date Outreach / Digest should speak | No — pure rules; delegates all content-writing to the agents it triggers |
| (Q&A) | — | "Ask the agent" from the composer | Answers plain-language questions about costs/dates/who's-said-what strictly from what the deterministic layer already computed — never does its own arithmetic | Yes — generateText |