Skip to content

Decisions & deviations from the design doc ​

The design (docs/design.md) was originally written with the working name Tripfold (now updated to Waypack). Deviations are recorded here, newest last.

#DecisionWhy
1Name: Waypack (waypoints + packed to go). App IDs com.hejitech.waypack; domain placeholder waypack.app; MCP at mcp.waypack.app/mcp; SDK path /__waypack/sdk/v1/waypack.js; global window.Waypack; npm scope @waypack/*.Owner asked for a real name under com.hejitech.*. Search-and-replace waypack/Waypack to rename.
2Local-first, deploy-ready. Everything runs locally (wrangler dev, supabase start, Docker tiler, Flutter simulator). No cloud resources created; docs/DEPLOY.md lists the secrets/commands.Owner choice.
3Tiler on Cloudflare Containers (services/tiler), invoked from the Worker's queue consumer via a Durable-Object-backed container binding. Dockerfile is portable to Fly/Cloud Run.One vendor with Worker/R2/Queues.
4Ajv runs at build time (standalone code, bundled with esbuild) instead of at runtime.Workers forbid new Function/eval. Same validator runs in CLI, Worker and browser.
5MapLibre GL JS v6 is ESM-only and spawns a separate worker module. The SDK ships waypack.js (IIFE) plus waypack-worker.js and calls setWorkerUrl().Keeps everything same-origin and offline; satisfies CSP worker-src 'self' blob:.
6Multiple map extracts → one basemap source via a custom wpk:// protocol that serves each tile from the first PMTiles archive covering it.Avoids duplicating ~100 style layers per extract.
7Glyphs: Protomaps Noto Sans fontstacks (~14 MB) ship with the SDK. CJK/Hangul labels are rendered from device fonts (localIdeographFontFamily), so they work offline without glyph PBFs.Offline labels without a 100 MB font payload.
8Basemap max zoom is 15 (Protomaps builds stop there); max_zoom: 16 in a manifest is accepted but clamped by the tiler, and MapLibre overzooms.Matches the data.
9Planet source: daily builds at https://build.protomaps.com/YYYYMMDD.pmtiles (listed at build-metadata.protomaps.dev/builds.json). Dev/preview read them directly via HTTP range; production mirrors monthly to R2 (services/tiler mirror job) per design §6.6.Open question 5: mirror for production, direct for dev. Check Protomaps' usage terms before relying on direct reads in production.
10waypack preview proxies range requests to the planet so the page stays same-origin under the real CSP.Faithful preview of in-app behavior.
11Manifest gains optional nav_app (google/apple) and place hours/cost. Extra properties are allowed by schema.Interview asks for preferred navigation app; template displays hours/cost. Additive, non-breaking.
12Routing/geocoding default provider is OpenRouteService (per design), behind an interface; the Worker also supports a keyless dev mode (Nominatim + OSRM demo servers) when ORS_API_KEY is unset.Lets the full flow run locally without keys. Public demo servers are not for production.
13Local server auth: per-launch token passed once as ?k= on the first navigation, exchanged for an HttpOnly; SameSite=Strict cookie scoped to the loopback origin.The SDK uses absolute /__waypack/... paths, so a path-prefix token wouldn't work.
14MCP endpoint is a small hand-written stateless JSON-RPC server (services/mcp/src/mcp/protocol.ts, Streamable HTTP with JSON responses) instead of agents' McpAgent.agents@0.26 pins @modelcontextprotocol/sdk@1.30.0 while the SDK is migrating to v2; a tools-only server needs ~150 lines, has no Durable Object/session state, and is fully unit-tested. OAuth is still @cloudflare/workers-oauth-provider.
15MCP sign-in = Supabase email OTP (6-digit code), verified server-side on the Worker's /authorize page, plus a 30-day signed session cookie so later connections are one tap. Apple/Google sign-in exist in the app; accounts match by email. Superseded by #29.No client JS, works in every browser the agent opens, and identical to the app's sign-in.
16Uploads and downloads go through the Worker with HMAC-signed, expiring URLs (PUT /upload/:id, GET /files/<key> with Range) instead of R2 S3 presigned URLs. download-urls lives in the Worker (GET /api/trips/:id/download), not a Supabase Edge Function.Works identically in wrangler dev (no S3 endpoint locally) and production; one place owns R2 access. Bundles ≤ 25 MB fit Worker request limits.
17Extracts retired by a manifest change are marked skipped; expired is reserved for the 30-day post-trip expiry, which entitled users can re-cut on demand when downloading.Keeps on-demand re-cut from resurrecting areas the trip no longer uses.
18Local Supabase uses ports 554xx (supabase/config.toml).Another local Supabase project on this machine owns the default 543xx ports.
19RevenueCat entitlement id pro; products waypack_annual, waypack_lifetime. Webhook is idempotent on event id and, when REVENUECAT_API_KEY is set, refreshes from RevenueCat's subscriber API (authoritative) instead of trusting event order. Superseded by #25.Design §11, renamed.
20App local server uses dart:io HttpServer directly instead of shelf.Needs only static files + Range + cookies; fewer deps, easier to audit the security checks.
21The SDK ships in the app as one asset (assets/sdk.zip), unpacked to app support storage once per SDK version.Flutter assets aren't recursive; ~1,000 glyph files with spaces in paths are awkward to declare.
22flutter_inappwebview 6.2.0-beta.3 — the stable 6.1.5 Android plugin fails on AGP 9 (proguard-android.txt).Move to stable 6.2.x when released.
23Local downloads record the owner (Supabase user id); another account signing in on the same device doesn't see them.Privacy on shared devices.
24Native "Map" overlay is a built-in SDK page (assets/app/map.html) served at /t/{trip}/__waypack_map.html, not a native map widget.Same offline renderer and styling as bundles, zero extra native map deps.
25Billing moves to the web with Stripe; the mobile apps are free viewers. The account portal (site/account.html) offers Stripe Checkout (Pro annual subscription; optional lifetime one-time price) and the Stripe Customer Portal. The Worker's /stripe/webhook verifies signatures, deduplicates on event id, and writes entitlements (source = 'stripe'). RevenueCat, the in-app paywall and purchases_flutter were removed.Owner direction (2026-10-03): what costs money is the cloud storage + map extraction behind MCP, so users subscribe on the web. App Store Guideline 3.1.3(f) (free companion to a paid web service) requires no purchase UI or calls to action in the app: the app only shows the current plan.
26One origin, one deploy: the Worker serves the landing page + portal (site/, Workers static assets), the app/portal API, OAuth and MCP at https://waypack.app (MCP URL https://waypack.app/mcp).Simplest deployment; the portal's session cookie and the MCP authorize page share one sign-in.
27Portal auth: email code → signed HttpOnly SameSite=Lax session cookie (same cookie as the MCP authorize page). Cookie-authenticated mutations require an X-Waypack: 1 header plus a same-origin Origin, so cross-site forms and scripts can't trigger them.CSRF protection without tokens; the API is never CORS-enabled.
28Upgrading cuts offline maps for trips already published on the free plan (on the Stripe webhook, and lazily when the app requests a download).Otherwise users would need to ask their agent to re-publish after paying.
29Sign in with Google or Apple only. No email/password and no email codes, anywhere. App: native Sign in with Apple sheet on iOS (signInWithIdToken + nonce, so no rotating secret is needed for the app) and a system auth pop-up (flutter_web_auth_2 + Supabase PKCE) for Google, and for Apple on Android. Web portal and MCP authorize page: Worker-side PKCE (/auth/start → Supabase → /auth/callback) with a browser-bound state cookie (login-CSRF), then the shared session cookie.Owner direction (2026-10-03): one-tap sign-in, minimal setup, no email sign-in. Apple is required next to Google in the iOS app (App Store guideline 4.8), and must also be on the web so Apple "Hide My Email" users can reach the portal and connect agents.
30Dev-only sign-in for local development and automated tests: POST /api/auth/dev and a form on the authorize page, which the Worker refuses unless ENVIRONMENT=development on a loopback PUBLIC_URL. The app shows it only in builds with --dart-define=DEV_SIGN_IN=true. It mints a real Supabase session via the admin API.Real Google/Apple can't run in CI or on this machine without your credentials. Production has the Email provider disabled.
31Email + password was tried (commit eb130a2) and reverted in favor of #29 (Google + Apple).Owner decision (2026-10-03) after comparing costs: Google/Apple sign-in is $0 per user (Apple needs the $99/yr developer membership the iOS app requires anyway). It sends no email and leaves password attacks, bot sign-ups and credential stuffing to Google/Apple, whereas email + password exposes the password-reset email to abuse (cost and sender reputation).
32Add to calendar without calendar permissions. The app uses add_2_calendar (system "new event" sheet: the user confirms each event; iOS asks at most once for write-only "add events" access and the app never reads the calendar). Google Calendar uses a calendar.google.com/calendar/render link with local wall-clock times plus ctz, so the event lands at the right local time whatever the phone's time zone. Without end_time, an event runs until the next timed item (max 3 h), or 1 h for the day's last item.Works offline for Apple; no read access to the calendar; Google's API would need OAuth scopes for a one-off add.
33Themes are data, not code: manifest.theme = preset (CSS variable set in assets/theme.css) + scene (layers drawn by assets/scene.js as inline SVG). Agents may still restyle freely.Agents get a destination-specific look by choosing words, not by writing SVG; the app can read the same accent to tint native screens; stays fully offline.
34Responsive breakpoints in the template: 760px (two columns) and 1100px (side rail + pinned map). The validator's wide-screen, theme and calendar checks are warnings, not errors.Future iPad/desktop clients; warnings keep older bundles valid.
35Mac app = the same Flutter app built for macOS, shipped as a notarized DMG from the site, not the Mac App Store. Free viewer, no purchases (same rule as iOS).Owner decision (2026-10-04). One codebase; all plugins except add_2_calendar already support macOS. The DMG avoids App Store review for the Mac.
36DMGs live in R2 (releases/mac/Waypack-<version>.dmg + latest.json) and are served by the Worker at /download/mac (302 to the versioned file, immutable caching, Range support).Workers static assets cap files at 25 MiB; the DMG is ~36 MB. Same bucket and serving code as trip downloads; one deploy.
37Mac Add to calendar opens Calendar.app with a one-event .ics, since macOS has no system "new event" sheet for third-party apps without calendar access. SDK Waypack.platform() gains "macos" (additive), which picks Apple Maps / Apple Calendar by default.Works offline and needs no calendar permission. Existing bundles keep working: unknown platforms fall back to the browser wording.
38No Mac auto-updater yet.Ship first; add Sparkle (EdDSA-signed appcast next to latest.json) once there are users to update.
39Live previews. Agents push work-in-progress bundles (push_preview, changed files only) to a per-trip draft; the user opens an unguessable link (/t/<32-char token>/) on any device and open pages reload themselves on each push (keeping tab and scroll). publish_preview turns the draft into a normal trip version through the same pipeline as uploads. Drafts never reach the app, never count toward trip limits and never cut offline maps (pages use the online basemap through a same-origin Range proxy).Owner direction (2026-10-04): let the user watch and redirect the plan while it's built, from any client, including chat agents with no shell.
40Previews are served from a separate origin (PREVIEW_URL, e.g. waypackpreview.com; http://localhost:8787 locally vs 127.0.0.1 for the site). The Worker routes by host before OAuth (run_worker_first: true): the preview origin serves only previews, the SDK and the tile proxy, with the app's CSP + frame-ancestors 'none', Referrer-Policy: no-referrer (the token never leaks to linked sites) and no cookies.Previews are agent-written HTML; on the main origin they could act with the viewer's session.
41Draft storage: content-addressed blobs in R2 (previews/<user>/<id>/blobs/<sha256>) + a path → sha256 map in trip_previews. A push writes new blobs, validates the whole draft (issues reported, never blocking), then swaps the map with a conditional update on rev (concurrent pushes merge instead of overwriting). Blobs used by neither the current nor the previous revision are deleted.Atomic for viewers, cheap incremental pushes, no R2 copy operations needed.
42Live reload = polling /__waypack/preview/<token>/state every 3 s while the tab is visible (15 s after 30 quiet minutes), not WebSockets.Simple and stateless; a Durable Object push channel can replace it if previews get heavy use. Workers KV is unsuitable (up to 60 s stale).
43Preview limits: 4 MB per push_preview call (zip path via create_upload {preview: true} for larger), 25 MB / 2,000 files per draft, 10 open drafts per account, 20 pushes/min and 600/day, expiry 14 days after the last push (cron).Bound abuse of free HTML hosting; generous for real plans (examples are 0.1–1 MB).
44Public, remixable trips. share_trip snapshots a published version into its own content-addressed blobs (shares/<user>/<id>/) served read-only on the preview origin at /t/<token>/, with a "Plan this trip" button to PUBLIC_URL/remix/<token>. The remix page builds a prompt for the viewer's own agent ("Open in Claude" via claude.ai/new?q=, or copy); the agent reads the plan with get_shared_trip (manifest without route geometry + page text). One live share per trip: sharing again moves the same link to the current version. Deleting the trip stops sharing it.Owner direction (2026-10-04): growth loop where every shared trip can turn into a new one; agents plan better from a proven trip.
45Shares are redacted automatically (booking/confirmation/PNR-style codes containing a digit become ••••; the manifest's trip_id is dropped), and the owner is warned that names, private numbers and notes are not detected. Agents can share a cleaned copy (share_trip { files }).Regex redaction is a safety net, not a guarantee; the explicit confirm + cleaned-copy path covers the rest.
46Remix count = get_shared_trip reads (an atomic increment_remix SQL function), not published remixes. Shared pages stay noindex. Owners are never shown to viewers.Counting publishes would need a manifest field linking trips; reads are a good enough signal for now. Indexing and author credit can come with a gallery later.
47Travel companions. The owner's invite link (/join/<24-char code>, reusable, 14 days, can be turned off) lets companions join after signing in; they become viewers (trip_members): the trip appears in their app and portal ("Shared with you by …"), they can download it with the owner's offline maps, and they can leave. They can't change, share, delete or see other companions; the owner can remove them. Max 8 per trip. Not exposed as MCP tools (owners manage companions in the app or portal).Owner direction (2026-10-04): the whole group should have the plan, not one phone.
48Offline handoff = QR + local HTTP, not Multipeer Connectivity / Nearby Connections. The sender zips its stored version and serves it on the LAN (HttpServer on 0.0.0.0, random port) behind an 8-character code, for 15 minutes or until the screen closes, shutting down after 20 wrong codes. The QR carries the addresses, the code and the SHA-256 of the offer; the receiver verifies the offer, then every file, and installs atomically like a download (received_nearby). Typed address + code works without a camera (Mac, simulators).Works across iPhone, Android and Mac (Apple's and Google's P2P frameworks don't interoperate); a Personal Hotspot gives a LAN with no signal; reuses the app's existing download/install path.
49Offline trip assistant on-device only. One Dart interface (lib/assistant/engine.dart) with one channel protocol implemented natively: Apple Foundation Models on iOS/iPadOS/macOS 26+ (WaypackAssistant.swift, shared by both Xcode targets) and Gemini Nano via ML Kit's GenAI Prompt API 1.0.0-beta4 on Android (WaypackAssistant.kt; minSdk raised 24 → 26). Devices with neither get a plain "not available on this device yet" screen. A downloadable open model (Qwen on LiteRT-LM) slots in later as another AssistantEngine in AssistantEngines.candidates(); the picker already supports "downloadable" engines and keeps the most helpful reason.Owner direction (2026-10-04). Answers must work with zero signal; no server, no cost per question, nothing leaves the device.
50Small-model grounding is done in Dart, not by the model. Each question gets a compact brief (≤ 7,000 chars ≈ 1,800 tokens; both models have ~4k context): now/next in the trip's time zone, today (+ tomorrow for time questions, + named days), "most relevant" places (named in the question or matching its category, with phone/address/hours), other places with straight-line distance + direction from the GPS fix, emergency contacts (first for urgent questions, never trimmed) and guide passages (kept with their headings; plan-B questions also match backup words). The question appears before and after the notes; instructions are plain and short. Tuned against Apple's real model with apps/mobile/tool/assistant_eval/ (11 cases, expected facts): 11/11 on repeated runs.Small models echo ALL-CAPS headings, grab the first number they see and over-apply rules ("give the emergency number first" on every answer); these were measured and fixed, not guessed. Residual risk: paraphrase drift (e.g. "sledding" where the plan said "gentle play"), hence the on-screen "Answers can be wrong" note.
51Revisions are agent-led, not modeled. No schema for "options" or "bookings": the agent decides what an update ("we booked X", "add a day", "the road is closed") implies. The product makes that cheap and safe: the skill's generic "Updating an existing trip" procedure (find → start from the latest files → change everything affected, keep the rest → preview with a note → publish on approval); get_trip returns the latest published manifest.json + index.html (other files on request, whole-zip download_url for CLI agents); push_preview { trip_id } starts the preview as a copy of the latest published version (re-based if a newer one was published elsewhere), so agents push only changed files; note tells viewers what changed ("Updated: …" toast, portal).Owner direction (2026-10-04): updates have endless shapes and agent judgment improves with models; a structured options model would cap both.
52The assistant sees the whole plan, whatever its keys. Manifest keys are suggestions for agents (the app acts on the ones it knows), not a description every trip must fit, so the assistant doesn't depend on them: PlanIndex walks the entire manifest generically (any key, nested lists split into entries, ids/coordinates hidden, coordinates under any spelling kept for distances) plus the page's passages with their section paths, and searches it (BM25 + name boost + question-word expansion such as fever → medical/hospital). Apple Foundation Models get a searchPlan tool (the model searches as it needs; Swift calls back into Dart for results); Gemini Nano has no tool calling in the Prompt API, so the same search runs up front and pre-fills the notes. Computed facts stay computed: now/next in the trip time zone, today's schedule, GPS distance/direction. Measured with tool/assistant_eval/run.sh (in-app, real model, 14 cases incl. 3 about keys the app has never seen): 14/14 with tool calling and 14/14 with pre-filled results (both on Apple's model).Owner direction (2026-10-04): the assistant should get all information, as context or through a lookup tool. ~4k-token models can't take the whole plan, so search is the way to "all information"; the eval found and fixed distraction failures (raw route numbers, the wrong backup plan) by returning fewer, location-filtered results.
53Downloadable fallback model on Android: Qwen3-1.7B (dynamic int4, 977 MB) on LiteRT-LM 0.17.1 (WaypackLocalModel.kt, channel waypack/assistant/litert, same protocol as the system engines). Dart tries Gemini Nano first, then this one; a ready engine beats one that needs a download. Offered on 64-bit phones with ≥ 5.5 GB RAM (deviceTooSmall otherwise). The file is pinned to a Hugging Face commit, downloaded in-app with HTTP Range resume, checked for free space, and verified by SHA-256 before use. It runs on the GPU when LiteRT can use it, else on the CPU, thinking turned off, instructions at the top of the user turn (this model's chat template rejects a system message), and it is unloaded after 3 idle minutes.Gemini Nano covers few phones (e.g. not the owner's OnePlus Open). Eval on the Mac's CPU with the same 14 assistant prompts: Qwen3-1.7B int4 10/14, ~14 s per answer; Qwen3.5-2B int8 (2.1 GB) got stuck repeating itself on several answers and was slower, so it was rejected. Apache 2.0, ungated. Costs: about 22 MB of native code per ABI in the APK. Follow-ups: test on a real phone, WorkManager for downloads that survive the app closing, iOS devices without Apple Intelligence.
54Offline maps can be cut on the phone (MAP_EXTRACTS=device). The container approach stays (default server), but the owner won't pay for Containers until there are paying users. In device mode the Worker sends the trip's areas (device_tiles) and the planet URL; the app plans each extract from the planet's directories, fetches only those tiles with merged range requests (≤4 MB, 6 in flight), and writes a clustered PMTiles v3 file (lib/services/pmtiles.dart), hashed locally so reuse and companion handoff work unchanged. Output matches pmtiles extract tile-for-tile (Sequoia z15: 1,021 tiles; Bay Area z15: 81 MB in ~7 s, 107 requests).Owner direction (2026-10-04): personal project, no fixed costs before revenue. The agent can't do it (most MCP clients have no shell; MCP can't carry 100 MB files; uploads would be unverifiable), the phone can. Trade-offs: needs the app in the foreground while the map builds (no background session for range requests), and many devices reading build.protomaps.com directly (check their terms; the R2 mirror is the fix).
55Online map in the app, on by default, with a Settings switch. /download always names an online basemap (online_tiles_url: ONLINE_TILES_URL, else the planet maps are cut from), for every plan. The app's loopback server proxies range reads at /__waypack/tiles/{trip}/online.pmtiles (Range required, so the planet is never streamed whole; 502 when offline), keeping the bundle CSP at connect-src 'self'. Downloaded extracts are tried first per tile, so the online map only fills in outside them or when none exist. Settings → Maps → "Use online map" (off = downloaded maps only, e.g. to save roaming data).Owner request (2026-10-04). Before this, the app had the plumbing but never set the URL, so trips without an offline map showed places on a blank background.
56Files live in Supabase Storage, not R2. Bundles, uploads, previews and shares go to the private waypack bucket (migration 20261009000000_file_storage.sql), written only by the Worker with the service key. src/lib/bucket.ts implements the slice of R2's bucket API the code used (head/get with range/put/delete/list), so call sites didn't change; Storage's list is one folder deep, so a service-role-only storage_list RPC lists keys by prefix. R2 remains only for container mode's optional planet mirror (BASEMAP).Owner direction (2026-10-04): data and files in one place. Trade-offs: Supabase free plan caps files at 50 MB (bundles are ≤ 25 MB) and 1 GB total, and egress counts against Supabase's quota where R2's was free. Server-mode map extracts can exceed 50 MB; they need Pro if the container comes back.
57Mac DMGs ship as GitHub release assets (supersedes #36). release_mac.sh --notarize --upload creates release mac-v<version> on the public repo with the DMG as Waypack.dmg; the Worker's /download/mac is a 302 to github.com/<repo>/releases/latest/download/Waypack.dmg (MAC_RELEASES_REPO, default tianhaoz95/waypack). No storage, no latest.json, no API calls.Owner direction (2026-10-04). The fixed asset name keeps one stable link; release history and checksums live on GitHub. Only Mac releases use GitHub releases (phones ship through the stores), so "latest" stays the Mac build.
58"Scrapbook" home screen. Trips are polaroids on kraft paper: upcoming in a 2-up grid (more columns on wide screens) with washi tape and handwritten captions (Caveat, OFL, bundled in assets/fonts), past trips in a smaller 3-up "Remember when…" grid. Status is a stamp in the photo corner (✓ offline, Update, progress ring, Get = download). Tap opens an offline trip, otherwise shows the trip's actions; long-press always does (Open, Download/Update, Share, Bookmark, Delete), replacing the card buttons and swipe actions. Trips without a cover image get a drawn landscape in their theme colour. Dark mode keeps the polaroids light on a dark kraft background.Owner pick (2026-10-08) from ten candidates in a design review. Two-up polaroids fit about four trips per screen versus one and a half full-width cards.
59The whole app is a scrapbook (extends #58). Every screen uses the shared widgets in lib/widgets/scrapbook.dart and the app theme scrapbookTheme(), which replaces the blue Material seed with kraft paper, coral accent and sticky-note snack bars. Rules: Caveat only for titles and captions, Inter for body and buttons; objects stuck on the page (polaroids, sticky notes, ticket stubs, the QR code, confirm notes) stay light paper in dark mode, surfaces (cards, sheets, buttons) follow the theme; coral = primary, green = ready/on, amber = update, brick red only for destructive. Per screen: sign-in photo pile; long-press sheet with the trip's polaroid as header; trip view keeps the bundle untouched except a taped paper ⋯ button; trip menu as Today/Map/Ask/Share paper tiles plus a short list; Today as a polaroid "now" plus ticket stubs on a dashed trail; full-bleed map under a floating title pill (map controls stay top-right); Ask with sticky-note questions and "from your plan" answer cards; model download as a "before you go" note; share sheet as postcards (for anyone / for fellow Waypackers); QR hand-off as a polaroid; receive scanner inside a polaroid frame; Settings as index cards; confirms as taped notes.Owner picks (2026-10-08) from a 14-screen design review, option A everywhere. The home already shipped in this style; stock Material blue on every other screen made the app feel like two products.
60Ticket-stub logo and app icon (replaces the blue folded-map icon). A paper ticket tilted −10° on coral (#D9694F): notches and a dashed tear line, a handwritten "W" on the stub (Caveat Bold, outlined into brand/logo.mjs so rendering needs no font) and a coral map pin on the ticket. Still one source (brand/logo.mjs → node brand/build.mjs) for iOS, macOS, Android (adaptive + themed monochrome with the W and pin cut out), favicons and the site logo; opaque iOS icons are flattened onto coral.Owner pick (2026-10-08) from 20 scrapbook candidates. The blue icon was the last piece of the old look after #59; the ticket matches Today's ticket stubs, keeps the pin, and its wide silhouette stays readable down to 29pt.
61Landing page in the scrapbook style (follows #59/#60). site/index.html uses the app's palette: dotted kraft background, paper cards, a sticky-note eyebrow, washi tape on the hero phone and Caveat for the wordmark, section headings and captions. The accent is a deeper coral (#B4523A light, #E88468 dark) so white button text stays readable. Caveat is self-hosted (site/fonts/caveat.woff2, Latin subset, OFL) rather than loaded from Google Fonts, keeping the page free of third-party requests. The account portal (account, join, remix), privacy and terms pages, the server-rendered sign-in/consent pages (services/mcp/src/pages.ts) and the website feedback widget use the same palette (2026-10-08).Owner request (2026-10-08) after the new logo: the coral ticket icon sat next to blue buttons.
62One top bar per trip, controls pre-built (replaces the floating ⋯ button over trip pages). The page declares <header data-waypack-bar> and only styles it and sets its title; trip SDK 1.1 (served by the app, so app releases update every trip) pins it first in <body> (sticky, safe-area padding) and adds ‹ back and ⋯ trip menu in the app plus ☰ once the page calls Waypack.onMenu(). Layout-critical CSS is !important so pages can restyle but not move or hide it. No bar → the SDK adds a plain one (old downloaded trips' .topbar is adopted); no SDK at all → the app shows the old floating ⋯ after the page loads. The validator warns when data-waypack-bar is missing; the app's own map page opts out with data-waypack-no-bar.Owner request (2026-10-08). Agent-generated layouts are unpredictable, so a floating native button always ended up covering something; owning one strip of the page makes overlap impossible and adds a visible back button.
63Store-style trip listings in the portal, with optional agent media (instead of viewing trips live on the web or rendering screenshots on the server). manifest.listing = {tagline, cover, screenshots[{src, caption}]} with files under listing/: PNG/JPEG/WebP/SVG, ≤ 1.5 MB each, ≤ 5 MB total, ≤ 6 screenshots; the validator sniffs real types, checks sizes and shape, and rejects SVGs with scripts, handlers, foreignObject or remote URLs. On publish the Worker stores listing files beside the bundle (bundles/…/vN/listing/…) and strips them from bundle.zip, so the app never downloads them; a raster listing cover doubles as the app card cover when cover_image is unset. Updates built from get_trip (which has no listing files) keep the previous version's copies; referenced files with nothing to keep are dropped from the stored manifest, so a missing listing file is only a validator warning. The portal (/api/trips → listing: signed 6 h URLs + a manifest digest) shows each trip as a listing card and a store-style sheet: hero cover, icon, tagline, facts strip, phone-frame screenshot carousel with a lightbox, "About this trip", lodging, and the existing companions/share/delete actions. Media is only ever shown via <img>, and SVGs are served with default-src 'none' and nosniff. No media → a cover drawn from theme (the app's landscape) and no screenshot section. waypack screenshot <dir> [--manifest] captures the template's sections at 390×844 (2× JPEG via Playwright, else npx playwright PNG).Owner decision (2026-10-08): viewing private trips on the web needs bearer links to agent-written pages, and server-side capture needs paid browser time; agent-made media costs nothing, can be more expressive than screenshots, and degrades gracefully. Chat agents are steered to an SVG cover plus tagline (base64 images cost too many tokens).
64The account portal has three tabs: Trips · Set up · Settings. Trips (default) holds the trip listings and in-progress previews, with a getting-started guide when there are none and a note when a free account is at its active-trip limit; Set up holds Connect your AI agent and Get the app; Settings holds plan & billing, API tokens and the account. Tabs are hash routes (#trips, #setup, #settings); older links keep working (#connect → Set up, #tokens/#billing → Settings, #trip=<id> → that listing), a plan chip in the header links to billing, and banners such as checkout success show on every tab.Owner decision (2026-10-09). Trips are the reason to visit and were below billing; connecting an agent is onboarding, not a setting, so it gets its own tab rather than hiding under Settings.
65Sparkle 2 auto-updates for the Mac app (direct-download build). Sparkle 2.9.6 via Swift Package Manager on the Runner target, SUFeedURL = github.com/tianhaoz95/waypack/releases/latest/download/appcast.xml, EdDSA public key in Info.plist, automatic daily checks that ask before installing; "Check for Updates…" in the app menu and an "Updates" section in Settings (Mac only, over the waypack/updater channel). The app stays sandboxed: SUEnableInstallerLauncherService plus mach-lookup exceptions for com.hejitech.waypack-spks/-spki (written literally, since release_mac.sh re-signs with the raw entitlements file). release_mac.sh signs Sparkle's nested helpers innermost-first, then after notarizing and stapling zips the app, signs it with sign_update (tools pinned by checksum) and writes appcast.xml from the GitHub release notes; mac-release.yml uploads both with the DMG. Debug builds never update. Verified end to end before release: a sandboxed, Developer ID-signed build 900 downloaded a notarized build 901 from a local feed and installed it on quit, and the result passed Gatekeeper.Owner request (2026-10-09). The Mac app ships outside the App Store, so without an updater users stay on whatever DMG they downloaded. Feed on GitHub's latest-release URL rather than the Worker domain, which isn't final yet.
66Trip gallery: templates from trips people actually took (owner pick, 2026-10-09, "Magazine + postcards + map" from reports/marketplace-mock). A template is a dateless, nameless plan made from a trip whose dates have passed (trip_templates: card for the gallery, cleaned manifest for agents; one per trip). The owner's agent debriefs them (kept / would cut / surprised; at least one note required), calls draft_template (always a draft, also after edits), and publish_template only after the owner approves; owners can also publish, take down and delete on the template page or in the portal. Server-side: only the month traveled is kept, travelers become a crew shape, traveler notes, trip id and route geometry are dropped, booking codes masked. Discover = magazine home (trip of the week rotating weekly among the five most planned, automatic collections, most planned) + search shown as postcards or on a stylised map (Natural Earth 1:110m land, public domain, no tiles) + template page (postcard, notes, route map of the stops, "Plan this trip" prompt → get_template). Same in the app (compass button on the home screen). search_templates lets agents offer proven trips during the interview. "Planned from it" counts get_template reads by other users. Filtering happens in the Worker over ≤ 500 published cards; move to SQL when the gallery grows. Lists omit the day plan (smaller payloads).Owner direction (2026-10-09): saves agents iterations, gives inspiration, and the post-trip notes are what no agent can research. Templates are public and indexable; the TRAVELED postmark is only possible because templates require a past trip.
67Price history and booking-timing signals (spec: price-tracking-booking-signals.md). Places gain booking_status, append-only price_snapshots and a computed booking_signal. Agents record prices they actually saw (record_price; no scraping, no ML, no FX) and mark bookings (set_booking_status); suggest_booking_timing answers "book now or wait?". Recorded state lives in place_price_snapshots / place_bookings, not in the bundle, so a price check never mints a trip version; publish and preview merge it into manifest.json (snapshots unioned with any the agent wrote, recorded status wins) and recompute every signal there. Rules (@waypack/bundle-schema computeBookingSignal, deterministic, reasons cite snapshots and dates): past trip / booked / held / mixed currencies → none; note deadline within 7 days → book_now; ≥ 5% above / below the first observation → book_soon / good_time; trip within 21 days and unbooked → book_soon; else stable. Deadlines come from notes with a cue word (ends, expires, until, through, book by, deadline, last day) + YYYY-MM-DD, Oct 11, 11 Oct or M/D. Publishing a version without a place deletes its recorded history. Notes that look like booking codes or card numbers are rejected by the tools and the validator; templates drop all price fields. The starter bundle shows a Prices card in the Guide (hidden when nothing is tracked). Narrows #51: booking status is now modeled, options still aren't.Spec non-goals keep v1 explainable and ToS-safe. Signals must work offline, so the server computes and embeds them; the page only displays. Keeping prices out of versions avoids a new version (and map re-cut) per price check.
68The downloadable Qwen3 model on iPhones without Apple Intelligence (extends #53). Same file (pinned commit, SHA-256), settings (int4, 4,096 tokens, thinking off, instructions in the user turn, unloaded after 3 idle minutes) and channel protocol as Android, but CPU only: on a real iPhone (17, iOS 26) every answer failed on the GPU engine, which the simulator can't create, so evals had only ever run on the CPU; LiteRT-LM's open iOS Metal issues (#2814, #3087, #3913: garbled digits, hangs) also argue against it, in ios/Runner/WaypackLocalModel.swift. LiteRT-LM's official Swift API (0.18.0) is vendored as a local Swift package (ios/LiteRTLM: Google's swift/ sources, Apache 2.0) whose native runtime is Google's prebuilt CLiteRTLM.xcframework, pinned by checksum, so builds don't clone the ~900 MB LiteRT-LM repository. Offered when physicalMemory ≥ 5 GB (6 GB iPhones such as the 14 and 15 qualify; 4 GB ones get deviceTooSmall). An actionable Apple Intelligence reason ("turn it on", "still downloading") wins over offering the download. Model files are excluded from iCloud backup. The Swift (C) API passes message content as a list of parts, which Qwen3's built-in chat template can't concatenate (tried to use + operator on unsupported types string and sequence), so conversations use the same template with list content flattened (chatTemplate). Eval (tool/assistant_eval/run.sh <simulator> --no-tools --litert, iPhone 17 simulator, downloaded through the app): 11/14. Downloads resume with HTTP Range; switching apps pauses them after iOS's background grace period. iPhone only for now: Macs without Apple Intelligence still get the "not available" screen.Owner direction (2026-10-11): their iPhone can't run Apple Foundation Models. Reusing Android's model and runtime keeps one measured setup (DECISIONS #53's eval) instead of a second runtime (llama.cpp, MLX). Costs: a 61 MB framework in the iPhone app (~18 MB compressed). Follow-ups: a background URLSession so downloads survive the app being suspended, measure answers and memory on a real 6 GB iPhone.
69The repo becomes a monorepo; Waypack moves to projects/waypack/. Layout and paths inside it are unchanged (apps/mobile, services/mcp, packages/*, site, skill, docs, …), so intra-project relative paths still work. Shared at the root: supabase/ (one Supabase project and migration history; new projects get their own schema + bucket, see supabase/README.md), npm workspaces (projects/*/packages/*, projects/*/services/*), tsconfig.base.json, .github/actions/apple-signing, and scripts/cut_release.sh <project>, driven by projects/<name>/release.env. Workflows are prefixed per project (waypack-ci.yml with path filters; waypack-testflight.yml / waypack-mac-release.yml run only for waypack-v* tags). Waypack releases are now tagged waypack-v<version> and stay the repo's "latest"; other projects publish with --latest=false. A temporary root services/mcp/ shim forwards Cloudflare Workers Builds' old deploy command.Owner direction (2026-10-15): host many projects, each with a Cloudflare site, Supabase needs and mobile/desktop apps, in one public repo sharing one free Supabase project. Installed Mac apps read releases/latest for updates, so only one project may own it.
70Waypack's tables move from public to their own waypack schema (2026-10-16), like every project in the monorepo: set schema in place (data, grants, RLS, indexes, triggers kept), the two remix functions recreated, declarative source in supabase/schemas/waypack/. The Worker sends Accept-Profile/Content-Profile: waypack; the apps are unaffected (they use Supabase only for sign-in).Owner decision while there are no paying customers: in public, Supabase grants every new table to the anonymous role, so a table without RLS is exposed the moment it exists; one rule for every project; public could otherwise collect other projects' strays. Graduation works either way.

Source available under the PolyForm Perimeter License 1.0.1.