Skip to content

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:

  1. Looks up the active token for the booking
  2. Returns https://my.stayonthesnow.com/p/{token}{subPath} if a token exists
  3. 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
  4. Last-resort: returns the bare my.stayonthesnow.com/ URL

Where it’s wired

PhaseOld URLNew URL
T-14 welcomeforms/pre-checkin?b=N, etc./p/{token}/pre-checkin, /check-in, /check-out
T-3 / T-1 remindersforms/pre-checkin?b=N/p/{token}/pre-checkin
T-0 with codesforms/checkin?bookingID=N/p/{token}/check-in
T-0 hard nudgeforms/pre-checkin?b=N/p/{token}/pre-checkin
M4d late-checkout pickerforms/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=N token-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

Terminal window
# Spot-check a real booking
curl -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