This document is the product spec for building Portage — the direct booking infrastructure layer for Northern Stay. It covers brand, tiers, fee logic, user types, lead flows, every live page, and every integration still needed.
Portage-facing screens (operator dashboard, guest checkout, white-label sites) use the Portage mark. NS member-facing screens use the Northern Stay mark. The wordmark is always Inter 800, tight tracking. The period is always #c2410c. The "by Northern Stay" sub-label is always Cormorant Garamond italic.
"You see every review before it goes live. You have 72 hours to reach out. If there's a dispute, it stays hidden while we talk to both sides."
/campground/[slug]At the end of the guest checkout flow, a popup presents optional upgrades. Operators configure which add-ons they offer, the price, and the description from their dashboard. The list below is a default template — each operator's actual offerings will differ. Add-on revenue goes directly to the operator; NS does not take a cut beyond normal Stripe processing.
Default add-on categories (operator customises per property)
Implementation notes: Add-ons are configured per-operator in the dashboard. Each add-on has: name, description, price, per-night vs per-stay toggle, max qty per booking, and an active/inactive toggle. The checkout popup is triggered after Stripe payment details are entered but before final confirmation — same timing pattern as Airbnb's add-ons flow. Add-on line items appear in the booking summary email to the guest and in the operator booking notification.
The operator dashboard is the primary surface campground owners interact with. Structure and conventions should follow Airbnb host dashboard, Campspot, and Lodgify — operators coming from these platforms should feel at home immediately. The calendar view is the centrepiece.
Calendar requirements:
Reference implementations: Airbnb host calendar (multi-listing row view), Campspot/Newbook operator portal (site-grid view). This is the screen operators will live in daily — it needs to load fast, work on mobile, and make conflicts obvious at a glance.
How the money actually moves: Guest pays nightly rate + 5.9% service fee at checkout. NS collects the full amount via Stripe. NS pays out the full nightly rate to the operator via Stripe Connect. Stripe deducts ~2.9% + $0.30 from that payout before it hits the operator's bank. NS retains the 5.9% service fee as revenue.
The fee calculator on screen 3 of the intake wizard shows this live. Operator enters a nightly rate, sees the guest-facing total and their own net payout after processing. This is the key conversion screen — operators make their decision here.
| Platform | Host Fee | Guest Fee | Net to Host |
|---|---|---|---|
| Portage. | $0 | 5.9% (guest pays) | Full nightly rate minus CC processing |
| Airbnb | 15.5% | Removed (built in) | ~84.5% of nightly rate |
| Hipcamp | ~15% | Varies | ~85% of nightly rate |
| Campspot | 10%+ | Quote only | Varies |
Sources: Airbnb Resource Centre Oct 2025 · Hipcamp Help Centre · Campspot Marketplace docs
| Lead Type | Entry Point | Trigger | Destination |
|---|---|---|---|
| Membership lead | Any SEO page CTA | Click on Get Started | /get-started quiz |
| Quiz lead | /get-started |
Email submitted on quiz email screen | /api/quiz-lead → Sheets quiz-leads tab → Julia drip email 1 |
| Membership purchase | https://northernstay.com/join/getaway-pass |
Stripe payment_intent.succeeded |
Worker confirmation email → Vercel must activate member account → member to /login |
| Checkout abandon | https://northernstay.com/join/getaway-pass |
Email entered + tab closed before purchase | /api/checkout-abandon → Sheets checkout-abandons → Julia recover email |
| Portage operator | /campground-intake or /portage-claim |
Intake form submitted | /api/portage-intake → Sheets portage-leads → operator confirmation email + admin alert |
| Guest booking inquiry | Campground partner page | Inquiry form submitted | /api/book-inquiry → email to guest + campground → /booking-sent confirmation |
| Gas calculator lead | /gas-calculator-canada |
Email submitted after calculation | /api/calculator-lead → Sheets calc-leads tab → Julia email |
| Post-stay review | Triggered by Angel manually | /api/send-review-request called with booking data |
Review request email to guest → guest submits → 72hr operator dispute window → published or disputed |
All Worker API endpoints post to Google Sheets via postToSheets(). All member emails are sent via SendGrid, signed by Julia. The Stripe webhook at /api/stripe-webhook is built on the Worker side — needs STRIPE_WEBHOOK_SECRET in Cloudflare env and a corresponding trigger on the Vercel side to activate the member account.
| URL | Tag | Purpose |
|---|---|---|
| northernstay.com/get-started | NS Funnel | 3-question membership quiz. Recommends Getaway Pass or Lifestyle. Captures email. Result screen shows campground photos for chosen region. ?tier=getaway and ?tier=lifestyle skip quiz and land directly on result. |
| northernstay.comhttps://northernstay.com/join/getaway-pass | NS Funnel | Membership checkout. Stripe card + Affirm (pending) + internal financing. Postal code tax detection. Consent-gated GA4/Meta/Pinterest/Reddit pixels. Abandon cart recovery on tab close. |
| northernstay.com/portage | Portage | Operator-facing landing page. Top of funnel for campground owners. Brand and offer overview. CTA routes to intake wizard. |
| northernstay.com/campground-intake | Portage | 7-screen operator onboarding wizard. Screen 3: live fee calculator. Screen 4: competitor table. Screen 5: dispute and chargeback coverage. ?park=slug pre-fills property name and shows existing NS listing link on screen 1. |
| northernstay.com/portage-claim | Portage | Alternate entry for operators who want to set up direct online booking. Routes to same intake flow. |
| northernstay.com/campground/[slug] | NS Partner | 80 verified partner pages. Full amenity data, photos, booking inquiry form, membership CTA, schema markup. Bottom of every page: operator claim banner routing to /campground-intake?ref=owner-page&park=slug. |
| northernstay.com/booking-sent | NS Funnel | Booking inquiry confirmation. Shows booking summary, unconfirmed status, next steps. Membership pitch at bottom. |
| northernstay.com/[province]-campgrounds | SEO | Provincial directory pages. Rank for "[province] campgrounds" queries. List partner + directory campgrounds. Drive organic traffic into membership funnel. |
| northernstay.com/gas-calculator-canada | Tool | Gas cost calculator. Ranking ~position 8 nationally. Email capture at end routes to /api/calculator-lead and Julia drip. |
| northernstay.com/great-northern-route | Tool | Halifax to Alaska interactive map. 75 NS campgrounds plotted. Province-by-province route with gas cost estimates per driving leg. Drives membership intent for long-haul campers. |
Every form submission routes through the Worker. Fields below are what get posted, stored in KV, pushed to Sheets, and used to populate emails.
Form fields collected: first_name, email, phone, arrival, departure, guests, rig_type, rig_length, slide_outs, site_type, marketing_consent
On submit fires:
1. Guest confirmation email — from bookings@northernstay.com · Subject: "Inquiry sent to [Park Name]"
Contains: warm opener, "dates NOT confirmed" warning in amber, direct call/email buttons to campground, booking summary table (park, dates, guests, rig), membership pitch in forest green footer.
2. Campground notification email (partner pages) — from bookings@northernstay.com · Subject: "[Booking Referral] [Park] — [Date] — Full rate, paying guest"
Contains: Portage. header, "not a member booking" notice in amber, guest contact card with reply + call buttons, trip details table (arrival, departure, nights, guests, rig, site type), calendar block links (Google, Outlook, .ics download).
3. If marketing_consent = yes: schedules marketing drip sequence via scheduleMarketingDrip()
Form fields collected: property_name, property_type, owner_name, province, email, phone, website, num_sites, tier_interest, current_booking, notes, contract_signed, contract_version, agreed_marketing, agreed_listing, agreed_bookings, agreed_response, agreed_terms
On submit fires:
1. Admin alert to admin@northernstay.com — all fields as a data summary table. Subject: "[Portage Application] [Property Name] — [Province]"
2. Operator confirmation email to owner — Subject: "Your Portage application is in — here's what happens next"
Julia-voiced. Confirms receipt, outlines next steps (review, approval, account setup), sets expectation for timeline.
3. Stored in KV for 90 days under portage-intake/[slug]/[timestamp]
4. Pushed to Google Sheets portage tab with all fields + contract metadata
5. Portage drip record created in KV for follow-up sequence
Form fields collected: email, first_name, province, marketing_consent, nights (A[1]: low/mid/high), pain (A[2]: avail/afford/friction/planning/newbie), travel (A[3]: bc/ab/on/qc/maritimes/all), recommended_plan (getaway/lifestyle)
On submit fires:
1. Pushed to Sheets quiz-leads tab
2. Julia drip email 1 — subject line varies by pain point:
afford → "What $0/night actually looks like"
avail → "How members get the spots everyone else misses"
friction → "One login. Every park. No confusion."
planning → "The simple reason members never stress about camping anymore."
newbie → "Where to start when you don't know where to start"
3. If consent checked: sets localStorage ns_consent = 'all', fires Meta Lead, Pinterest Lead, Reddit Lead, GA4 generate_lead
Data collected: email, first_name, province (from postal code FSA), tier (getaway/lifestyle), payment_method (card/affirm/financing), postal_code
On Stripe payment_intent.succeeded fires:
1. Member welcome email from Julia — Subject: "You're in. Welcome to Northern Stay." (Lifestyle) or "Your Getaway Pass is active." (Getaway). Contains 3 onboarding steps + CTA to /login.
2. Admin alert to admin@northernstay.com — name, email, phone, tier, province, amount, payment ref, yellow callout: "Next step: Add to Vercel booking system"
3. Pushed to Sheets purchases tab
4. Missing: Vercel-side webhook to activate member account. This is the gap — member has paid but cannot log in until Vercel creates their account. Needs STRIPE_WEBHOOK_SECRET in Cloudflare env + corresponding trigger on Vercel.
Abandon cart: if email entered + tab closed before purchase → /api/checkout-abandon → Julia subject: "[Name], your Northern Stay membership is still waiting" → CTA to /get-membership?tier=[tier]. Deduped by email, 24hr TTL.
https://northernstay.com/join/getaway-pass, Stripe fires payment_intent.succeeded to /api/stripe-webhook on the Worker. The Worker handles the confirmation email. What's missing: a corresponding trigger on the Vercel side that activates the member account so the buyer can log in and book. Need your webhook endpoint and what the account creation payload expects.
/login. That currently lands on a cold login page. It needs to land on a proper onboarding sequence — welcome, what to do next, how to make their first booking. This is the highest-value moment in the entire funnel and it's currently a dead end.
postToSheets(). The following tabs need to exist in the sheet: quiz-leads, calc-leads, gas-leads, financing-applications, purchases, checkout-abandons, portage-leads. Confirm sheet ID with Erin.
admin@northernstay.com.
localStorage.getItem('ns_consent') === 'all' on both /get-started and https://northernstay.com/join/getaway-pass. Events: quiz_complete, generate_lead, begin_checkout, InitiateCheckout, Purchase. Not yet on the Vercel app side — purchase events from the Vercel booking flow are not tracked.
/resort/[slug] pages on Vercel are client-side rendered. Googlebot sees a loading state. These pages need SSR or static generation to be indexable. This is blocking organic search traffic to individual campground pages on the Vercel app side.
localStorage or URL params so the sales team doesn't lose context mid-call. Small fix, high daily impact.
The Managed tier gives operators a white-label booking website under their own brand and domain (e.g. book.murphyscamping.com). Portage infrastructure runs underneath — NS invisible to guests. Operator's logo, colours, and name on the front end.
Questions for Mike:
If custom domain deployment is straightforward, it becomes a strong Managed tier selling point — the operator doesn't need to build or host anything, they just point their domain and it goes live. If it requires per-operator builds, flag the operational cost to Erin before pricing the Managed tier.