# Free hosting on Cloudflare Pages This folder (`deploy/cloudflare`) is the **live, published** copy of the site. It keeps the **private inbox** (`inbox.html` + key) and the **anonymous message** promise on Cloudflare's free plan. **Status: live.** Site: . First published 28 October 2026. Cost: **R0**, no card on file. The QR code in `public/` is set to that address, `og:url`/`canonical` point at it, and the three forms post to `/api/submit`, which stores messages in a free Cloudflare D1 database. ``` deploy/cloudflare ├─ public/ the website (index.html, inbox.html, assets) ├─ functions/api/ │ ├─ submit.js receives one message, rate-limits, saves to D1 │ └─ submissions.js inbox API, needs MW_ADMIN_KEY ├─ schema.sql the two database tables ├─ wrangler.toml local dev / terminal deploy settings └─ package.json `npm run dev` / `npm run deploy` shortcuts ``` --- ## Current deployment | Item | Value | | --- | --- | | Pages project | `its-okay-to-not-be-okay` | | Live address | | | Deployment type | Direct Upload (`wrangler`) — no Git repository is connected | | D1 database | `mw-messages` (`55fe7dc7-03fa-4008-b899-0234a7cb8da1`) | | Binding | `DB` → `mw-messages`, declared in `wrangler.toml` | | Secrets | `MW_ADMIN_KEY`, `RATE_SALT` (production) | | Cloudflare account | `bceemashele@gmail.com` | | Inbox key file | `..\..\data\inbox-key.txt` (private, gitignored) | The project was created by Direct Upload, so Cloudflare's Git integration **cannot** be attached to it later — that option is only offered when a project is first created from a repository. All updates go through `wrangler`. ### Redeploying (the normal way to update) From this folder, with Node.js 18 or newer installed: ```powershell cd "C:\Users\bceem\OneDrive\Documents\Default Project\deploy\cloudflare" npx wrangler pages deploy public --project-name its-okay-to-not-be-okay --branch main ``` Wrangler uses the cached login on this computer. On a new computer, run `npx wrangler login` once and click **Allow** in the browser. Notes: - Writes progress to stderr, so PowerShell may show exit code 1 even when the deploy worked. Look for `Deployment complete!`. - Each deploy prints a preview URL (`https://.its-okay-to-not-be-okay.pages.dev`) and updates the main address a moment later. - `--commit-dirty=true` silences the "uncommitted changes" warning if this folder is ever put under git. - The inbox and its messages live in D1, not in the deployment, so redeploying never deletes messages. ### If you ever need to rebuild from scratch 1. `npx wrangler login` 2. `npx wrangler d1 create mw-messages` → copy the `database_id` into `wrangler.toml` 3. `npx wrangler d1 execute mw-messages --remote --file=schema.sql --yes` 4. `npx wrangler pages project create its-okay-to-not-be-okay --production-branch main` 5. `npx wrangler pages secret put MW_ADMIN_KEY --project-name its-okay-to-not-be-okay` and the same for `RATE_SALT` (paste a new random 40-character value at each prompt) 6. `npx wrangler pages deploy public --project-name its-okay-to-not-be-okay --branch main` The dashboard equivalent of steps 2 and 5 is described in the section below. ### Doing it from the dashboard instead Useful if `wrangler` is unavailable: - **Database:** Workers & Pages → **D1 SQL database** → `mw-messages` → **Console** for SQL. To recreate the tables, run each statement in `schema.sql` separately. - **Binding:** Pages project → **Settings → Bindings → Add → D1 database bindings** → variable name `DB` → `mw-messages` → **Save**. - **Secrets:** **Settings → Variables and Secrets → Add** → name, value, **Encrypt**, **Save**. Changing a secret needs a redeploy to take effect. - **Deploy:** Direct Upload projects cannot upload a folder from the dashboard; use `wrangler pages deploy` as above. ### Local preview (optional) You do not need this — the published site is the real test. To look at the layout and QR code on your own computer: ```powershell npm install Copy-Item .dev.vars.example .dev.vars npm run dev ``` Then open . The page, validation and QR code all work offline, but the forms will show their “could not send” fallback because the empty local database has no tables yet. Test the forms against the deployed address instead, where the schema has been applied. `.dev.vars` holds throwaway local values only and is ignored by git. Never put your real `MW_ADMIN_KEY` in a file that gets uploaded. --- ## Everyday use - **Read messages:** the inbox link above (`inbox.html?key=…`). It shows appointments, comments and anonymous messages, with search, filters and a CSV export button. It refreshes when you reopen the tab. - **Keep the key private.** Anyone with the key can read every message. It is not in the website code or the repository — only in your Cloudflare secret and in the link you bookmark. - **Delete a message:** Cloudflare dashboard → D1 → `mw-messages` → Console: ```sql DELETE FROM submissions WHERE ref = 'MW-ABCDE-FGH'; ``` - **Change the key:** Settings → Variables and Secrets → edit `MW_ADMIN_KEY` → redeploy. The old inbox link stops working immediately. - **Back up:** in the inbox, press **Export CSV**. ## What is stored, and what is not | Message | Stored | | --- | --- | | Appointment | name, phone, optional email, date, time, topic, notes | | Comment | optional name, optional email, message | | Anonymous | **message only** — no name, email, phone or device details are sent | - Rate limiting needs the sender's IP. It is hashed with `RATE_SALT` and kept in a **separate `rate_limits` table for 10 minutes**, never linked to a message and never joined to it. Old rows are deleted automatically. - Cloudflare, like any host, may keep standard request logs (which include IP addresses) for its own security and abuse handling. That is outside this site's control and is why the anonymous form says so plainly. - No cookies, no analytics, no third-party scripts, no tracking pixels. ## Free plan limits | Resource | Free allowance | | --- | --- | | Static requests | unlimited | | Function requests (`/api/*`) | 100,000 per day | | D1 storage | 5 GB total | | D1 rows read / written | 5 million read, 100,000 written per day | | Pages builds | 500 per month | A small NPO will not come close. There is no card on file, so nothing can be charged; if a limit were ever reached, the API would stop and the forms would show their “could not send” fallback instead of costing money. ## Updating the site 1. Edit the files in this folder's `public/` directory (that is the copy that gets published; the root-level `index.html` is the local `server.py` copy). 2. Redeploy: `npx wrangler pages deploy public --project-name its-okay-to-not-be-okay --branch main`. 3. Reload the site with a hard refresh (`Ctrl+F5`) to bypass the browser cache. Keep the project name unchanged — renaming it changes the address and breaks the printed QR code and the links in `config.js`, `index.html` and `inbox.html`. ## Changing the address `its-okay-to-not-be-okay.pages.dev` is globally unique and now owned by this project, so it will not be taken away. If the site ever has to move (for example to a custom domain), update these in the published copy first: 1. `public/assets/js/config.js` → `shareLink`. 2. `public/index.html` → `og:url`, `og:image` and `canonical`. 3. `public/inbox.html` → any absolute links, if added later. Then redeploy and reprint posters from the QR code on the site, which follows `shareLink`. ## Troubleshooting | Symptom | Cause / fix | | --- | --- | | Forms say “not connected to a server” | The page is being opened from a `file://` path, or the deploy is missing `functions/`. Open the `pages.dev` address. | | Inbox says the key is not set (500) | `MW_ADMIN_KEY` secret missing, or added after the last deploy — redeploy. | | Inbox says “That key is not correct” (401) | Typo in the key, or the secret was changed. | | Forms fail with 500 | `DB` binding missing or pointing at the wrong database; check Settings → Bindings, then redeploy. | | “Too many messages from this device” (429) | 20 messages in 10 minutes from one IP. Wait, or raise `RATE_LIMIT` in `functions/api/submit.js`. | | Messages vanish after a redeploy | They are in D1, not in the deployment. A missing `DB` binding is the usual cause — messages made while it was broken are lost. | Request failures are also visible in the Pages project under **Functions → Real-time logs** (or **Logs** in newer dashboards) while you submit a form.