Easy-to-manage touch screens
Free, open-source touch screens. Guided USB setup, a drag-and-drop editor and local control. Made for Home Assistant.
Proponete le mete, votatele con gli amici e fate girare la ruota: Roamie è la bucket list di viaggio gratuita per scegliere insieme il prossimo viaggio.
Translation pending
LifestyleTypeScript · JavaScriptGitHub @IvanAP-TAOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Decide your next group trip, together. Live at roamie.org
Translation pending
Taken from the repository README
Loading the full README.
Read the full 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.