Deploying Waypack
Everything runs locally today (see the Waypack README). This is the checklist to go to production. Nothing here has been run against real accounts yet.
Paths are relative to projects/waypack/ unless they start at the repo root (supabase/, scripts/, .github/). Waypack shares the Supabase project, Stripe account and GitHub repo with the other projects in this monorepo.
0. Accounts you need
| Service | Used for | Plan |
|---|---|---|
| Cloudflare | Worker (site + portal + API + MCP), KV, Queues, Containers | Workers Paid (Containers + Queues); not needed with device maps (§2a) |
| Supabase | Auth + Postgres + Storage (all files: bundles, uploads, previews, shares) | Free (50 MB per file, 1 GB total) → Pro at launch |
| OpenRouteService | geocode / compute_route | Free key for dogfooding |
| Stripe | Pro subscriptions on the web portal | Standard |
| Apple Developer / Google Play Console | App distribution | — |
| Domain | waypack.app (site, portal, API and MCP on one origin; planet. for the basemap mirror) | — |
1. Supabase
# from the repo root: one Supabase project and migration history for every project (supabase/README.md)
supabase link --project-ref <ref>
supabase db push # supabase/migrationsWaypack's tables are in its own waypack schema (moved out of public on 2026-10-16; source: supabase/schemas/waypack/, migrations via scripts/db_migration.sh waypack <what>), and the Worker selects it per request (Accept-Profile/Content-Profile in lib/db.ts). Users are shared with the other projects, so Waypack creates its rows lazily: there is no sign-up trigger (dropped by 20261016070000_waypack_lazy_user_rows.sql); a missing entitlements row reads as the free plan and Stripe upserts one on purchase. Dashboard → Authentication → Sign In / Providers:
- Email: turn it off. Waypack signs in only with Google and Apple.
- Google (≈15 min): Google Cloud Console → APIs & Services → OAuth consent screen (External; app name, support email, logo,
waypack.appas authorized domain; scopesemail,profile,openid) → Publish. Credentials → Create OAuth client ID → Web application → authorized redirect URIhttps://<ref>.supabase.co/auth/v1/callback. Paste the client ID and secret into Supabase's Google provider. Basic scopes don't need Google's verification review. - Apple (≈30 min, needs the Apple Developer account):
- Identifiers → your App ID
com.hejitech.waypack→ enable Sign in with Apple. - Identifiers → new Services ID (e.g.
com.hejitech.waypack.web) → enable Sign in with Apple → domain<ref>.supabase.co, return URLhttps://<ref>.supabase.co/auth/v1/callback. - Keys → new key with Sign in with Apple → download the
.p8(Key ID + Team ID). - Supabase Apple provider: Client IDs =
com.hejitech.waypack.web,com.hejitech.waypack(web Services ID + app bundle id for the native iOS sheet); Secret Key = a client-secret JWT generated from the.p8(Supabase's Apple docs include a generator). It expires after at most 6 months: put a calendar reminder to regenerate it. Only web/Android Apple sign-in needs it; the native iPhone sheet doesn't.
- Identifiers → your App ID
- URL configuration: Site URL
https://waypack.app; redirect URLshttps://waypack.app/auth/callbackandcom.hejitech.waypack://login-callback.
2. Cloudflare
Live setup (2026-10-05): env.production in services/mcp/wrangler.jsonc → Worker waypack at https://waypack.hejitech.workers.dev (free plan, device maps, no container/queue/R2, previews off; KV auto-provisioned). Cloudflare Workers Builds deploys every push to main:
- Root directory:
/(the repo root: the install needs the root lockfile and workspaces). Every command starts in the Worker's folder:- Build:
cd projects/waypack/services/mcp && npm run build -w @waypack/bundle-schema -w @waypack/trip-sdk - Deploy:
cd projects/waypack/services/mcp && npm run deploy -- --env production - Version (other branches):
cd projects/waypack/services/mcp && npm run upload-version -- --env production(preview URLs share production data)
- Build:
- Build watch paths:
projects/waypack/*,package.json,package-lock.json,tsconfig.base.json, so other projects' pushes don't redeploy Waypack. - Secrets (
wrangler secret put <name> --env production):SUPABASE_SERVICE_ROLE_KEY(the project's secret key),SUPABASE_ANON_KEY(publishable key),SIGNING_SECRET.
The original paid-plan checklist (container, R2 mirror, custom domain) follows for when it's needed.
cd services/mcp
npx wrangler login
npx wrangler kv namespace create OAUTH_KV # put ids into wrangler.jsonc
npx wrangler kv namespace create CACHE_KV
# Files live in Supabase Storage (bucket "waypack", created by the migrations); no R2 bucket for them.
npx wrangler r2 bucket create waypack-basemap # container mode's planet mirror only; connect planet.waypack.app (public)
npx wrangler queues create waypack-tiles
npx wrangler queues create waypack-tiles-dlqEdit wrangler.jsonc:
vars.PUBLIC_URL = "https://waypack.app",vars.SUPABASE_URL = "https://<ref>.supabase.co"vars.TILER_URL = ""(use the container binding),vars.PLANET_URL = "mirror",vars.BASEMAP_PUBLIC_URL = "https://planet.waypack.app",vars.ENVIRONMENT = "production"- uncomment
routesforwaypack.app(serves the site,/account,/api, OAuth and/mcp) - Live previews need a second domain. Previews are agent-written HTML, so they're served from their own origin with no cookies, API or portal. Register a separate domain (e.g.
waypackpreview.com, like GitHub'sgithubusercontent.com), add it toroutes, and setvars.PREVIEW_URL = "https://waypackpreview.com". A subdomain such aspreview.waypack.appalso works (the session cookie is host-only and API writes checkOrigin), but a separate domain keeps previews out ofwaypack.app's same-site context. LeavePREVIEW_URLempty to turn previews off; the Worker refuses aPREVIEW_URLon the same host asPUBLIC_URL.
Secrets:
npx wrangler secret put SUPABASE_SERVICE_ROLE_KEY
npx wrangler secret put SUPABASE_ANON_KEY
npx wrangler secret put SIGNING_SECRET # openssl rand -base64 48
npx wrangler secret put ORS_API_KEY
npx wrangler secret put MIRROR_TOKEN # openssl rand -hex 32
npx wrangler secret put R2_ACCOUNT_ID
npx wrangler secret put R2_ACCESS_KEY_ID # R2 API token with write access to waypack-basemap
npx wrangler secret put R2_SECRET_ACCESS_KEY
npx wrangler secret put STRIPE_SECRET_KEY # sk_live_… (sk_test_… for staging)
npx wrangler secret put STRIPE_WEBHOOK_SECRET # whsec_… from the webhook endpoint below
npx wrangler secret put STRIPE_PRICE_ANNUAL # price_… (recurring, yearly)
npx wrangler secret put STRIPE_PRICE_LIFETIME # optional price_… (one-time); omit to hide lifetime
npm run deploy # builds the tiler image from services/tiler/Dockerfile tooFirst planet mirror: trigger the monthly cron once (npx wrangler triggers / dashboard → "Trigger cron" for 0 4 2 * *). Until it lands (~1–3 h for ~130 GB), extracts read the public Protomaps build directly. Check Protomaps' terms before relying on direct reads in production (design open question 5).
Smoke test against production:
WAYPACK_URL=https://waypack.app WAYPACK_PREVIEW_URL=https://waypackpreview.com node scripts/e2e-preview.mjs # dev sign-in only; run against staging
WAYPACK_URL=https://waypack.app SUPABASE_URL=https://<ref>.supabase.co node scripts/e2e.mjs # dev sign-in is disabled in production: sign in with a real Google/Apple test account and run the agent steps manuallyThen connect for real: claude mcp add --transport http waypack https://waypack.app/mcp, and add it as a claude.ai custom connector (design §6.1 says to test real clients early; MCP Inspector works too).
2a. Device maps (no tiler container)
Until there are paying users, run without the container (DECISIONS #54): set vars.MAP_EXTRACTS = "device" and remove the containers block from wrangler.jsonc. Publishing then cuts nothing on the server: /api/trips/{id}/download returns tiles_status: "device" and device_tiles: { source, build, areas: [{ area_hash, bbox, max_zoom }] }, and the app reads just those tiles from the planet with range requests and writes the .pmtiles itself (apps/mobile/lib/services/pmtiles.dart). Same plan gating as before (Pro gets maps), no map storage in R2, no queue traffic.
- Planet:
PLANET_URL = "latest"reads build.protomaps.com directly. The Worker pins one build for 30 days (so unchanged areas keep theirarea_hashand aren't re-downloaded) and checks daily that it still answers, moving to the newest build when Protomaps drops it. Check Protomaps' terms for direct reads by many devices; the R2 mirror (PLANET_URL = "mirror", needs the container's/mirror) is the fix if it becomes a problem. - Dev:
npx wrangler dev --var MAP_EXTRACTS:device, thenMAP_EXTRACTS=device node scripts/e2e.mjs(needsdart, runs the app's extractor on the first area). - Back to the container later: set
MAP_EXTRACTS = "server", restorecontainers. Trips get server extracts on their next download (ensureTripExtracts). - Compare against the reference:
dart run tool/pmtiles_extract.dart <planet> <bbox> <z> out.pmtiles(apps/mobile), thenpmtiles verify out.pmtiles. Verified tile-for-tile againstpmtiles extract(go-pmtiles 1.31.2).
3. Site + account portal
site/ is deployed with the Worker (Workers static assets), so npm run deploy publishes the landing page and https://waypack.app/account too. Replace the beta mailto: links with TestFlight / Play links when ready.
4. Stripe
Live setup (2026-10-05) on the HEJI TECHNOLOGY account, which other products share: every Waypack object carries metadata.app = "waypack", and the webhook ignores events without it.
- Product
prod_VO2LNvFpem4Obw"Waypack Pro": annualprice_1UNGGjJiNqYDPd3ocrYtlQOR($14.99/yr, lookup keywaypack_pro_annual), lifetimeprice_1UNGGkJiNqYDPd3o2uawnedI($39.99 once,waypack_pro_lifetime). - Webhook
we_1UNGGtJiNqYDPd3oEVoepE0D→https://waypack.hejitech.workers.dev/stripe/webhook, API version2025-09-30.clover(the version the Worker sends), 5 events below. - Customer Portal: Waypack's own configuration
bpc_1UNGGuJiNqYDPd3omLmtc9Tq(not the account default, which other products use); cancel at period end, payment methods, invoices. - Worker: price ids and
STRIPE_PORTAL_CONFIGareenv.production.vars;STRIPE_SECRET_KEY(live) andSTRIPE_WEBHOOK_SECRETare Worker secrets. No GitHub Actions secret is needed: no workflow talks to Stripe. - Owner coupon
3jJS1A6P: 100% off forever, Waypack Pro only; single-use promotion code in~/Credentials/stripe.env(WAYPACK_OWNER_PROMO_CODE). Checkout accepts promotion codes.
Original checklist:
- Dashboard → Products: Waypack Pro with a yearly recurring price ($14.99) →
STRIPE_PRICE_ANNUAL; optionally a one-time "Lifetime" price ($39.99) →STRIPE_PRICE_LIFETIME. - Webhook endpoint
https://waypack.app/stripe/webhook, events:checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,charge.refunded→ signing secret →STRIPE_WEBHOOK_SECRET. - Customer Portal: enable cancel, update payment method and invoice history; set the return URL to
https://waypack.app/account. - Branding, receipts and tax (Stripe Tax) as needed.
- Test (milestone M4): with test keys, subscribe from
/accountusing card4242 4242 4242 4242→ plan shows Pro and existing trips get offline maps; cancel in the portal → back to Free at period end; as a free user, upload a 2nd trip and confirm the agent shows the upgrade link. - Local:
stripe listen --forward-to 127.0.0.1:8787/stripe/webhookand put itswhsec_…inservices/mcp/.dev.vars.
4b. Releases (GitHub Actions)
- CI (
.github/workflows/waypack-ci.yml): pushes/PRs touchingprojects/waypack/,supabase/or the shared root files run Waypack's package builds, typechecks and tests (incl. the SDK's Playwright tests), plusflutter analyzeandflutter test. - Ship a version:
scripts/cut_release.sh waypackfrom the repo root (try--dry-runfirst; settings inprojects/waypack/release.env). It checks main is clean, pushed and green in Waypack CI, bumpspubspec.yaml, and publishes GitHub Releasewaypack-v<major>.<minor>.<patch>marked as the repo's latest (releases up to v1.8.9 were taggedv<version>), which starts:waypack-testflight.yml: iOS build<version> (2000 + run number)→ App Store Connect → TestFlight (apps/mobile/tool/release_ios.sh).waypack-mac-release.yml: Mac build → Developer ID signing → notarization →Waypack.dmgattached to the release (release_mac.sh --attach)./download/macserves it once attached (~20–30 min). Both can also be started from the Actions tab.
- Secrets:
ASC_KEY_ID,ASC_ISSUER_ID,ASC_KEY_P8_BASE64(App Store Connect API key: uploads, signing profiles, notarization),SIGNING_P12_BASE64+SIGNING_P12_PASSWORD(Apple Distribution: HEJI TECHNOLOGY LLC),DEV_SIGNING_P12_BASE64+DEV_SIGNING_P12_PASSWORD(Apple Development: Tianhao Zhou;~/Credentials/tianhaozhou-apple-dev-cert.p12, passwordAPPLE_DEV_CERT_PASSWORDinhejitech.env),MAC_SIGNING_P12_BASE64+MAC_SIGNING_P12_PASSWORD(Developer ID Application: HEJI TECHNOLOGY LLC).,SPARKLE_PRIVATE_KEY(EdDSA key that signs Mac auto-updates; backup~/Credentials/waypack-sparkle-ed25519.key, also in the login keychain under Sparkle accountwaypack) Variables:WAYPACK_SUPABASE_URL,WAYPACK_SUPABASE_ANON_KEY(publishable),WAYPACK_API_URL(project-prefixed so Waypack can move to its own Supabase project; seesupabase/GRADUATION.md). - Waypack owns the repo's "latest" release. Installed Mac apps read
releases/latest/download/appcast.xmland/download/macredirects toreleases/latest/download/Waypack.dmg, so other projects publish with--latest=false(LATEST=falsein theirrelease.env; the script refuses two owners). - Mac auto-updates (Sparkle 2.9.6):
waypack-mac-release.ymlalso attachesWaypack.zip(the notarized, stapled app, zipped withditto) andappcast.xmlto each release, both written byrelease_mac.shand signed withSPARKLE_PRIVATE_KEY. Installed apps readreleases/latest/download/appcast.xml(SUFeedURLinmacos/Runner/Info.plist) once a day and offer the update; the public key isSUPublicEDKey. Never lose or rotate the private key casually: apps in the field only accept updates signed with it. Between publishing a release and the Mac job attaching the appcast (~20–30 min), update checks find no feed and quietly try again later. Builds before v1.5.0 have no updater: those users install the new DMG once by hand. - CI imports exported certificates every run instead of letting Xcode create them: the distribution identity and, for iOS archives, an Apple Development identity. Without the development one, automatic signing (
-allowProvisioningUpdates+ the API key) mints a new "Created via API" development certificate per run, which filled Apple's per-team limit on 2026-10-09 and failed the v1.4.1 archive ("Choose a certificate to revoke. Your account has reached the maximum number of certificates").waypack-testflight.ymlnow refuses to build without the pinned development identity. If the limit is hit anyway, revoke unused "Created via API" development certificates (their keys lived on discarded runners). - The Worker deploys separately (Cloudflare Workers Builds, §2); database migrations are still pushed by hand (
supabase db push).
5. Mobile app
npm run build -w @waypack/trip-sdk && node apps/mobile/tool/bundle_sdk.mjs
cd apps/mobile
flutter build ipa --dart-define=SUPABASE_URL=https://<ref>.supabase.co --dart-define=SUPABASE_ANON_KEY=<publishable> \
--dart-define=API_URL=https://waypack.app
flutter build appbundle …same defines…- iOS: team signing in Xcode, bundle id
com.hejitech.waypack, capability Sign in with Apple (already inRunner.entitlements). Thelocal-testflight-setupskill can script uploads. - Android: create an upload keystore and a
signingConfigs.release(currently signs with debug keys). - The apps are free with no in-app purchases (App Store 3.1.3(f) companion app: no purchase buttons or links in the app). Listing copy, privacy labels and review notes:
docs/store-listing.md. Privacy policy:https://waypack.app/privacy.html.
5b. Mac app (DMG on the site)
Needs the Developer ID Application certificate (in this Mac's keychain) and notary credentials (FA_ASC_KEY_ID + FA_ASC_ISSUER_ID, already set in ~/.zshrc, or a notarytool store-credentials profile in WAYPACK_NOTARY_PROFILE).
cd apps/mobile
tool/release_mac.sh --check
SUPABASE_URL=https://<ref>.supabase.co SUPABASE_ANON_KEY=<publishable> API_URL=https://waypack.app \
tool/release_mac.sh --notarize --upload # → GitHub release mac-v<version> (asset Waypack.dmg); /download/mac redirects to itNeeds gh logged in with write access to the repo. Bump version: in pubspec.yaml for each release (the script refuses an existing tag); /download/mac always redirects to releases/latest/download/Waypack.dmg. There's no auto-updater yet: users re-download from the site.
6. Before launch
- [ ] Field test (design §15): drive into a no-service area with only downloaded data; check the blue dot on a walk.
- [ ] Android emulator/device run of
integration_test/app_test.dart(iOS passes; Android was only build-verified). - [ ] Rate limits in
lib/geo.tstuned to the ORS quota. - [ ] Legal review of
site/privacy.htmlandsite/terms.html(drafts). - [ ] Try real Google and Apple sign-in on a device, the portal and the agent sign-in page (locally only the redirects and dev sign-in are tested).
- [ ] Calendar reminder: regenerate the Apple client secret every 6 months.