Deployment
About 15 minutes. You need a Cloudflare account and the ability to add a DNS record. Nothing in this project needs a server — it is a static site.
Before you start, decide the hostname, for example oss.example.com. You cannot use a path on
someone else's site; it must be a hostname you control.
Step 0 — Build the clean output folder
cd oss-compliance-analyzer
npm run build:static
This creates dist/ containing only index.html, _headers, assets/, src/, samples/
and image/ (281 KB). It deliberately excludes tools/ — the CLI and MCP server are Node
processes with filesystem and stdio access, and publishing them to a corporate URL would be
unnecessary exposure. This is ADR-013.
Checkpoint: ls dist should contain no tools directory.
Step 1 — Get the files onto Cloudflare
Pick one route.
Route A — Wrangler from your machine (fastest, no Git needed)
npx wrangler login # opens a browser, authorises the CLI
npm run deploy:pages
The first run asks you to confirm the project name and prints the preview URL,
https://oss-compliance.pages.dev.
Route B — Git integration (re-deploys on every push)
-
Push the project to GitHub or GitLab.
-
Cloudflare dashboard → Workers & Pages → Create → Pages → Connect to Git.
-
Select the repository, then set:
Setting Value Framework preset None Build command npm run build:staticBuild output directory dist -
Save and deploy.
Cloudflare has been merging Pages into Workers. If your dashboard only offers Workers, use
npx wrangler deploy with static assets instead, or create the Pages project via Wrangler
(Route A) — both end in the same place.
Checkpoint: open the *.pages.dev URL and click Demo milestone diff. If the diff card
appears, the upload is good.
Step 2 — Add your custom domain
- Open the Pages project → Custom domains → Set up a custom domain.
- Enter your hostname → Continue.
Then it depends on where your DNS lives.
Case 1 — the domain already uses Cloudflare DNS
Cloudflare adds the DNS record itself. Nothing to do. Wait for the certificate: the status goes
Initializing → Active, usually under five minutes, occasionally up to an hour.
Case 2 — DNS is with another provider, or another team owns the apex
Pages needs the zone in your Cloudflare account. Two options:
-
Move the whole domain to Cloudflare DNS — Cloudflare walks you through the nameserver change. Cleanest, but it affects everything on the domain, so get the DNS owner's sign-off.
-
Delegate only the subdomain — add these records at your current provider:
oss.example.com. NS <name1>.ns.cloudflare.com.oss.example.com. NS <name2>.ns.cloudflare.com.then add
oss.example.comas a zone in Cloudflare. This touches nothing else — a good option in a company where apex DNS is locked down.
Using the bare apex (
example.comwith no subdomain) works only if the domain is on Cloudflare DNS, because Cloudflare flattens the CNAME at the root. With external DNS, use a subdomain.
Checkpoint: https://oss.example.com loads the app and the padlock is valid.
Step 3 — Lock it down (recommended)
No SBOM is ever uploaded — parsing happens in the browser — so a public URL leaks no data. But this is internal tooling; keep it off the open internet.
- Cloudflare dashboard → Zero Trust → Access → Applications → Add an application → Self-hosted.
- Application domain:
oss.example.com(all paths). - Add a policy: Allow → Emails ending in →
@yourcompany.com. Save.
Now do the same for oss-compliance.pages.dev, or the login bypasses your policy by using the
default URL. Alternatively add a Bulk Redirect from oss-compliance.pages.dev to your custom
domain.
Also worth setting on the zone: SSL/TLS → Overview → Full (strict).
Step 4 — Verify
curl -I https://oss.example.com
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' https://oss.example.com/src/app.js
Expect 200 and text/javascript. If src/app.js returns text/plain or text/html, ES
modules will be blocked and the page renders blank.
Then in the browser, load Demo milestone diff and confirm the diff card appears.
Security headers
_headers sets the following, and must sit in the deployed root (dist/_headers):
/*
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
X-Frame-Options: DENY
/index.html
Cache-Control: no-cache
/src/*
Cache-Control: no-cache
no-cache on HTML and JS is deliberate. There is no content hashing in the build, so aggressive
caching would leave users on stale code after a redeploy. Cloudflare supports up to 100 rules in
this file.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Blank page, console shows a module MIME error | src/*.js not served as text/javascript. Confirm the deploy included src/ and that no rule rewrites content types. |
| 525 / 526 on the custom domain | Certificate still issuing. Wait; check status on the Custom domains tab. |
Works on pages.dev, fails on the custom domain | DNS record missing or not proxied — should be a CNAME to <project>.pages.dev with the orange cloud on. |
| Old version after a redeploy | Browser cached src/. _headers sets no-cache for JS/HTML; hard-reload once. |
_headers seems ignored | It must sit in the deployed root. |
| App works but demo buttons fail | samples/ missing from the upload. |
Re-deploying and removing
- Route A:
npm run deploy:pages. Route B: push to the connected branch. - Removing: Pages project → Manage deployment → Delete project, then remove the custom domain and the DNS record it created.
What cannot be deployed
tools/cli.mjs and tools/mcp-server.mjs are Node processes — they need a filesystem and
stdio, which Workers do not provide. Run them locally or in your build agents.
If you later want a hosted API, wrap src/risk-engine.js in a Worker with a fetch handler —
the engine is pure and has no Node dependencies. Do not try to reuse the stdio MCP server on
Workers; there is no stdin and no filesystem.