尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Decide your next group trip, together. Live at roamie.org
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Decide your next group trip, together. Everyone proposes the places they'd love to visit, spreads 10 points across the options, and a wheel weighted by the group's votes picks the destination. Then the group finds the dates that work for everyone and plans the itinerary day by day.
Live at roamie.org · Android app in closed testing on Google Play · Italian, English and Spanish
Roamie is a solo side project by Ivan Pace, designed and shipped end to end: product, web app, Android app, backend, SEO and marketing automation. It was built with an AI coding assistant (Claude Code): I defined the product and the priorities, made the decisions, tested everything on real devices and set up every external service; most of the code was written by the assistant under that direction.
What it covers:
video/automation.Roamie is a working example of how I use AI to ship real software: one person, six weeks from the first commit (13 September 2026), 199 commits, about 36,000 lines of TypeScript, 39 database migrations, 6 Edge Functions, 6 GitHub Actions workflows and 256 automated tests. Almost every commit was written together with Claude Code, Anthropic's coding agent, and says so in its message. The git history is the evidence: every change is small, explained and versioned.
ci.yml), including small tests that guard decisions,
like the one that keeps the inline theme script and its Content-Security-Policy hash in sync.video/automation/copy.mjs and
video/automation/linkedin/posts.mjs.video/automation): Postgres cron (pg_cron) starts a GitHub
Actions workflow that plans a post from the travel guides, has the LLM write the copy, renders a video or a
carousel with Remotion (React), checks the output and publishes it to YouTube Shorts,
Instagram, Facebook and TikTok, with a Telegram alert when a platform fails. It reads Instagram's numbers to favour
the formats that work, without ever dropping the rules that keep the feed varied.Claude Code with MCP servers (Supabase for migrations, SQL and advisors; Vercel for deployments; Make for the LinkedIn scenario), agent skills (Remotion, marketing and directory submissions, design review), memory that persists across sessions, and a built-in browser to check every visible change.
The product decisions and the priorities, what to cut, every account and every secret (the AI never handles keys or passwords), testing on real devices, and the decision log. The AI writes most of the code; I decide which code is worth writing.
© 2026 Ivan Pace. All rights reserved: the source is published so it can be read, not reused (see LICENSE).
The rest of this file is the technical documentation.
| Layer | Choice |
|---|---|
| App | Vite + React 19 + TypeScript |
| UI | Tailwind CSS v4, shadcn/ui-style components on Radix, vaul bottom sheets, Framer Motion, Lucide |
| Data | Supabase (Postgres + RLS, Google Auth, Realtime), TanStack Query |
| Maps | React-Leaflet + OpenStreetMap tiles, Nominatim geocoding |
| Photos | Wikimedia Commons API, with CC attribution always shown |
| Extras | canvas-confetti, dnd-kit (itinerary reordering), vite-plugin-pwa, i18next |
| Motion | View Transitions API, CSS scroll-driven animations, Web Animations; Framer Motion where a spring is needed |
| Automation | GitHub Actions started by Supabase pg_cron, Remotion (videos and carousels), Mistral (copy), Make.com (LinkedIn), Telegram (alerts and hand-posting kit) |
| Team memory | Obsidian vault in a private repository, read and written by a weekly Claude Code agent |
| Fonts | Fraunces + Instrument Sans, self-hosted via Fontsource (no Google Fonts requests) |
npm install
supabase/migrations, in filename order. Use one of these:
npx supabase link --project-ref <ref> and then npx supabase db pushhttps://roamie.orghttp://localhost:5173/** and https://roamie.org/**https://<project-ref>.supabase.co/auth/v1/callbackhttps://roamie.org/privacyhttps://roamie.org/termsemail, profile and openid"Use your email" sends a 6-digit code (signInWithOtp / verifyOtp, src/components/auth/EmailSignIn.tsx); new
addresses get an account on first use, with the name typed next to the email.
smtp.resend.com, port 465, username resend,
password the Resend API key, sender [email protected].{{ .Token }} in the body and the subject instead of {{ .ConfirmationURL }}, so the email carries the code.cp .env.example .env.local
| Variable | Purpose |
|---|---|
VITE_SUPABASE_URL | Project URL. Also written into the Content-Security-Policy at build time. |
VITE_SUPABASE_ANON_KEY | anon / publishable key. It is public by design; RLS protects the data. |
VITE_PUBLIC_APP_URL | Base URL for invite links and the OAuth redirect. https://roamie.org in production; leave it empty locally. |
VITE_CONTACT_EMAIL | Contact shown in the Privacy Policy. It is also sent to Nominatim/Wikimedia to identify the app. |
npm run dev
In development, /__preview renders the trip screens with mock data, so you can review the UI without a Supabase project. Try /__preview/trips, /places, /map, /roulette, /itinerary and /group. This route is stripped from production builds.
| Script | What it does |
|---|---|
npm run dev | Dev server |
npm run build | Typecheck, production build (PWA service worker included), then prerender the public pages and fill in _headers |
npm run preview | Serves dist/ with the production security headers, to check the CSP locally |
npm test | Unit tests plus database tests: the real migrations run on PGlite (Postgres in WASM) to verify RLS, invites, votes, the weighted roulette, hardening and account deletion, with no Supabase needed |
npm run i18n:check | Checks that all locales have the same keys and that every t('…') key exists |
npm run types | Regenerates src/lib/database.types.ts from the live schema (run npx supabase login once). Run it after every migration: the Supabase client is typed with it, so wrong table, column or RPC names fail the build |
npm run icons | Regenerates the PWA icons from public/favicon.svg |
npm run og-image | Regenerates the social preview images in public/og/ |
npm run guide-photos | Makes the AVIF copies of the guides' photos and covers in public/guides/photos/ (existing files are kept) |
npm run bump | Raises the patch version before every push; the version is shown in the footer next to the copyright |
python scripts/fonts.py | Cuts the display fonts to what the design uses: Fraunces upright with its soft/wonky axes pinned, and the logo's 3 KB wordmark (needs pip install fonttools brotli) |
SECURITY DEFINER helpers (is_trip_member, can_edit_trip, is_trip_owner), which avoids recursive policies on trip_members. The helpers aren't callable by anon. Column-level UPDATE grants make ids, trip_id, owner_id and authors immutable.
upload.wikimedia.org) or built-in scenes, enforced by check constraints and again in the client. Avatars can only be Google URLs. Attribution links are https only. This blocks tracking pixels and CSS/HTML injection from other group members.vercel.json sets a strict Content-Security-Policy (no inline scripts, only the hosts the app talks to), HSTS, X-Frame-Options: DENY, nosniff, a restrictive Permissions-Policy, and long-lived caching for hashed assets.get_trip_preview: the only thing anon can call. It returns name, cover and member count.join_trip, regenerate_invite_code, set_points, spin_roulette, delete_accountset_points; 0 removes them). A trigger enforces the budget under an advisory lock, so concurrent requests can't overspend; selected or visited destinations don't count, so a spin gives their points back.checklist_items: to book / to pack, assignable only to trip members, done_at kept by a trigger) and useful links (trip_links: http(s) URLs and notes, no uploads). Members read; owners and editors write. Stops can have a place (same search as destinations) and appear on the map as numbered routes, one colour per day; the itinerary exports to an .ics file (src/lib/ics.ts): timed stops as 1-hour events in floating local time, the rest as all-day events.trips; every member marks days as free or maybe (, written through ). The Dates view shows a calendar coloured by how many are free and the best non-overlapping windows (: a "maybe" counts half); fixing a window sets /.vercel.json sets the build (npm run build → dist), clean URLs, rewrites, headers and caching. Set the four VITE_ variables above in Settings → Environment Variables (Production and Preview); locally they live in .env.local or .env.production, both ignored by git.https://roamie.vercel.app/**) so Google sign-in works there too.roamie.org and www.roamie.org (redirecting to the apex), then point the DNS records at Vercel as instructed.The Supabase free plan has no downloadable backups and pauses projects after about a week without activity.
roamie-backups): dumps contain personal data, and
artifacts of a public repository are downloadable by anyone. It dumps roles, schema and data with the Supabase CLI and
keeps them as a workflow artifact for 90 days; it needs the secret SUPABASE_DB_URL (Supabase → Connect →
Session pooler connection string).supabase-keepalive.yml (here) runs one read-only RPC every day. Add the repository variables SUPABASE_URL and
SUPABASE_ANON_KEY.Test a restore before you need one. Download an artifact, create a second free Supabase project, and run:
psql --single-transaction --variable ON_ERROR_STOP=1 --file roles.sql --file schema.sql --command 'SET session_replication_role = replica' --file data.sql --dbname "<new project connection string>"
roamie.org as a Domain property in Google Search Console (DNS TXT record in Cloudflare), submit https://roamie.org/sitemap.xml, then import the site into Bing Webmaster Tools. Check Pages and Core Web Vitals reports after launch, and use PageSpeed Insights on /.https://roamie.org/ and https://<project-ref>.supabase.co/auth/v1/health.Keep these free-tier limits in mind:
TileLayer URL to another free or self-hosted provider, and update img-src/connect-src in vercel.json.availabilityset_availabilitysrc/lib/dates.tsstart_dateend_datesrc/lib/offline.ts, 20 days: it doubles as the queries' gcTime, and setTimeout overflows above ~24.8 days) for the user's trips, each trip's data, the profile and the passport. Opening a trip loads all its views at once, so it stays readable without a connection. Offline, an expired access token can't be refreshed, so useAuth falls back to the stored session until the network is back. The copy is deleted on sign-out and when another account signs in on the device.proposed with at least one point. The draw is server-side (spin_roulette), weighted by points and serialised with a row lock, so no client can choose the winner. The client animates the wheel to the returned result, and each slice's size equals its real probability.gen_random_uuid() bytes. Regenerating a code invalidates the old link immediately; existing members keep their access.src/entry-prerender.tsx + scripts/prerender.mjs): / (Italian), /en, /es, /privacy, /terms, 404.html and the guides. Each ships its real content, title, description, canonical, Open Graph/Twitter tags and, on the landing, hreflang alternates and JSON-LD (WebSite, Organization, WebApplication, FAQPage).useSeo keeps the same tags in sync on client-side navigation.shell.html (noindex) through the rewrites in vercel.json; unknown paths get a real 404 status. robots.txt is in public/; sitemap.xml is generated from the prerendered routes, with hreflang alternates.src/content/guides/) in three languages, each with its own URL (/guide/…, /en/guides/…, /es/guias/…), hreflang, Article/HowTo/BreadcrumbList JSON-LD, and links from the landing and the footer. Every destination has an "Add to a trip" action that proposes it in one of your trips, with coordinates, mood, budget and a Commons photo.User-Agent, so the app identifies itself with the Referer header and the email parameter.src/lib/viewTransition.ts wraps navigation in a view transition: the shared photo gets its
view-transition-name only during the transition (a named element would switch off the glass blur inside it), moves
by translate and scale (html.vt-morph), and heavy pages render their header first and the rest once the animation
has started (usePageSettled). The app header and the trip and guide covers follow the scroll with
animation-timeline: scroll() (with a one-step fallback where it isn't supported); everything animates transform and
opacity only, and parallax is off with reduced motion.src/content/guides/covers.json holds each guide's cover photo (the photo of its first place, kept
equal by a test; the guides without places have their own; a guide's page opens on its last place instead, so the
first place card doesn't repeat the photo above it), small enough for the landing page to show photos without downloading
the guides. The landing's postcards use their own cropped AVIFs in public/landing/, credited like every photo./passport collects the visited destinations of all your trips (RLS scopes the query) and places you add yourself (visited_places, readable only by their author): countries and continents reached (ISO code from the place search, localized with Intl.DisplayNames), a map, and stamps with ratings, filterable by trip. "I wasn't there" (passport_exclusions) hides a group visit from your passport only. visited_at is kept by the status trigger; members rate visited destinations 1–5 with set_rating (0 removes it). The share button draws a 1080×1920 story image on a canvas in the browser (src/lib/passportCard.ts) and hands it to the system share sheet, or downloads it./p/:slug (set_trip_public): the slug is the trip name plus 8 random characters, so pages can't be enumerated. get_public_trip is the only data anon gets besides the invite preview: trip details, destinations with photo and total points, and the itinerary. Members, individual votes, comments, links and the checklist are never included. On Vercel, api/public-trip.ts serves the app shell with the trip's own title, description, image, canonical and TouristTrip JSON-LD, so link previews and search engines show the trip (CDN cache 60 s, so unpublishing takes previews down quickly).src/lib/climate.ts). Compare mode puts 2–3 destinations side by side (points, best time, budget, mood, comments).api/join.ts, api/og/invite) shows the trip, the places in the running and that a
name is enough. After a proposal, and after the last of your points, a toast offers to tell the group in the chat with
the message ready (useTellTheGroup). New trips show their creator three steps (invite, places, vote) that tick
themselves off (FirstSteps).supabase/functions/telegram-bot is @RoamieTripBot. Adding it from the invite page
(t.me/RoamieTripBot?startgroup=<code>) links the group to the trip; triggers queue what happens in the trip
(telegram_outbox: a place proposed, someone joining, the wheel's pick, dates fixed) and pg_net asks the function to
send it. In groups the bot only receives commands (/mete, /collega, /scollega), never the chat. Needs the
Edge Function secret TELEGRAM_GROUP_BOT_TOKEN; GET …/telegram-bot?setup registers the webhook and the menu.set_my_budget); group_budget returns only the lowest band,
and only once three have answered, so no answer can be told apart. Nobody, the owner included, can read
trip_budgets.public.funnel_report() counts the funnel (real trips are the ones a Google or email account made);
the funnel-report Edge Function serves it to the Monday workflow (report.yml, started by pg_cron), which posts it
on Telegram. The same workflow builds main, runs taste-skill's inspect.mjs (pinned commit) on six public pages
keeping only objective layout rules (video/automation/qa.mjs), and writes funnel, the week's posts and the page
check to the private brain repository as Numeri/<ISO week>.md (brain.mjs, secret BRAIN_TOKEN). Funnel numbers
never land in this public repository.Agenti/Capo.md and Regole.md from the vault: compare the week's numbers, choose one action, write
Piani/<ISO week>.md (a workflow in the brain repository sends its summary to Telegram) and, if needed, open one
pull request here from a capo/… branch after lint, typecheck and tests. It never pushes to main, holds no
secrets and has no connectors.