Website (excelrewind.com) + Install Page

Module doc for apps/website, scripts/templates/install.html, and scripts/gen-sitemap.mts — part of the ExcelRewind architecture documentation suite.

1. Overview

apps/website is the public marketing site for ExcelRewind, served at excelrewind.com. Unlike apps/addin-excel and apps/viewer it is not Vite-built — it is a plain folder of static HTML/JS/binary files deployed as-is: wrangler pages deploy apps/website --project-name excelrewind-website --branch master (the deploy:website script in root package.json, one leg of npm run deploy). Because there is no build step, none of its pages can read import.meta.env — the Supabase URL and anon key are hard-coded directly into each script (see §7).

The module has five jobs: (1) the landing page (index.html) that pitches the product and links out to the add-in install page and viewer; (2) a self-serve account dashboard (account.html) that signs a user in with Supabase email-OTP and reads their profile/devices/org/referral/tier data directly via PostgREST + RPC — bare fetch, no Supabase SDK; (3) a static privacy policy and a troubleshooting page for the most common add-in support issue (Excel's add-in cache); (4) first-touch marketing attribution and click telemetry (js/attribution.js) that posts anonymous events to a Supabase table; and (5) hosting the two installer binaries themselves — ExcelRewind-Setup.exe and ExcelRewind-Installer.pkg are committed into apps/website/ and served directly from this origin (not through any build pipeline).

A sixth, related artifact lives outside apps/website: scripts/templates/install.html is a template (placeholders __ADDIN_ORIGIN__ / __VIEWER_ORIGIN__) that scripts/make-manifest.mts fills in and writes to apps/addin-excel/public/install.html at build time — so the actual install page is served from addin.excelrewind.com, not from this module's origin, even though its template lives in the repo next to the website's other pages and its markup links back to excelrewind.com for the installer downloads and js/attribution.js. scripts/gen-sitemap.mts is a small codegen script (npm run sitemap) that regenerates apps/website/sitemap.xml from a hard-coded route list — indexable marketing routes only; app subdomains and /account are deliberately excluded.

Cloudflare Pages headers (apps/website/_headers) apply site-wide: HTML pages are no-cache (always revalidate — a prior incident served a stale account.html from browser cache after a deploy), /account.html additionally gets no-store, and a CSP restricts connect-src to 'self' plus the Supabase project origin.

2. Architecture diagram

flowchart TB
  subgraph PAGES["apps/website (static, deployed as-is to Cloudflare Pages)"]
    IDX["index.html
(landing page)"] ACC["account.html
(self-serve dashboard, bare fetch)"] PRIV["privacy.html
(static policy)"] TS["troubleshoot/index.html
(add-in cache repair guide)"] COMP["companion/index.html
(redirect stub → install.html)"] NF["404.html
(hard-404, disables SPA fallback)"] HDR["_headers
(Cloudflare Pages headers: CSP, cache)"] ROBOTS["robots.txt"] SMAP["sitemap.xml
(generated)"] EXE["ExcelRewind-Setup.exe
(committed binary)"] PKG["ExcelRewind-Installer.pkg
(committed binary)"] OG["og.png
(shared social-card image)"] subgraph JS["js/"] ATTR["attribution.js
(first-touch + telemetry)"] PRICE["pricing.js
(progressive-enhancement pricing)"] end end subgraph GEN["Codegen (build time, outside apps/website)"] TPL["scripts/templates/install.html
(placeholder template)"] MKMAN["scripts/make-manifest.mts
(npm run manifest)"] GENSMAP["scripts/gen-sitemap.mts
(npm run sitemap)"] end subgraph ADDINPUB["apps/addin-excel/public/ (deployed to addin.excelrewind.com)"] INSTALLOUT["install.html
(filled template)"] MANIFEST["manifest.prod.xml"] end IDX -->|"cross-links"| INSTALLOUT IDX -->|"cross-links"| VIEWERAPP["viewer.excelrewind.com"] IDX -->|"footer link"| ACC IDX -->|"footer link"| TS ACC -->|"cross-links"| TS TS -->|"download link"| EXE TS -->|"'Excel on the web' link"| INSTALLOUT INSTALLOUT -->|"download links"| EXE INSTALLOUT -->|"download link"| PKG INSTALLOUT -->|"script src (cross-origin)"| ATTR IDX --> ATTR ACC --> ATTR PRIV --> ATTR TS --> ATTR NF --> ATTR IDX --> PRICE ACC --> HDR PRIV --> HDR COMP -->|"meta-refresh + location.replace()"| INSTALLOUT TPL --> MKMAN --> INSTALLOUT MKMAN --> MANIFEST GENSMAP --> SMAP classDef file fill:#f7f8fa,stroke:#c9cede,color:#1a2333; classDef script fill:#f2edfc,stroke:#7C3AED,color:#3d1a7a; classDef ext fill:#eef7f0,stroke:#3f9e5c,color:#1a4d2b; class IDX,ACC,PRIV,TS,COMP,NF,HDR,ROBOTS,SMAP,EXE,PKG,OG,TPL,INSTALLOUT,MANIFEST file; class ATTR,PRICE,MKMAN,GENSMAP script; class VIEWERAPP ext;

3. Data-flow diagram(s)

Two independent data flows live in this module: outbound telemetry (nobody reads it back on the page) and inbound account data (read/write through PostgREST + RPC under a user JWT).

3.1 Attribution & telemetry (write-only, fire-and-forget)

flowchart LR
  V["Visitor's browser"] -->|"URL query: utm_source, utm_medium, utm_campaign, ref, referrer header"| ATTR["js/attribution.js"]
  ATTR -->|"classify() → source enum
(share/directory/paid/ai/organic/referral/direct)"| LS1["localStorage: rw_attr
{source, utm_campaign} (first-touch, once)"] ATTR -->|"validated ?ref (regex A-Za-z0-9_-, 1-32 chars)"| LS2["localStorage: rw_ref"] ATTR -->|"crypto.randomUUID() fallback Math.random"| LS3["localStorage: rewind.viewer-id
(shared key with viewer app)"] ATTR -->|"HTTPS POST JSON: {viewer_id, event, props, source?, utm_campaign?}
keepalive:true, Prefer: return=minimal"| SB["Supabase REST
/rest/v1/viewer_events"] ATTR -.->|"navigator.doNotTrack / globalPrivacyControl"| OPT["Opt-out check
(no capture, no storage, no send)"] PRICE["js/pricing.js"] -->|"HTTPS POST /rest/v1/rpc/public_tiers"| SB2["Supabase RPC
public_tiers()"] SB2 -->|"JSON: [{key, name, monthly_price, sort_order}]"| PRICE PRICE -->|"innerHTML (esc()'d) replaces #pricing-tiers"| DOM1["index.html DOM"]

3.2 Account dashboard (read/write, authenticated)

flowchart LR
  U["User"] -->|"email"| ACC["account.html"]
  ACC -->|"POST /auth/v1/otp {email, create_user:true}"| AUTH["Supabase GoTrue"]
  AUTH -->|"6-digit code (email, out of band)"| U
  U -->|"code"| ACC
  ACC -->|"POST /auth/v1/verify {type:email, email, token}"| AUTH
  AUTH -->|"JSON: {access_token, ...}"| ACC
  ACC -->|"localStorage: rewind.account-token"| LS["Browser localStorage"]
  ACC -->|"GET /rest/v1/profiles?select=email,tier,status,created_at
Authorization: Bearer <token>"| PG["Supabase PostgREST"] ACC -->|"GET /rest/v1/devices?select=label,last_seen&order=last_seen.desc"| PG ACC -->|"POST /rest/v1/rpc/get_my_org {}"| PG ACC -->|"POST /rest/v1/rpc/my_referral, public_promo_config, my_referrals"| PG ACC -->|"POST /rest/v1/rpc/public_tiers {}"| PG ACC -->|"POST /rest/v1/rpc/redeem_coupon {p_code}"| PG PG -->|"JSON rows (RLS: caller sees only own profile/devices/org)"| ACC ACC -->|"esc() then innerHTML"| DOM2["account.html DOM
(dl/ul fields, referral list, tier cards)"]

4. Process/sequence diagrams

4.1 First page load → attribution capture → page_view event

sequenceDiagram
  participant Browser
  participant AttrJS as "attribution.js"
  participant LocalStorage
  participant Supabase as "Supabase REST (viewer_events)"

  Browser->>AttrJS: page load (index.html / account.html / privacy.html / troubleshoot / 404 / install.html)
  AttrJS->>AttrJS: optedOut() check (DNT / GPC)
  alt opted out
    AttrJS-->>Browser: return immediately, no storage, no network
  else not opted out
    AttrJS->>LocalStorage: read rw_attr
    alt first visit (no rw_attr)
      AttrJS->>AttrJS: classify() from utm_*/referrer host
      AttrJS->>LocalStorage: write rw_attr = {source, utm_campaign}
    end
    AttrJS->>LocalStorage: read/write rw_ref (validated ?ref, first wins)
    AttrJS->>LocalStorage: read/write rewind.viewer-id (random UUID)
    AttrJS->>Supabase: POST /rest/v1/viewer_events {viewer_id, event:"page_view", source, utm_campaign, props:{path}}
    Note over Supabase: fire-and-forget — .catch(()=>{}) swallows failure
    Browser->>AttrJS: click on an anchor (delegated, capture phase)
    alt href contains "install.html"
      AttrJS->>Supabase: POST viewer_events {event:"cta_install_click"}
    else href matches .exe/.msi/.pkg/.dmg
      AttrJS->>Supabase: POST viewer_events {event:"download_started", props:{platform}}
    end
  end

4.2 Account sign-in (email OTP) → dashboard render

sequenceDiagram
  participant User
  participant AccountHtml as "account.html"
  participant GoTrue as "Supabase Auth (/auth/v1)"
  participant PostgREST as "Supabase PostgREST (/rest/v1)"

  User->>AccountHtml: enter email, click "Send code"
  AccountHtml->>GoTrue: POST /auth/v1/otp {email, create_user:true}
  alt request fails
    GoTrue-->>AccountHtml: non-2xx
    AccountHtml-->>User: "Could not send the code. Check the email and try again."
  else success
    GoTrue-->>AccountHtml: 2xx (email sent out of band)
    AccountHtml-->>User: shows code field, "Code sent - check your email."
    User->>AccountHtml: enter 6-digit code, click "Verify"
    AccountHtml->>GoTrue: POST /auth/v1/verify {type:"email", email, token:code}
    alt code invalid
      GoTrue-->>AccountHtml: non-2xx / no access_token
      AccountHtml-->>User: "That code did not match. Try again."
    else code valid
      GoTrue-->>AccountHtml: {access_token, ...}
      AccountHtml->>AccountHtml: localStorage.setItem(rewind.account-token, access_token)
      AccountHtml->>PostgREST: GET /rest/v1/profiles?select=... (Bearer token)
      alt 401 (expired/invalid token)
        PostgREST-->>AccountHtml: 401
        AccountHtml->>AccountHtml: clear token, "Your session expired - sign in again."
      else 2xx
        PostgREST-->>AccountHtml: [{email, tier, status, created_at}]
        AccountHtml->>PostgREST: GET /rest/v1/devices?select=label,last_seen
        AccountHtml->>PostgREST: POST /rest/v1/rpc/get_my_org {}
        Note over AccountHtml,PostgREST: get_my_org failure is swallowed (try/catch) — org card just stays hidden
        AccountHtml->>PostgREST: POST /rest/v1/rpc/my_referral, public_promo_config
        AccountHtml->>PostgREST: POST /rest/v1/rpc/public_tiers {}
        PostgREST-->>AccountHtml: JSON rows
        AccountHtml-->>User: renders Account, Organization, Active devices, Refer & earn, Plans cards
      end
    end
  end

4.3 Coupon redemption

sequenceDiagram
  participant User
  participant AccountHtml as "account.html"
  participant PostgREST as "Supabase PostgREST"

  User->>AccountHtml: enter code, click "Redeem"
  AccountHtml->>PostgREST: POST /rest/v1/rpc/redeem_coupon {p_code: code}
  alt success
    PostgREST-->>AccountHtml: {tier, value}
    AccountHtml-->>User: "Code applied - N month(s) of <Tier>."
    AccountHtml->>AccountHtml: showAccount() re-fetch (refresh tier + tier cards)
  else known error code (invalid_coupon / expired / used_up / already_redeemed / not_authenticated)
    PostgREST-->>AccountHtml: error message containing one of the known codes
    AccountHtml-->>User: friendly mapped message (REDEEM_ERR table)
  else unknown error
    PostgREST-->>AccountHtml: other error
    AccountHtml-->>User: "Could not redeem that code."
  end

4.4 Install / repair flow (Windows)

sequenceDiagram
  participant User
  participant InstallPage as "install.html (addin.excelrewind.com)"
  participant Website as "excelrewind.com"
  participant Excel

  User->>InstallPage: visit (from index.html "Install the add-in" link)
  InstallPage->>InstallPage: OS auto-detect (navigator.platform/userAgent) → scroll to Windows or Mac section
  User->>InstallPage: click "Download for Windows"
  InstallPage->>Website: GET /ExcelRewind-Setup.exe (download)
  Note over Website: attribution.js (loaded cross-origin from excelrewind.com)
fires download_started {platform:"windows"} on the click User->>User: double-click ExcelRewind-Setup.exe alt SmartScreen warning (unsigned binary) User->>User: "More info" → "Run anyway" (documented with SVG callouts on both install.html and troubleshoot/) end User->>Excel: close + reopen Excel Excel-->>User: ExcelRewind on Home tab (installer registered the shared-folder catalog) opt add-in cache broken (post-update) User->>Website: visit /troubleshoot/ alt easiest path User->>Website: GET /ExcelRewind-Setup.exe again User->>User: re-run installer — resets %LOCALAPPDATA%\...\Wef cache, re-registers else manual path User->>User: close Excel, delete %LOCALAPPDATA%\Microsoft\Office\16.0\Wef contents User->>Excel: Insert → My Add-ins → Shared Folder → ExcelRewind → Add end end

5. Interconnections

flowchart TB
  WEB["apps/website
(excelrewind.com)"] SB["Supabase project
tohvepsjfeecjycspocg.supabase.co"] ADDIN["addin-excel
(addin.excelrewind.com)"] VIEWER["viewer
(viewer.excelrewind.com)"] CF["Cloudflare Pages"] STRIPE["Stripe
(billing, referenced not wired here)"] WEB -->|"HTTPS: auth OTP, PostgREST, RPC"| SB WEB -->|"HTTPS: anonymous event insert"| SB ADDIN -->|"install.html script src (cross-origin)"| WEB ADDIN -->|"install.html download links"| WEB WEB -->|"cross-links (Install CTA)"| ADDIN WEB -->|"cross-links (Open the viewer CTA)"| VIEWER CF -->|"static hosting, wrangler pages deploy"| WEB WEB -.->|"pricing copy mentions Stripe/Razorpay Payment Links (not implemented in this module)"| STRIPE
TargetFromProtocol / endpointPurpose
Supabase Auth (GoTrue)account.htmlPOST /auth/v1/otp, POST /auth/v1/verifyEmail OTP sign-in; issues an access token stored in localStorage['rewind.account-token']
Supabase PostgRESTaccount.htmlGET /rest/v1/profiles?select=email,tier,status,created_atRLS read of the caller's own profile row (401 handled → session-expired UX)
Supabase PostgRESTaccount.htmlGET /rest/v1/devices?select=label,last_seen&order=last_seen.descRLS read of the caller's active devices (2-device limit is enforced server-side)
Supabase RPCaccount.htmlPOST /rest/v1/rpc/get_my_orgOrg name + seat usage; empty array/failure hides the Organization card
Supabase RPCaccount.htmlPOST /rest/v1/rpc/my_referral, public_promo_config, my_referralsReferral code, program config (gates the Refer & earn card), and the referee list (lazy-loaded on expand)
Supabase RPCaccount.html, js/pricing.jsPOST /rest/v1/rpc/public_tiersLive tier list (key, name, monthly_price, sort_order, has_members, seat_limit) — single source of truth is public.tiers; js/pricing.js fails silently to the static cards already in index.html, but account.html's renderTiers() has no static fallback — on failure it just clears #tiers to blank
Supabase RPCaccount.htmlPOST /rest/v1/rpc/redeem_coupon {p_code}Applies a coupon to the caller's licence; code passed verbatim, never a tier (server validates)
Supabase PostgRESTjs/attribution.jsPOST /rest/v1/viewer_events (anon key, Prefer: return=minimal, keepalive:true)Anonymous telemetry sink: page_view, cta_install_click, download_started; shares the rewind.viewer-id key with the viewer app's own telemetry
addin.excelrewind.comapps/website/index.html, troubleshoot/index.html, account.htmlPlain link (<a href>)"Install the add-in" / "Add-in help" CTAs point at install.html on the add-in origin
viewer.excelrewind.comapps/website/index.htmlPlain link"Open the viewer" CTA
excelrewind.com (this module)apps/addin-excel/public/install.html (built from scripts/templates/install.html)Cross-origin <script defer src> + download linksThe add-in's install page is hosted on addin.excelrewind.com but loads https://excelrewind.com/js/attribution.js and links to https://excelrewind.com/ExcelRewind-Setup.exe / -Installer.pkg — requires excelrewind.com in the add-in's own CSP script-src
Cloudflare Pagesroot package.json (deploy:website)wrangler pages deploy apps/website --project-name excelrewind-website --branch masterDeploys the folder verbatim — no build step for this module
Stripeprivacy.html (documented, not called from this module's code)n/a herePayment processing is described in the privacy policy and referenced in an index.html comment ("STRIPE/RAZORPAY: replace the install-page hrefs... once Payment Links exist") but no billing call originates from this module

6. Key functions & files

FileFunction / exportWhat it doesWhy it matters
apps/website/index.htmlLanding page: hero, "why not video" comparison, how-it-works, trust section, pricing cards (#pricing-tiers), FAQ, JSON-LD (SoftwareApplication + FAQPage)Pricing cards and JSON-LD offers are a static fallback that must stay in sync with public.tiers by hand if js/pricing.js's live fetch fails or JS is disabled
apps/website/account.htmlapi(path, opts)Thin fetch wrapper: attaches apikey + (if signed in) Authorization: Bearer to every Supabase REST callAll auth/PostgREST/RPC calls in the page route through this one function — the single point where the bearer token is attached
apps/website/account.htmlrpc(fn, args)POSTs to /rest/v1/rpc/<fn>, normalizes the result to {ok, data} / {ok:false, code}code is the raw Postgres error message; redeem_coupon's error branch does a substring match against it (REDEEM_ERR) — a Postgres message-wording change silently breaks the friendly-error mapping
apps/website/account.htmlesc(v)HTML-escapes a value before it's interpolated into innerHTMLLoad-bearing XSS guard — every server-controlled string rendered on this page (device labels, referral emails, tier names, redeem messages) must pass through it; refer-link is the one exception, set via .value not innerHTML
apps/website/account.htmlshowAccount()Orchestrates the post-auth render: profile → devices → org (best-effort) → renderRefer() + renderTiers()Explicitly sets #app's display:'block' inline — the file comments this: a stylesheet rule hides #app by default, and an inline empty string would fall back to that hidden rule (a real field bug they hit and left a comment about)
apps/website/account.htmlrenderRefer()Fetches my_referral + public_promo_config in parallel; hides the whole Refer & earn card unless referrals_enabled and a referral code exist; lazy-loads my_referrals() only when the referee list is first expandedFeature-flags an entire UI section off a live RPC result rather than a build-time flag
apps/website/js/attribution.jsclassify()Maps utm_source/utm_medium/referrer hostname into one of: share, directory, paid, ai, organic, referral, directOrder-sensitive: explicit UTM/ref signals are checked before referrer-hostname heuristics; the AI/SEARCH hostname lists are the only place these are enumerated
apps/website/js/attribution.jscaptureFirstTouch()Writes rw_attr to localStorage exactly once (first touch wins); validates and stores ?ref with a strict regex ^[A-Za-z0-9_-]{1,32}$First-touch semantics are enforced client-side only — a user clearing localStorage resets their attribution
apps/website/js/attribution.jstrack(event, row, props)Fire-and-forget POST to viewer_events with keepalive:true; every call wrapped so it never throwsExplicitly never blocks page load or navigation, including on the download/install click path
apps/website/js/attribution.jsdelegated click listenerCapture-phase listener on document: fires cta_install_click for links containing install.html, download_started for links matching .exe/.msi/.pkg/.dmgComment notes download_started "was never wired" before this — the funnel's Download-started stage previously always read 0
apps/website/js/pricing.jsIIFE, card(t, i)Re-renders #pricing-tiers from live public_tiers RPC results; index 1 (2nd tier by sort_order) is always styled "Popular"Fails silently on any network/parse error — the static cards in index.html are the guaranteed fallback, "never blanked" per the file's own comment
apps/website/js/pricing.jsDESCSHard-coded per-tier description strings keyed by tier key; unknown/new tiers fall back to "Custom plan."The RPC returns whitelisted columns only (no description column) — copy for new tiers must be added here manually or they show a generic line
apps/website/privacy.htmlStatic privacy policy: what's collected (account email, two-tier usage metadata, anonymous viewer telemetry, opt-in bug-report attachments), lawful bases (GDPR), retention (24-month metadata purge), sub-processors (Supabase, Stripe, Cloudflare), and region-specific sections (GDPR/India DPDPA/CCPA)Explicitly states .rewind files and workbook content are never sent to any listed processor — the same never-upload claim the rest of the site repeats; still marked "Draft - pending legal review" in a visible on-page note
apps/website/troubleshoot/index.htmlDocuments the #1 add-in support issue: Excel's sideload cache (%LOCALAPPDATA%\Microsoft\Office\16.0\Wef) going stale after an Office update; offers a "re-run the installer" repair path and a manual Explorer pathExplicitly frames re-running the installer as "the same one-click setup... and it doubles as the repair tool" — the installer is not just first-run, it's the support tool
apps/website/companion/index.htmlDead-URL redirect: meta-refresh + location.replace() to install.html, noindexKept alive only for old bookmarks/SEO after the standalone Companion download was folded into the unified installer — has both a no-JS (meta refresh) and JS path
apps/website/404.htmlStatic not-found pageComment notes its mere presence makes Cloudflare Pages hard-404 unknown paths instead of SPA-fallback-serving index.html
apps/website/_headersCloudflare Pages header rules: no-store for /account+/account.html, no-cache + CSP + security headers for /*CSP connect-src must keep the Supabase origin listed or auth/telemetry/RPC calls break site-wide; script-src/style-src allow 'unsafe-inline' because every page relies on inline <script>/<style>
scripts/gen-sitemap.mtsROUTES (const array)Hard-coded list of indexable marketing routes; writes apps/website/sitemap.xmlManual step — a new indexable page does not appear in the sitemap until someone adds it here and reruns npm run sitemap
scripts/templates/install.htmlTemplate with __ADDIN_ORIGIN__ / __VIEWER_ORIGIN__ placeholders; OS auto-detect script scrolls to the Windows or Mac section; documents the Windows SmartScreen bypass with inline SVG screenshot mockups (Mac Gatekeeper bypass is text-only, no SVG)Not deployed from this module — scripts/make-manifest.mts fills it and writes the result into apps/addin-excel/public/install.html, so it ships on the add-in's origin
scripts/make-manifest.mtsfill(text)replaceAll of the two placeholders using VITE_EXCELREWIND_ADDIN_ORIGIN / VITE_EXCELREWIND_VIEWER_ORIGIN from .envOnly runs for the prod path (!DEV) — dev testers get manifest.dev.xml directly and never see a generated install.html

7. Invariants & gotchas