Internal research · noindex · checked on the dates shown
How to host Lightfall, and never think about SSL again
Where to put the site, the map tiles and the domain, what it costs each month, how HTTPS certificates get handled, and what has to be true before launch. Every fact has a numbered source you can open. Anything that is arithmetic or my judgement is labelled that way. Nothing here has been set up: no account was created, no money spent, nothing deployed.
The answer, first
Put everything in one Cloudflare account: the site on Cloudflare Workers (its static-file hosting), the four map tile files on Cloudflare R2 (its file storage), and the domain on Cloudflare too. That is already the plan in SPEC §4, and the research backs it. Cloudflare's own docs say "Start new projects with Workers" [1], and Astro's guide agrees [2]. Once the domain is on Cloudflare, SSL is not a job you do: Cloudflare "handles issuance, renewal, and deployment automatically", free on every plan [24].
Why not the usual alternatives: Vercel's free Hobby plan is "restricted to non-commercial personal use only", and processing payment or advertising a product for sale counts as commercial [48]. GitHub Pages is not allowed to run "your online business, e-commerce site" [57]. On Netlify's free plan, running out of credits pauses every site on the account [54].
The site: Workers static assets
- What
- A static Astro site needs no adapter: a small
wrangler.jsoncfile pointing at./dist, built withnpx astro buildand published withnpx wrangler deploy[2]. - Cost
- "Requests to static assets are free and unlimited" and "There is no additional cost for storing Assets" [5]. Workers Paid, if ever needed, is $5/month [4].
- Fits?
- Each file max 25 MiB; 20,000 files per version on Free [3]. The current
dist/is 128 files, the largest about 1.0 MB (checked on disk). The tiles are not indist/. - The catch
- A card-data clause in Cloudflare's terms for Free Services. See risks; it probably does not bite because Shopify takes the card, but that is my reading.
The tiles: R2 with a tiles subdomain
- What
- A bucket holding the four
.pmtilesfiles, served at something liketiles.your-domain.com. R2 supports theRangeheader the map reader depends on [38]. - Cost
- Storage $0.015/GB-month with 10 GB-month free; reads $0.36 per million after 10 million free a month; egress (data sent to visitors) "Free" [37]. About 362.2 MiB of tiles fits inside the free storage (arithmetic).
- Why not in the site
nyc-region-z15.pmtilesis 326.4 MiB, over Workers' 25 MiB per-file cap [3]. No Cloudflare doc says Workers static assets answer range requests either [6][9].- The catch
- How many range requests one map visit makes has not been measured, so the read bill is an estimate (table below).
Domain and SSL: Cloudflare
- What
- Buy the domain at Cloudflare Registrar, or buy it elsewhere and point its nameservers at Cloudflare [32][23]. Then attach it to the Worker as a Custom Domain: "Cloudflare will create DNS records and issue necessary certificates on your behalf" [21].
- Cost
- Registrar charges "what is paid to the registry... No markup" [31]. A third-party tracker lists .com at $10.46 a year to register and renew [34]; Porkbun lists .com at $11.08 [35]. Universal SSL certificates: free [24].
The easiest path, step by step
- Pick and buy a domain. Easiest: Cloudflare Registrar, so DNS is already in the right place [32]. If you buy elsewhere, onboard it: Domains, Onboard a domain, enter the root domain, pick a plan, review DNS records, then paste Cloudflare's nameservers at the registrar [22]. Turn DNSSEC off at the registrar first, and allow up to 24 hours [23].
- Create an R2 bucket and upload the four tile files. Connect
tiles.your-domainas the bucket's Custom Domain (R2, bucket, Settings, Custom Domains, Add) [39], and add a CORS policy so the site may read it [40][41]. Details in the tiles section. - Add one config file,
wrangler.jsonc, pointing at./dist[2]. (A repo change, so it is a suggestion; not made.) - Connect the GitHub repo. Workers & Pages, select the Worker, Settings, Builds, Connect, then push a commit [10]. Set the tile address as a Build variable, because Astro bakes it in at build time [11][18].
- Attach the domain to the Worker. Settings, Domains & Routes, Add, Custom Domain [21]. Add a
wwwredirect rule [29]. - Switch on Always Use HTTPS [26], and consider HSTS with a short max-age [27]. The certificate itself needs nothing from you [24].
- Run the launch checklist: security headers, the tile range test, the card-data question, the real domain in
astro.config.mjs.
What it costs each month
| Item | Price | Lightfall at launch | Source |
|---|---|---|---|
| Domain (.com) | $10.46/year register and renew at Cloudflare (third-party tracker, not an official price); Porkbun $11.08/year | About $0.87 a month spread over a year (arithmetic) | [34][35] retrieved 2026-09-15 |
| Site hosting (Workers static assets) | Static asset requests free and unlimited; storing assets no extra cost. Workers Paid: $5/month minimum, 10 million requests included | $0 on the Free allowance. $5 only if you choose Workers Paid | [5][4] retrieved 2026-09-15 |
| Builds from GitHub | Free: 3,000 build minutes/month, 1 build at a time. Paid: 6,000 minutes, then $0.005/minute | $0; the Free allowance is 3,000 minutes | [12] retrieved 2026-09-15 |
| HTTPS certificates | Universal SSL: "free", on Free, Pro, Business and Enterprise | $0 | [24] retrieved 2026-09-15 |
| Tile storage (R2) | $0.015/GB-month; 10 GB-month free | $0: 362.2 MiB is inside the free 10 GB (arithmetic) | [37] retrieved 2026-09-15 |
| Tile reads (R2 Class B) | 10 million free per month, then $0.36 per million | $0 until reads pass 10 million a month. Not measured; see the table in tiles | [37] retrieved 2026-09-15 |
| Data sent to visitors (egress) | R2 egress "Free" | $0 | [37] retrieved 2026-09-15 |
| Email at the domain (forwarding only) | Email Routing: "Available on Free and Paid plans" (no price stated) | Forwarding to an existing inbox; cannot send as the domain | [77][79] retrieved 2026-09-15 |
| Email you can send from (optional) | Fastmail Individual $5/month billed annually or $6 monthly; Google Workspace Business Starter $7/user/month regular price | $5 to $7 a month if you want to reply as the domain | [82][81] retrieved 2026-09-15 |
Bottom line (arithmetic on the rows above): about $10.46 a year for the domain, and $0 a month while traffic stays inside the free allowances. Realistic ceiling at launch: $5 a month if you take Workers Paid, plus $5 to $7 for email you can send from. One honest gap: no fetched page literally states "the Cloudflare Free plan costs $0"; the docs call it the Free plan and the terms call it "Free Services" [19].
Words used here
- Static site
- Plain files (HTML, CSS, JavaScript, images) built ahead of time. No server program runs when someone visits. Lightfall's build is static.
- Host
- The company whose computers hand those files to visitors.
- CDN (content delivery network)
- Copies of your files kept in data centres around the world, so each visitor gets them from somewhere nearby.
- Cloudflare Workers / static assets
- Cloudflare's app platform. "Static assets" is its feature for serving plain files like Lightfall's.
- Cloudflare R2
- Cloudflare's file storage (a "bucket" of files), used here for the big map tile archives.
- Domain / registrar
- Your address on the web (like lightfall.com). A registrar is the shop you rent it from, yearly.
- DNS
- The internet's phone book. It turns a name into the address of the computer that answers for it.
- Nameservers
- The company whose phone book is in charge of your domain. Pointing nameservers at Cloudflare hands it that job.
- Zone
- Cloudflare's word for one domain added to your account, with all its DNS records.
- Apex (root) domain
- The bare name,
your-domain.com, with nothing in front. - Subdomain
- A name in front of the apex:
www.your-domain.com,tiles.your-domain.com. - CNAME record
- A DNS record that says "this name is an alias of that other name".
- SSL / TLS certificate
- TLS is the encryption behind the padlock and https. The certificate proves the site is really yours. SSL is the old name people still use.
- Certificate authority (CA)
- The organisation that issues certificates, such as Let's Encrypt.
- HSTS
- A header telling browsers "only ever use https for this site", remembered for a set time.
- DNSSEC
- A signature on DNS answers. It must be off while you move nameservers, or the domain can go dark.
- Egress
- Data sent out of a provider to visitors. Many storage services charge for it; R2 does not.
- Byte-range request
- Asking for part of a file ("bytes 1,000 to 5,000") instead of all of it. The map reads small pieces of a 91 MiB archive this way.
- PMTiles
- One big file holding every map tile, read by byte-range requests instead of millions of small files.
- CORS
- A browser rule: a page may only read data from another address if that address says it is allowed. The tiles subdomain counts as another address.
- Preview deploy
- A private test copy of the site at its own URL, built from a branch, that does not change the live site.
- Build variable
- A setting read while the site is being built, then baked into the files. Different from a runtime variable, which a server reads on each visit.
- CSP (Content Security Policy)
- A header listing where a page may load scripts, images and data from, which blunts injected code.
- Headless Shopify
- Lightfall's own pages are the shop front; Shopify runs only the cart, checkout and orders behind them.
Hosting platforms compared
The questions that matter for Lightfall: may a site that sells prints use the cheap tier, where can a 91 MiB tile file live, is HTTPS automatic, and do pushes to GitHub deploy with previews. "Not verified" means the fact-checker did not find it on a fetched page, not that the answer is no.
| Platform | Selling prints on the cheapest tier | Price and allowance | File size / the 91 MiB tile | HTTPS | Git deploys and previews | Over the limit | Source |
|---|---|---|---|---|---|---|---|
| Cloudflare Workers (recommended) | No general commercial ban found. Free Services may not process or collect card information (§2.2.1(h)) | Static asset requests free and unlimited; Paid $5/month | 25 MiB per file; tile goes to R2. Range on static assets not documented | Automatic, free (Universal SSL) with a Custom Domain | Workers Builds: 3,000 min/month free; non-production branches get preview URLs | Limits table: Free 100,000 requests/day. Billing page: static asset requests free and unlimited (so the daily cap reads as applying to Worker code; that link is drawn from the two pages together) | [19][5][3][12][13] retrieved 2026-09-15 |
| Cloudflare Pages | Same account terms as above | Free: 500 builds/month | 25 MiB per file; 20,000 files on Free | Automatic (Cloudflare) | "unlimited number of preview deployments"; Cloudflare now says start new projects on Workers | Builds time out after 20 minutes | [14][1] retrieved 2026-09-15 |
| Vercel | No: Hobby is "non-commercial personal use only"; payment or advertising a sale needs Pro | Pro $20/month with $20 usage credit; Pro 1 TB transfer included, then from $0.15/GB | No per-file limit or Range behaviour documented | Automatic Let's Encrypt, renewed 14 to 30 days before expiry | Git deploys; Hobby cannot connect repos owned by a Git organization | Hobby 100 GB/month included | [48][49][50][51][52] retrieved 2026-09-15 |
| Netlify | Terms not fetched (AUP and subscription agreement not checked) | Free $0 with 300 credits; Personal $9 (1,000); Pro $20 (3,000). Bandwidth 20 credits/GB; each production deploy 15 credits | No per-file limit found; sets Accept-Ranges itself, Range handling not documented | Free automatic HTTPS (Let's Encrypt) | Unlimited deploy previews on Free | All sites on the account paused; Free cannot buy more credits | [53][54][55][56] retrieved 2026-09-15 |
| GitHub Pages | No: not allowed for "your online business, e-commerce site" | Soft 100 GB/month bandwidth | Site max 1 GB. GitHub blocks files over 100 MiB and warns over 50 MiB | Let's Encrypt; "Enforce HTTPS" in Settings, Pages | Deploys time out after 10 minutes; no previews found | Soft limits | [57][58][47] retrieved 2026-09-15 |
| AWS Amplify Hosting | Not verified | Free up to 1,000 build minutes, 5 GB stored, 15 GB served a month; then $0.15/GB served | No per-file limit listed (5 GB build artifact); Range not verified | ACM certificate, 13 months, auto-renews | PR previews; each counts toward 50 branches per app | Pay as you go | [59][60][61][62] retrieved 2026-09-15 |
| AWS S3 + CloudFront | Not verified | CloudFront flat-rate Free $0 (1M requests, 100 GB), Pro $15/month; S3 $0.023/GB | S3 website endpoints have no HTTPS, so CloudFront is required. CloudFront serves and caches Range requests from S3 | Flat-rate plans include a TLS certificate | Not researched | Flat-rate: "no additional overage charges" | [63][64][65][67] retrieved 2026-09-15 |
| Firebase Hosting | Not verified | 10 GB storage and 10 GB/month transfer free; then $0.026/GB stored, $0.15/GB sent | 2 GB per file; Range not verified | Certificate within 24 hours of pointing A records, usually a few hours | GitHub action creates a preview channel per PR and comments the URL | Free (Spark) sites over transfer are disabled until next month | [68][69][70] retrieved 2026-09-15 |
| Render | Not verified | Static sites "free to deploy"; bandwidth Hobby 5 GB, Pro 25 GB, then $0.15/GB | Not retrieved | Free automatic TLS (Let's Encrypt, Google Trust Services) | PR previews with their own URL | Hobby with no card: services spun down until next month | [72][71] retrieved 2026-09-15 |
| DigitalOcean App Platform | Not verified | 3 static-site apps free, 1 GiB transfer each, then $0.02/GiB; more apps $3/month | Not documented; serves via Spaces CDN, which Protomaps lists as HTTP/1.1 only | Issuer not stated on pages fetched | Not documented on pages fetched | Overage billed | [73][74][41] retrieved 2026-09-15 |
Why Cloudflare wins for Lightfall (judgement): it is the only option where the site, the tiles, the domain, DNS and certificates sit in one account with free egress, and the plan SPEC §4 already chose. AWS S3 plus CloudFront is the credible runner-up for tiles (documented Range caching), at the price of a second vendor and more moving parts.
Domain, DNS and SSL, in plain English
When someone types your address, their browser asks DNS where it lives. Your domain's nameservers answer. If those nameservers are Cloudflare's, Cloudflare answers with its own servers, shows the browser a certificate proving the site is yours, and hands over the page over an encrypted connection. So the one decision that makes SSL effortless is letting Cloudflare run the domain's DNS.
Buying the domain
| Registrar | Pricing model | Example prices (register / renew per year) | Worth knowing | Source |
|---|---|---|---|---|
| Cloudflare Registrar | "No markup. No surprise fees." Renews "at the list price set by the registry". Free WHOIS redaction; over 430 extensions | Third-party tracker: .com $10.46 / $10.46; .gallery $22.20; .photography $28.20; .co $30.00; .shop $30.20; .studio $31.20 | Must use Cloudflare nameservers while registered there. Auto-renew on by default. No unicode (IDN) names. Registration can take up to 30 seconds | [31][33][32][34] retrieved 2026-09-15 |
| Porkbun | Retail registrar; free WHOIS privacy and free auto-renewed Let's Encrypt certificates | .com $11.08 / $11.08; .gallery $23.17; .photo $26.26; .nyc $26.26; .photography $29.35; .studio $32.44; .shop sale $2.06, renews $31.41 | You would then onboard the domain to Cloudflare by changing nameservers (below). .nyc eligibility rules not verified | [35] retrieved 2026-09-15 |
| Google Domains | Gone | — | "As of 10 July 2024, all domains have migrated to Squarespace." | [36] retrieved 2026-09-15 |
If the domain is bought somewhere else
- In Cloudflare: Domains, Onboard a domain, enter the apex domain (for example
example.com), choose how to add DNS records, Continue, select a plan [22]. - Review the DNS records Cloudflare found [22].
- At your registrar, turn DNSSEC off first: "Changing nameservers while DNSSEC is active can cause your domain to become unreachable" [23].
- Replace the registrar's nameservers with Cloudflare's, copied exactly: "If their names are not copied exactly, your DNS will not resolve correctly." Then "Wait up to 24 hours" [23]. Full setup is the only option on the Free plan [23].
- Complete the SSL/TLS setup step [22]. Certificates are "issued after your domain is active on Cloudflare"; no issuance time is given [24].
Pointing the domain at the site
- Apex on the Worker: Workers & Pages, select the Worker, Settings, Domains & Routes, Add, Custom Domain, enter the domain. Cloudflare creates the DNS record and certificate. It must be an active zone you own; it fails on a hostname that already has a CNAME record; wildcards are not supported [21].
- www is a separate hostname. A Worker on
example.comdoes not receivewww.example.com[21]. Cloudflare's template redirectshttps://www.*tohttps://${1}with a 301 and the query string kept [29]; the www hostname needs a proxied DNS record for the redirect to run [30]. - tiles subdomain on R2: added from the bucket, not the Worker (see tiles). Universal SSL covers the apex and first-level subdomains like
wwwandtiles; deeper names (a.b.example.com) need Total TLS or Advanced certificates [24].
SSL: what Cloudflare does, and the two switches you own
- Certificates: "free, unshared, publicly trusted", Domain Validated, and Cloudflare "handles issuance, renewal, and deployment automatically" [24]. Validity 90 days; renewal starts 30 days before expiry; "Cloudflare controls the validity periods and certificate authorities" [25].
- Industry change, no action needed: Let's Encrypt moves its default certificates to 64 days on 10 Feb 2027 and 45 days on 16 Feb 2028, and says manual renewal "is not recommended" [28]. With Cloudflare renewing for you, this is background.
- Switch 1, Always Use HTTPS (SSL/TLS, Edge Certificates; every plan): redirects every http request to https. It "does not resolve issues with mixed content" (an https page loading an http file) [26]. The redirect status code is not stated.
- Switch 2, HSTS: max-age from 1 to 12 months, with includeSubDomains, preload (needs 12 months) and No-Sniff options. Warning from the doc: remove HTTPS before max-age runs out and "your website becomes inaccessible to visitors" [27]. My advice (judgement): start at 1 month, leave preload off until the site has run cleanly for a while.
The map tiles: R2, and the alternatives
| File | What it holds | Size | Under 25 MiB? |
|---|---|---|---|
nyc-region-z15.pmtiles | The city and the region round it, zooms 0 to 15 | 326.4 MiB (342,263,784 bytes) | No |
world-z5.pmtiles | The planet, zooms 0 to 5 | 14.3 MiB (15,007,228 bytes) | Yes, but Range support is not documented |
marble-z5.pmtiles | NASA relief and ocean imagery | 10.3 MiB (10,805,021 bytes) | Yes, but Range support is not documented |
clouds-z4.pmtiles | NASA cloud layer | 11.2 MiB (11,761,758 bytes) | Yes, but Range support is not documented |
| Total | All four move together (tile-url.ts) | 362.2 MiB | — |
Why all four go to R2, not just the big one. The only mention of Range in the Workers static-assets headers doc is a cache-control condition [6], and the one match in the asset-worker source is that same condition (utils/headers.ts:60) [9]. No doc says a partial response (206) comes back. R2's GetObject lists "Range" as supported [38]. The code already treats the tile location as one base address for all four files (src/lib/map/tile-url.ts), so splitting them would mean two settings.
Setting up the bucket
- Upload the four files without Content-Encoding. If Cloudflare has to decompress an encoded response "it ignores Range" and sends the whole body as 200 OK [44]. Protomaps suggests rclone for files over 300 MB [42]; these are all under.
- Custom domain: R2, bucket, Settings, Custom Domains, Add, enter the domain, Continue, review the DNS record, Connect Domain, wait for Initializing to become Active. The domain must be a zone "in the same account as the R2 bucket" [39].
- Leave the r2.dev address off: it is "rate-limited and should only be used for development purposes". Disable under Settings, Public Development URL (type "disallow"). Do not point a CNAME at r2.dev: "an unsupported access path" [39].
- CORS policy: R2, bucket, Settings, CORS Policy, Add CORS policy, JSON tab, Save [40]. Protomaps' recommended values are GET and HEAD, headers
rangeandif-match, exposeetag, max age 3000 [41]. Origins arescheme://hostwith no path; changes can take up to 30 seconds, and cached files on a custom domain keep old headers until the cache is purged [40]. - Cache Rule for .pmtiles. Free plans cache files up to 512 MB, but PMTILES is not on the default cached-extension list, so it needs a Cache Rule (Free allows 10) [43][45]. The exact rule settings were not researched. Cloudflare aligns range fetches to 1 MiB cache blocks, and "On a cold cache miss, the first origin request also omits Range" [44].
[
{
"AllowedOrigins": ["https://your-domain.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["range", "if-match"],
"ExposeHeaders": ["etag"],
"MaxAgeSeconds": 3000
}
]Draft, untested: Cloudflare's dashboard JSON shape [40] filled with Protomaps' values [41]. The Wrangler command-line format is different (it wraps rules in {"rules":[...]}) [40]. Add every origin that must show the map (see preview deploys in risks).
What the reads could cost
Protomaps says "each Range tile request will count as a GET" [41]; GetObject is a Class B operation [37]. Nobody has measured how many range requests a Lightfall visit makes, so the numbers below are assumptions, for scale only.
| Assumed range requests per visit | Visits a month inside the free reads | Read cost at 100,000 visits/month | Read cost at 1,000,000 visits/month |
|---|---|---|---|
| 25 | 400,000 | $0.00 | $5.40 |
| 50 | 200,000 | $0.00 | $14.40 |
| 100 | 100,000 | $0.00 | $32.40 |
| 200 | 50,000 | $3.60 | $68.40 |
Latency, a third-party claim: Protomaps writes that R2 "is known to have higher latency (500ms or higher) than other Cloud Storage products, but lower storage and no egress costs" [42]. That is not a Cloudflare measurement. Protomaps' optional Worker in front of R2 serves individual tiles and needs to sit on your own domain for caching [42]; Lightfall reads whole archives, so a plain R2 custom domain matches today's code.
Alternatives for the tiles
| Option | Price | Range / HTTP | Catch | Source |
|---|---|---|---|---|
| Cloudflare R2 (recommended) | $0.015/GB-month, 10 GB free; Class B $0.36/M after 10M free; egress free | Range supported; Protomaps lists R2 as HTTP/2 | Latency claim above; cache rule needed | [37][38][41] retrieved 2026-09-15 |
| Backblaze B2 behind Cloudflare | $6.95/TB/month, first 10 GB free; Class A, B and C calls free; egress through Cloudflare free and unlimited | Protomaps lists B2 as HTTP/1.1 only | A second vendor account | [46][41] retrieved 2026-09-15 |
| AWS S3 + CloudFront | S3 $0.023/GB, GET $0.0004 per 1,000; S3 to CloudFront free; CloudFront Free plan $0 for 1M requests and 100 GB | CloudFront "serves the requested range and also caches it"; S3 is HTTP/1.1 only (Protomaps) | Second vendor; more parts to wire | [67][65][64][41] retrieved 2026-09-15 |
| Inside the Workers site | Free | Range not documented | Impossible for nyc-region-z15: 25 MiB per-file cap | [3][6] retrieved 2026-09-15 |
| In the Git repo | — | — | Git warns over 50 MiB, GitHub blocks over 100 MiB; nyc-region-z15 (326.4 MiB) would be blocked. Still hits the Workers cap | [47][3] retrieved 2026-09-15 |
Deploys from GitHub, and preview links
The goal: push to main and the live site updates; push any other branch and you get a private preview link. Cloudflare's version of this is Workers Builds.
{
"name": "lightfall",
"compatibility_date": "YYYY-MM-DD",
"assets": { "directory": "./dist" }
}Astro's example uses the name my-astro-app; "lightfall" is my suggestion. Astro also shows "not_found_handling": "404-page" for a custom 404 [2]; Lightfall has no 404 page today (checked: no src/pages/404.astro). This file is not in the repo; adding it is a suggestion.
- Connect: Workers & Pages, select the Worker, Settings, Builds, Connect, follow the prompts, push a commit [10].
- Settings: Git branch defaults to
main; build command optional (Astro's guide:npx astro build; Lightfall'snpm run buildisastro build); deploy command defaults tonpx wrangler deploy[11][2]. - Previews: non-production branches run
npx wrangler versions uploadby default, which makes a preview version without promoting it [11]. "Every time you create a new version of your Worker, a unique static version preview URL is generated automatically" [13]. - Limits: Free 3,000 build minutes/month, 1 build at a time; Paid 6,000 minutes then $0.005/minute, 6 at a time; builds time out at 20 minutes on both [12].
Honest comparison: Cloudflare rates Workers' branch deploy controls as partial next to Pages, saying Workers Builds "does not yet have the same level of configurability as Pages does" [8]. The configuration page says nothing about PR comments or GitHub checks [11]. If an automatic comment with the preview link on every pull request matters, Firebase documents exactly that [69]; for Lightfall I would still take Workers (judgement).
Analytics: Cloudflare Web Analytics is "Available on all plans" [16], but the one-click setup documented is for a Pages project (Metrics, Enable) [15]; setup for Workers static assets is not documented on the page fetched. No limit on sites proxied through Cloudflare [17].
Security headers, and keeping /admin out
Headers are instructions sent along with each file. On Workers static assets you set them in a plain-text _headers file in public/: up to 100 rules, 2,000 characters per line; they do not apply to responses generated by Worker code [6]. A _redirects file works the same way (2,000 static plus 100 dynamic redirects) [7]. Lightfall has neither file today (checked with ls).
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy: document-domain=()
Content-Security-Policy-Report-Only: default-src 'self'; worker-src 'self'; img-src 'self' data: blob:; connect-src 'self' https://tiles.your-domain.com; frame-ancestors 'none'- The first four lines are the examples in Cloudflare's headers doc [6].
nosniffstops browsers guessing file types;DENYstops other sites framing yours. - CSP belongs in a header, not a meta tag:
frame-ancestorsand Report-Only cannot be set with<meta>.connect-srcgovernsfetch(), which is how tile range reads travel [75]. - MapLibre's needs:
worker-src 'self'andimg-src data: blob: 'self', and with a strict CSP usesetWorkerUrl[76]. Lightfall already calls it (src/lib/map/globe.ts:74, checked). - Start Report-Only. The draft only reports violations, it blocks nothing. What Astro's build inlines (scripts, styles) was not researched, so an enforcing policy could break the page. When the shop is built,
connect-srcalso needshttps://{store}.myshopify.com, the Storefront API host [83].
The draft above is a starting point assembled from the cited docs, not a tested policy. Adding public/_headers is a repo change and is not made.
/admin never ships
- Already guarded.
src/integrations/admin.mjslines 132 to 139: after every build,findAdminInDistscansdist/and throws "the built site carries admin, which must never ship". Read from the file, not run. - Why that protects the live site: Workers Builds runs the build before deploying [10], so a build that throws never reaches the deploy step (my reading of the ordering: build command, then deploy command [11]).
- The menu only lists Admin under
astro dev, so the built menu never links it (src/lib/site/menu.ts).
Environment variables
An environment variable is a setting kept outside the code, so the same code can point at localhost on your laptop and at R2 in production.
| Variable | Local value (on disk) | Production value | Where to set it | Source |
|---|---|---|---|---|
PUBLIC_TILE_URL | http://localhost:4322/tiles in .env; .env.example and the comment in tile-url.ts say port 4321 | https://tiles.your-domain.com, no trailing slash (tile-url.ts) | Workers Builds Build variable (Settings, Build). Build variables "will not be accessible at runtime", which is fine: Astro replaces it at build time | [11][18] retrieved 2026-09-15 |
| Shopify Storefront public token (when the shop is built) | — | Public token: "visible to buyers" by design, sent as X-Shopify-Storefront-Access-Token | May be a PUBLIC_ build variable | [83][18] retrieved 2026-09-15 |
| Shopify private token (only if a server is ever added) | — | Never in the browser; header Shopify-Storefront-Private-Token | Never a PUBLIC_ variable; a static site has nowhere private to keep it | [83][18] retrieved 2026-09-15 |
- Astro: variables "are statically replaced at build time", and only ones starting with
PUBLIC_reach code in the browser [18]. Changing the tile address means a rebuild, not a settings change on the live site. .envis git-ignored (.gitignoreline 7) and.env.exampleis committed, both checked. Nothing secret is in the repo for the host to read.- Port mismatch, found on disk:
.envuses 4322, while.env.exampleand tile-url.ts's comment say 4321. Harmless for hosting; worth tidying so a fresh clone matches.
Does the print shop change hosting? And email
- No server needed for the shop. Shopify headless works with "any framework, tooling, or hosting" [84]. The public Storefront token is designed for the browser; the cart's
checkoutUrlsends the buyer to Shopify's hosted checkout, and should be requested when the buyer is ready because it can go stale [83]. The Headless app is free [85]. Which Shopify plan is required was not verified. - Card data: because the card is typed on Shopify's page, not Lightfall's, the Cloudflare Free Services clause on collecting card information probably does not apply. That is my reading, not Cloudflare's. An embedded card form on Lightfall itself would change that [19].
- Email forwarding (hello@your-domain): Cloudflare Email Routing, "Available on Free and Paid plans", requires Cloudflare DNS, adds MX, SPF and DKIM records, and needs the destination inbox verified [77][78]. It only forwards: "Email Routing does not support sending or replying from your Cloudflare domain" [79]. Limits: 200 rules, 25 MiB messages [80]. Strict DMARC on the sender can make forwarded mail fail [79].
- Email you can send from: Fastmail Individual $5/month billed annually ($6 monthly), 60 GB, custom domain; its Business Basic tier does not support custom domains [82]. Google Workspace Business Starter regular price $7.00/user/month [81] (the page's promo dates contradict its own fine print, so ignore the promo).
Launch checklist
Suggestions, in order. Items marked (repo) change files the site's engineers own; none were made. Account creation and spending are Jake's call.
- Decide the card-data question: confirm with Cloudflare that a Free-plan site sending buyers to Shopify's checkout is fine under §2.2.1(h) [19]. Whether paying for Workers alone changes it is also unverified, since DNS, SSL and CDN would still be Free Services.
- Domain bought, zone active on Cloudflare, DNSSEC handled [32][23].
- R2: bucket, four files uploaded without Content-Encoding,
tiles.custom domain Active, r2.dev disabled, CORS saved, Cache Rule for .pmtiles [39][40][44][43]. - Test a range request against R2 before wiring the site:
curl -I -H "Range: bytes=0-99" https://tiles.your-domain.com/world-z5.pmtilesshould return only the requested bytes (status 206 Partial Content is the HTTP standard's answer; not taken from a fetched page). Look forCf-Cache-Status: HITon a repeat [42]. (A test to run; not run.) - Measure range requests per map visit in the browser's Network tab, then redo the cost table with the real number.
- (repo) wrangler.jsonc with
assets.directory./dist[2]. - (repo) astro.config.mjs: change
sitefromhttps://lightfall.example(checked, line 27) to the real domain. - Workers Builds connected to the private repo;
PUBLIC_TILE_URLset as a Build variable [10][11]. Check the first build log for the Node version (see risks). - Custom Domain on the Worker for the apex; proxied
wwwrecord plus the 301 redirect rule [21][29][30]. - HTTPS: certificate shows Active, Always Use HTTPS on, HSTS at a short max-age with preload off [24][26][27].
- (repo) public/_headers with the draft above, CSP in Report-Only; watch the console, then enforce [6][75].
- Confirm /admin is absent on the live site (expect a 404) and that the build log shows no admin guard error.
- Preview branch test: push a branch, open its preview URL, confirm the map loads (the CORS origins must include the preview host) [13][40].
- Email: Email Routing for forwarding, or Fastmail/Workspace to send [77][82].
Risks, gaps and things not verified
- Card-data clause. Cloudflare's self-serve agreement: you may not "process or collect personal or business credit card information on any web property that is receiving Free Services" [19]. Checkout on Shopify's page probably keeps Lightfall clear, but Cloudflare has not confirmed that. It is the most important open question.
- Large-files clause. Outside Enterprise, Cloudflare says video and "other large files" must go through paid services such as the Developer Platform, and it may limit CDN use serving "a disproportionate percentage" of large files without them [20]. Whether R2 tiles count as the Developer Platform route is a reading, not confirmed.
- Range on Workers static assets is undocumented. Only a cache-control condition mentions Range in the docs and the asset-worker source;
worker.tsand the platform edge were not read [6][9]. This is why every tile file goes to R2. - Requests per map visit are unmeasured, so the R2 read bill is illustration. Whether Cloudflare cache hits on an R2 custom domain avoid Class B charges is not stated anywhere fetched. That "one range request = one Class B read" comes from Protomaps, not Cloudflare.
- The .pmtiles Cache Rule settings (match by extension, edge TTL) were not researched; only the Free limit of 10 rules was.
- Preview deploys and CORS. A preview URL is a different host from your domain; unless the bucket's CORS list includes that host, the map will not load on previews. The exact preview hostname was not captured, and this is my reading of how CORS applies, untested.
- Node version on Workers Builds was not researched. Lightfall's scripts rely on Node stripping TypeScript types (lint-type.mjs says Node 25; this machine runs v25.9.0) and the repo pins no version (no .nvmrc, no engines field). If
astro buildtouches those paths on an older Node, the first build could fail. - Timings not stated: zone activation and Universal SSL issuance times are not given; the nameserver change says up to 24 hours.
- Whether zone HSTS and Always Use HTTPS apply to Workers Custom Domain responses is not stated in either doc. Check with a browser after launch.
- R2 latency of "500ms or higher" is Protomaps' claim, not measured by Cloudflare or by us. The globe's first paint could feel it.
- Registrar lock-in: a Cloudflare Registrar domain cannot use another DNS provider while it stays there. Moving means transferring the domain.
- Official Cloudflare Registrar prices were not fetched from Cloudflare (where its per-extension price list lives was not established); .com $10.46 comes from a third-party tracker. Namecheap was not verified (403). .nyc eligibility rules were not verified.
- "Free plan costs $0" is not literally stated on any page fetched, and the original claim was dropped by the fact-checker for that reason.
- Web Analytics for Workers static assets: automatic setup is documented for Pages only; no price stated beyond "Available on all plans".
- Shopify: the required Shopify plan and price, the Headless app's plan list and how Prodigi connects were not verified.
- Other hosts' gaps: Netlify's terms not fetched; Vercel, Firebase, Render, DigitalOcean and Amplify have no Range or per-file documentation in what was fetched; Render plan prices not retrieved.
- Google Workspace's promo window (28 Sep to 28 Dec 2026) starts after today and conflicts with its own "12 months" fine print; only the $7.00 regular price is quoted.
- Extraction, not raw pages: the fetch tool returns a model's reading of each page. Quotes matched on re-fetch, but word-for-word accuracy is not guaranteed.
- Search budget: web search ran out (200 of 200); later checks used direct fetches and a GitHub API code search.
How this was researched
- Verified finding records
- 69
- Claims rejected
- 18
- Sources cited
- 85
- Research areas
- 2
Two research areas (hosting platforms; domain, DNS, SSL, tiles, email, security and shop) each had a researcher and then an adversarial fact-checker who re-fetched every page, corrected wrong figures and sources, and dropped anything not on the page. 18 claims were rejected and none appear here. Some of the 69 records are the same page checked by both areas.
Official docs and pricing pages were preferred. Third-party pages are labelled: cfdomainpricing.com (prices) and Protomaps (a map-tile project whose docs make claims about providers). Facts about the Lightfall repo (tile sizes, file counts, admin guard, headers files, env files, ports) were read on disk on the same day, not fetched. Retrieval dates are recorded as 2026-09-15 as instructed; one researcher's system clock read 2026-09-14.
Sources
Cloudflare: hosting the site
- Cloudflare Pages docs (banner recommending Workers) · retrieved 2026-09-15
- Astro docs — Deploy your Astro site to Cloudflare · retrieved 2026-09-15
- Cloudflare Workers — Limits · retrieved 2026-09-15
- Cloudflare Workers — Pricing · retrieved 2026-09-15
- Cloudflare Workers static assets — Billing and limitations · retrieved 2026-09-15
- Cloudflare Workers static assets — Headers · retrieved 2026-09-15
- Cloudflare Workers static assets — Redirects · retrieved 2026-09-15
- Cloudflare — Migrate from Pages to Workers · retrieved 2026-09-15
- cloudflare/workers-sdk source — asset-worker utils/headers.ts (read via GitHub API) · retrieved 2026-09-15
- Cloudflare Workers Builds · retrieved 2026-09-15
- Cloudflare Workers Builds — Configuration · retrieved 2026-09-15
- Cloudflare Workers Builds — Limits and pricing · retrieved 2026-09-15
- Cloudflare Workers — Preview URLs · retrieved 2026-09-15
- Cloudflare Pages — Limits · retrieved 2026-09-15
- Cloudflare Web Analytics — Get started · retrieved 2026-09-15
- Cloudflare Web Analytics — Overview · retrieved 2026-09-15
- Cloudflare Web Analytics — Limits · retrieved 2026-09-15
- Astro docs — Using environment variables · retrieved 2026-09-15
- Cloudflare Self-Serve Subscription Agreement (updated 12 Sept 2025) · retrieved 2026-09-15
- Cloudflare Service-Specific Terms — Application Services (CDN) · retrieved 2026-09-15
Domain, DNS and SSL
- Cloudflare Workers — Custom Domains · retrieved 2026-09-15
- Cloudflare — Onboard a domain · retrieved 2026-09-15
- Cloudflare DNS — Full setup · retrieved 2026-09-15
- Cloudflare SSL — Universal SSL · retrieved 2026-09-15
- Cloudflare SSL — Certificate validity periods · retrieved 2026-09-15
- Cloudflare SSL — Always Use HTTPS · retrieved 2026-09-15
- Cloudflare SSL — HTTP Strict Transport Security (HSTS) · retrieved 2026-09-15
- Let's Encrypt — From 90 to 45 days · retrieved 2026-09-15
- Cloudflare Rules — Redirect www to root · retrieved 2026-09-15
- Cloudflare Rules — Create a Single Redirect in the dashboard · retrieved 2026-09-15
- Cloudflare Registrar docs — Overview · retrieved 2026-09-15
- Cloudflare Registrar docs — Register a domain · retrieved 2026-09-15
- Cloudflare — Registrar product page · retrieved 2026-09-15
- cfdomainpricing.com by NameBeta (third party, not Cloudflare; updated 2026-09-14) · retrieved 2026-09-15
- Porkbun — Domains · retrieved 2026-09-15
- Google Domains (moved to Squarespace) · retrieved 2026-09-15
Map tiles and storage
- Cloudflare R2 — Pricing · retrieved 2026-09-15
- Cloudflare R2 — S3 API compatibility · retrieved 2026-09-15
- Cloudflare R2 — Public buckets · retrieved 2026-09-15
- Cloudflare R2 — Configure CORS · retrieved 2026-09-15
- Protomaps — PMTiles on cloud storage · retrieved 2026-09-15
- Protomaps — Deploy on Cloudflare (third-party claims labelled) · retrieved 2026-09-15
- Cloudflare Cache — Default cache behavior · retrieved 2026-09-15
- Cloudflare Cache — Range requests · retrieved 2026-09-15
- Cloudflare Cache — Cache Rules · retrieved 2026-09-15
- Backblaze B2 — Pricing · retrieved 2026-09-15
- GitHub Docs — About large files on GitHub · retrieved 2026-09-15
Other hosts compared
- Vercel — Fair use guidelines (updated 2026-07-29) · retrieved 2026-09-15
- Vercel — Pro plan · retrieved 2026-09-15
- Vercel — Pricing · retrieved 2026-09-15
- Vercel — Limits · retrieved 2026-09-15
- Vercel — Working with SSL · retrieved 2026-09-15
- Netlify — Pricing · retrieved 2026-09-15
- Netlify — Billing FAQ for credit-based plans · retrieved 2026-09-15
- Netlify — HTTPS (SSL) · retrieved 2026-09-15
- Netlify — Custom headers · retrieved 2026-09-15
- GitHub Docs — GitHub Pages limits · retrieved 2026-09-15
- GitHub Docs — Securing your GitHub Pages site with HTTPS · retrieved 2026-09-15
- AWS Amplify — Pricing · retrieved 2026-09-15
- AWS Amplify — Quotas · retrieved 2026-09-15
- AWS Amplify — Using SSL/TLS certificates · retrieved 2026-09-15
- AWS Amplify — Web previews for pull requests · retrieved 2026-09-15
- Amazon S3 — Website endpoints · retrieved 2026-09-15
- Amazon CloudFront — Range GETs · retrieved 2026-09-15
- Amazon CloudFront — Pricing (flat-rate plans) · retrieved 2026-09-15
- Amazon CloudFront — Pay-as-you-go pricing · retrieved 2026-09-15
- Amazon S3 — Pricing · retrieved 2026-09-15
- Firebase Hosting — Usage, quotas and pricing · retrieved 2026-09-15
- Firebase Hosting — GitHub integration · retrieved 2026-09-15
- Firebase Hosting — Connect a custom domain · retrieved 2026-09-15
- Render — Outbound bandwidth · retrieved 2026-09-15
- Render — Static sites · retrieved 2026-09-15
- DigitalOcean — App Platform pricing · retrieved 2026-09-15
- DigitalOcean — App Platform limits · retrieved 2026-09-15
Security, email and the shop
- MDN — Content Security Policy (CSP) · retrieved 2026-09-15
- MapLibre GL JS docs (CSP directives) · retrieved 2026-09-15
- Cloudflare Email Routing — Overview · retrieved 2026-09-15
- Cloudflare Email Routing — Enable Email Routing · retrieved 2026-09-15
- Cloudflare Email Routing — Postmaster · retrieved 2026-09-15
- Cloudflare Email Routing — Limits · retrieved 2026-09-15
- Google Workspace — Pricing · retrieved 2026-09-15
- Fastmail — Pricing · retrieved 2026-09-15
- Shopify dev — Storefront API (includes cart and checkoutUrl) · retrieved 2026-09-15
- Shopify dev — Headless storefronts · retrieved 2026-09-15
- Shopify App Store — Headless · retrieved 2026-09-15