# Deployment — demo + landing pages > One page. Two services. ~30 minutes from "code complete" to > "URL the user can hit." Every step here is from-scratch reproducible > on a clean laptop. > **Version**: 1.0 · **Adopted**: 2026-05-01 This doc covers the **two distribution surfaces** that ship to public URLs: the Streamlit demo (the iframe target) and the Cloudflare Pages landing pages (the marketing surface that embeds it). The *paid* product — PyInstaller installers, code-signing, Gumroad listing — is covered in `docs/NEXT-STEPS.md`. --- ## Part 1 · Deploy the demo (Streamlit Community Cloud — free) ### A. Pre-flight (one-time, ~2 min) You need a free [Streamlit Community Cloud](https://streamlit.io/cloud) account. Sign in with the GitHub account that hosts this repo. ### B. Deploy (~5 min, mostly waiting for the Cloud build) 1. **Push the repo to GitHub** (private or public — both work). The important files are at the **repo root**: - `streamlit_app.py` — Cloud auto-detects this; nothing to configure - `requirements.txt` — Cloud installs from this - `.streamlit/config.toml` — Cloud honours this - `samples/demo/*.csv` + `*_pipeline.json` — the demo's data - `src/` — the engine 2. In Streamlit Community Cloud → **New app**: - Repository: your fork - Branch: `main` - Main file path: `streamlit_app.py` (the default — leave it) - App URL: `datatools-demo` (or any free subdomain) - **Deploy** 3. First build is 2–3 min while Cloud installs `pandas`, `phonenumbers`, `rapidfuzz`, etc. Subsequent deploys are < 30 s. ### C. Verify Open the deployed URL. Append `?p=shopify-pet` to the URL bar — the persona-specific demo loads. Try `?p=bookkeeper` and `?p=revops` to confirm all three personas route correctly. Click **Run pipeline**; the AFTER preview should appear within ~1 second. ### D. The output URL The deployed URL is what feeds into `landing/deploy.config.json` → `demo_base_url`. Without trailing slash. For example: https://datatools-demo.streamlit.app ### E. Migration trigger Per `BUSINESS.md` §9 / `DEMO-PLAN.md` §9, migrate to a $5–10/mo VPS when: - Streamlit Community Cloud rate-limits / sleeps too aggressively, OR - the demo crosses ~5 k page-views/month (free-tier capacity) The migration is one command if you containerise: `docker run -p 8501:8501 -v $(pwd):/app python:3.12-slim …` --- ## Part 2 · Deploy the landing pages (Cloudflare Pages — free) ### A. Pre-flight (one-time, ~5 min) You need: - A Cloudflare account (free) and a domain (any registrar) with nameservers pointed at Cloudflare. **OR** skip the custom domain step and use the auto-generated `*.pages.dev` URL. - A Gumroad listing URL (placeholder until your account is set up — use `https://gumroad.com/l/datatools` and update it later). ### B. Build the deploy-ready bundle (~30 sec) ```bash # One-time: copy the template cp landing/deploy.config.example.json landing/deploy.config.json # Edit it with your real URLs edit landing/deploy.config.json # Build python3 landing/deploy.py # → produces landing/dist/ ``` `landing/deploy.config.json` is **gitignored**; your real URLs never hit the repo. ### C. Deploy (~3 min) Two paths — pick one: **Drag-and-drop (zero CLI):** 1. Cloudflare Pages dashboard → **Create project** → **Direct Upload** 2. Drag `landing/dist/` into the upload zone 3. Project name: `datatools` (becomes `datatools.pages.dev`) 4. Click **Deploy** **Wrangler CLI (one command, scriptable):** ```bash npm install -g wrangler # one-time wrangler login # one-time wrangler pages deploy landing/dist ``` ### D. Custom domain (~5 min, optional) Pages dashboard → your project → **Custom domains** → add `datatools.app` (or whichever apex domain you registered). Cloudflare auto-issues TLS. Once propagated: - `https://datatools.app/` → apex chooser - `https://datatools.app/shopify-pet/` → Shopify landing - `https://datatools.app/bookkeeper/` → Bookkeeper landing - `https://datatools.app/revops/` → RevOps landing ### E. Verify For each persona: 1. Open the persona URL. 2. Confirm the demo iframe loads (the URL inside it points at the Streamlit demo from Part 1). 3. Click "Run pipeline" inside the iframe → AFTER preview appears. 4. Click the "Get DataTools" button → opens Gumroad with the correct `?from=` query (verify in the URL bar). If the iframe shows "Refused to connect", check Cloudflare Pages → **Settings** → **Functions** for any CSP that disallows Streamlit's domain. (Default Pages config does not set CSP, so this is rarely an issue.) --- ## Part 3 · Updates The cycle is: ```bash # 1) Edit code or copy edit landing//index.html edit src/gui/app_demo.py # 2) Rebuild landing python3 landing/deploy.py # 3) Re-deploy landing wrangler pages deploy landing/dist # 4) Re-deploy demo git push origin main # (Streamlit Cloud auto-deploys on push) ``` Both surfaces deploy in under 5 minutes end-to-end. --- ## Part 4 · Sanity checks (post-deploy, ~3 min) Run these once, then trust the build (per `POST-LAUNCH.md` §6): ```bash # Landing pages serve and reference the right demo URL curl -s https://datatools.app/ | grep -c persona-card # → 3 (one per persona card) curl -s https://datatools.app/shopify-pet/ | grep -c "datatools-demo" # → ≥1 (iframe src points at your demo) # Demo responds and routes the persona param curl -s https://datatools-demo.streamlit.app/?p=shopify-pet | grep -c "Shopify" # → ≥1 # Sitemap is valid XML and lists all 4 pages curl -s https://datatools.app/sitemap.xml | grep -c "" # → 4 ``` --- ## Part 5 · Cost ceiling check | Service | Tier | Cost | Cap | |---|---|---|---| | Cloudflare Pages | Free | $0 | 500 builds/month, unlimited bandwidth | | Streamlit Community Cloud | Free | $0 | 1 GB RAM, sleeps after 7 days idle | | Custom domain | Cloudflare or registrar | ~$15/year | n/a | | GitHub | Free for private repos with limited collaborators | $0 | n/a | | **Total ongoing** | | **~$1.25/mo** (domain only) | | Well inside the `BUSINESS.md` §9 cap of $1,200/mo recurring. The $5–10/mo VPS migration is a contingency only — don't pre-build it. --- ## Troubleshooting **Streamlit Cloud build fails with "ModuleNotFoundError: src.core"** `streamlit_app.py` puts the repo root on `sys.path` before invoking the demo module — but only if the file is at the repo root. Confirm `streamlit_app.py` lives at `/streamlit_app.py`, not nested in a folder. **Cloudflare Pages deploy succeeds but persona pages 404** The directory layout is preserved by `deploy.py`. Confirm your `landing/dist/` has `shopify-pet/index.html`, etc. — not just three flat files. If you used drag-and-drop, drag the **directory**, not its contents. **The iframe shows "X-Frame-Options denied"** Streamlit Community Cloud allows iframe embedding by default. If you've migrated to a self-hosted demo with a reverse proxy, set `X-Frame-Options: ALLOWALL` (or remove the header entirely) for the demo's domain. **Gumroad URL has no `?from=` parameter when clicked** The `&from=` query param is added by the landing-page CTA, not by Gumroad. If it's missing, the landing-page HTML wasn't substituted — re-run `python3 landing/deploy.py` and re-deploy.