Email cutover (M6)
P12.M6 swapped guest-facing email links from the legacy
/forms/{name}?bookingID={id} URLs to the portal
https://my.stayonthesnow.com/p/{token}/{slug} form.
What changed
The portalUrl(env, bookingId, subPath, legacyFallback) helper in
src/lib/guest-token.ts is the single source of truth. It:
- Looks up the active token for the booking
- Returns
https://my.stayonthesnow.com/p/{token}{subPath}if a token exists - Falls back to
legacyFallback(an old-style URL) if no token — important during the cutover window for in-flight bookings whose workflow hasn’t issued a token yet - Last-resort: returns the bare
my.stayonthesnow.com/URL
Where it’s wired
| Phase | Old URL | New URL |
|---|---|---|
| T-14 welcome | forms/pre-checkin?b=N, etc. | /p/{token}/pre-checkin, /check-in, /check-out |
| T-3 / T-1 reminders | forms/pre-checkin?b=N | /p/{token}/pre-checkin |
| T-0 with codes | forms/checkin?bookingID=N | /p/{token}/check-in |
| T-0 hard nudge | forms/pre-checkin?b=N | /p/{token}/pre-checkin |
| M4d late-checkout picker | forms/late-checkout?b=N | /p/{token}/addons |
Token issuance
BookingFlagWorkflow.run() issues the token as its first non-fetch
step:
await step.do("issue-guest-token", async () => { await issueOrGetGuestToken(this.env, bookingId, booking.departure);});issueOrGetGuestToken is idempotent — returns the existing token if
already issued. Safe to run on every workflow respawn.
The daily-flag-sync cron re-spawns workflows for any in-flight bookings that don’t have an alive workflow, so every active booking gets a token within 24h of the cutover deploy.
What still uses legacy URLs
- Bill-facing emails (arrival-day, departure-day, still-dirty,
M4c decide) — all admin-side. They keep
/admin/...URLs. - Cleaner-facing messages —
/clean-ack?b=Ntoken-protected separately; not affected by the guest-portal cutover. - Channel-specific OTA thread sends (Airbnb confirm) — currently plain text without URLs; nothing to update.
Fallback path during cutover
If a guest somehow has an email pointing at the legacy URL (e.g., from
before M6 deployed), the legacy /forms/{name}?bookingID=N route
still works against stayonthesnow-pms.minnetonka.workers.dev. P12.M7
will retire those routes after a ~30-day burn-in.
Verification
# Spot-check a real bookingcurl -sI "https://my.stayonthesnow.com/p/{token}/pre-checkin"# Should: HTTP 200, content-type text/html# Form action="/p/{token}/pre-checkin" in the body