QiD — DevOps Reference

Cloudflare Guidebook

Service tiers, competitive positioning, deployment quality gates, and automated cleanup for a rapid-prototyping workflow on Cloudflare Pages.

v1.0 · August 2026 · Prepared for QiD Engineering

01 — Service Tiers

Cloudflare Pages ships a generous free tier that covers most early-stage and even moderate-production workloads. The paid upgrade path is the Workers Paid plan at $5/month — far below competitor thresholds — which unlocks higher build concurrency, more deployments, and Workers/Functions compute.

Dimension Free Pro ($20/mo) Business ($200/mo)
Deployments / month 500 5,000 20,000
Concurrent builds 1 5 20
Build timeout 20 minutes (all tiers)
Files per site 20,000 100,000 100,000
Max file size 25 MiB (all tiers)
Bandwidth Unlimited — all tiers, no egress fees
Preview deployments Unlimited active previews
Custom domains Unlimited (contact CF if adding many)
Header rules 100 max per _headers file
Redirects 2,000 static + 100 dynamic
Workers Paid add-on $5/mo — 10M requests included
QiD Relevance Cloudflare Pages is particularly useful for rapid prototyping — spinning up API demos, proof-of-concept apps, and internal tools quickly. The 500 free monthly deployments are shared across all projects on the account, so with active prototyping across multiple projects, that budget can go fast. All deployed prototype files can be easily deleted periodically, and this cleanup can be automated with a script (see Section 05). No need to upgrade to Pro unless deployment volume consistently exceeds the cap.

How Cloudflare Pages generates shareable URLs

Every deployment on Cloudflare Pages gets two kinds of automatically generated URLs — no DNS configuration required, no custom domain purchase, no setup delay. This is one of the platform's most practical features for a team that needs to put working software in front of customers fast.

Commit-level URLs — Each individual deploy gets a unique, permanent URL based on a short hash of the commit:

https://<COMMIT_HASH>.<PROJECT_NAME>.pages.dev

# Example:
https://373f31e2.qid-auth-demo.pages.dev

This URL is immutable. It will always point to exactly that version of the code, even after subsequent deploys. This makes it ideal for sharing a specific state of a prototype with a customer — the link won't break or change underneath them if the team keeps pushing code.

Branch-level URLs — Each branch also gets a stable, human-readable URL that always points to the latest deploy on that branch:

https://<BRANCH_NAME>.<PROJECT_NAME>.pages.dev

# Example:
https://staging.qid-auth-demo.pages.dev
https://feature-oauth.qid-auth-demo.pages.dev

This is the URL you share when you want the customer to always see the latest version — it auto-updates with every push to that branch. Share it once in a Slack thread or email, and the customer always gets the freshest build without needing a new link.

Production URL — The production branch (typically main) gets the root project URL:

https://qid-auth-demo.pages.dev

Add a custom domain at any time and it points to the same production deployment.

Why this matters for QiD

For a verified identity API company doing active development, these auto-generated URLs are a free, zero-config demo infrastructure. Push a branch, copy the URL, drop it in a customer conversation. No staging server to maintain, no deploy pipeline to babysit, no DNS propagation to wait for. The customer sees a live, globally-distributed build within seconds of a push.

How competitors handle this differently

Vercel and Netlify both offer preview URLs, but with meaningful constraints that make them less useful as a free prototyping tool:

Cloudflare is the only platform where every commit and every branch gets a globally-distributed, SSL-enabled, unlimited-bandwidth URL on the free tier with no commercial-use restrictions. For a small team that needs to show working software to customers frequently, this is a significant operational advantage.

02 — Competitive Landscape

The three dominant platforms for frontend and full-stack deployment in 2026 are Cloudflare Pages, Vercel, and Netlify. Each has a meaningfully different pricing structure and developer experience. The right choice depends on your stack, team size, and cost sensitivity at scale.

Dimension Cloudflare Pages Vercel Netlify
Free bandwidth Unlimited 100 GB/mo 100 GB/mo
Paid plan entry $5/mo (Workers Paid) $20/user/mo $20/mo flat
Cost at 1 TB/mo traffic $0 – $5 $200 – $600 $150 – $400
Cold starts <5 ms (V8 isolates) 50 – 250 ms 100 – 500 ms
Edge network 330+ cities ~20 regions ~15 CDN PoPs
Object storage R2 — zero egress Blob Storage Blobs (beta)
Next.js DX Improving, not native Best-in-class Good
Deploy DX / polish Rougher; CLI-oriented Best dashboard UX Clean, close second
Team seat pricing No per-seat cost Per-seat Flat (unlimited members)

Bottom Line

For a company like QiD — building a verified identity API service and iterating rapidly through demos, proofs-of-concept, and internal tools — Cloudflare is the clear cost winner. Unlimited bandwidth on the free tier means prototype traffic costs nothing, and the rougher developer experience is a non-issue for a technical team comfortable with CLI-driven workflows. If a prototype graduates to production, the $5/month Workers Paid plan scales further than competitors at 10x the price.

03 — Why Cloudflare for QiD

Zero-cost rapid prototyping

QiD's development workflow involves rapid prototyping — building and iterating on API service components, developer-facing demos, and internal tools on short timelines. On Vercel or Netlify, each project's bandwidth and serverless invocations are metered — costs accumulate across projects and can spike unpredictably during testing or partner reviews. On Cloudflare, bandwidth is unlimited at every tier, including free. Spin up as many prototypes as needed, share URLs with stakeholders, and pay nothing until you need Workers compute.

This pairs directly with the auto-generated preview URLs described in Section 01. A customer asks for a change on a call — you push a fix, and within seconds you have a live URL you can text them. No staging server, no deploy queue, no "I'll send you the updated link tomorrow." The customer sees the change while the conversation is still warm. That speed of response is how a small team wins product-market fit against larger incumbents: the customer feels heard, sees proof immediately, and builds trust in QiD's ability to ship. The branch-level URL (https://staging.qid-auth-demo.pages.dev) stays the same across pushes, so you share it once and every subsequent fix is automatically visible — no follow-up texts needed.

Disposable by design

Most prototypes have a short useful life. Cloudflare Pages makes this easy to manage: each prototype is its own Pages project with its own deployment history. When a prototype has served its purpose, the project and all its deployments can be deleted cleanly. Old deployments that linger beyond 180 days can be pruned automatically (see Section 05). This keeps the account tidy and the free-tier deployment budget available for active work.

Edge performance for demos and developer docs

Cloudflare's 330+ city edge network means prototypes and documentation load fast regardless of where the user is. Workers' V8 isolate model eliminates the cold-start latency that plagues Lambda-backed competitors — important when a partner or developer clicks a demo link for the first time and first impressions matter.

R2 for prototype assets

Prototypes that involve file uploads, image galleries, or document processing can use R2 for S3-compatible object storage with zero egress fees. Where AWS S3 would charge ~$90 to transfer 1 TB out, R2 charges $0. This removes cost anxiety from asset-heavy demos.

Graduation path

When a prototype graduates to production, the $5/month Workers Paid plan provides 10 million requests, 5 concurrent builds, and 5,000 monthly deployments. No migration needed; the same Pages project just scales up.

Workflow Pattern Each prototype deploys as its own Cloudflare Pages project. External-facing prototypes get a custom subdomain (e.g., demo.qid.dev). Internal experiments stay on the default *.pages.dev URL. When a prototype concludes, delete the project entirely — or let the automated cleanup script handle stale deployments.

04 — Quality & Safety Gates

Cloudflare Pages doesn't impose an opinionated CI/CD pipeline, so quality gates must be built deliberately. Here's a layered approach for any QiD project deployed to Cloudflare.

A. Branch build controls

By default, every commit to every branch triggers a build. Lock this down immediately:

B. Protect preview deployments with Cloudflare Access

Preview deployment URLs are public by default. For pre-launch prototypes or anything touching sensitive identity data, this should be locked down.

Common Gotcha The Access policy created by the Pages UI only protects preview deployments on the *.pages.dev subdomain. Your custom domain (e.g., demo.qid.dev) is not covered by this policy. If you need to gate a custom domain too (e.g., a pre-launch prototype), create a separate Access application for that domain.

C. CI pipeline gates (GitHub Actions)

Cloudflare Pages' built-in builds are minimal — they just run your build command. Layer a proper CI pipeline in front:

# .github/workflows/ci.yml — runs before Cloudflare Pages build
name: QiD CI
on:
  pull_request:
    branches: [main, staging/*]

jobs:
  quality-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run lint          # ESLint
      - run: npm run type-check    # TypeScript strict
      - run: npm run test          # Vitest / Jest
      - run: npm run build         # Catch build failures before CF

Configure branch protection rules on GitHub to require this workflow to pass before merging to main.

D. Production deploy approval

For any project that graduates to production, consider requiring manual approval before production deploys:

E. SEO & crawler protection

Cloudflare Pages automatically adds X-Robots-Tag: noindex to all preview deployments, preventing search engines from indexing duplicate content. No action needed — this is on by default. Verify it hasn't been overridden in your _headers file.

F. Secret management

Never commit API keys to your repo. Use Cloudflare Pages' Environment Variables panel (under Settings → Environment variables) and mark sensitive values as Encrypted. Set different values for Production and Preview environments. This applies to any third-party service credentials (database URLs, API tokens, etc.).

05 — Automated Deployment Cleanup

Preview deployments accumulate indefinitely on Cloudflare Pages. Over months of rapid prototyping, stale deployments pile up — consuming visual clutter in the dashboard and making it harder to find what's current. The script below deletes any deployment older than 180 days that hasn't been revisited, across all specified projects.

Caution The Cloudflare API does not allow deleting the latest deployment on any branch. This is a safety rail — your current production and active branch tips are always preserved. However, run this in a dry-run mode first (see the DRY_RUN flag below) before executing deletions.
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────
#  cleanup-old-deployments.sh
#  Deletes Cloudflare Pages deployments older than 180 days
#  that have not been "revisited" (modified_on == created_on).
#
#  Requirements: curl, jq, date (GNU coreutils)
#  Usage: ./cleanup-old-deployments.sh
# ──────────────────────────────────────────────────────────────

# ─── CONFIGURATION ────────────────────────────────────────────
ACCOUNT_ID="YOUR_CLOUDFLARE_ACCOUNT_ID"
API_TOKEN="YOUR_CLOUDFLARE_API_TOKEN"

# QiD projects — add/remove as prototypes are created
PROJECTS=(
  "demo-acme-dashboard"
  "prototype-intake-form"
  "internal-tooling-v2"
  # Add new project names here as they're created
)

MAX_AGE_DAYS=180
DRY_RUN=true      # Set to false to actually delete
PER_PAGE=50       # API pagination size (max 100)
RATE_LIMIT=0.3    # Seconds between API calls

# ─── COMPUTED ─────────────────────────────────────────────────
CUTOFF_EPOCH=$(date -d "-${MAX_AGE_DAYS} days" +%s 2>/dev/null \
  || date -v-${MAX_AGE_DAYS}d +%s)  # GNU || macOS fallback
CUTOFF_ISO=$(date -d "@${CUTOFF_EPOCH}" -Iseconds 2>/dev/null \
  || date -r "${CUTOFF_EPOCH}" -Iseconds)

BASE_URL="https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects"
TOTAL_DELETED=0
TOTAL_SKIPPED=0

echo "═══════════════════════════════════════════════════════"
echo "  Cloudflare Pages Deployment Cleanup"
echo "  Cutoff:    ${CUTOFF_ISO} (${MAX_AGE_DAYS} days ago)"
echo "  Dry run:   ${DRY_RUN}"
echo "  Projects:  ${#PROJECTS[@]}"
echo "═══════════════════════════════════════════════════════"
echo ""

for PROJECT in "${PROJECTS[@]}"; do
  echo "──── Project: ${PROJECT} ────"
  PAGE=1
  PROJECT_DELETED=0
  PROJECT_SKIPPED=0

  while true; do
    # Fetch a page of deployments
    RESPONSE=$(curl -s \
      "${BASE_URL}/${PROJECT}/deployments?per_page=${PER_PAGE}&page=${PAGE}" \
      -H "Authorization: Bearer ${API_TOKEN}" \
      -H "Content-Type: application/json")

    # Check for API errors
    SUCCESS=$(echo "${RESPONSE}" | jq -r '.success')
    if [ "${SUCCESS}" != "true" ]; then
      echo "  ✗ API error for ${PROJECT}: $(echo ${RESPONSE} | jq -r '.errors[0].message // "Unknown"')"
      break
    fi

    # Extract deployments as JSON lines
    DEPLOYMENTS=$(echo "${RESPONSE}" | jq -c '.result[]')

    # Exit pagination if no results
    if [ -z "${DEPLOYMENTS}" ]; then
      break
    fi

    while IFS= read -r deploy; do
      DEPLOY_ID=$(echo "${deploy}" | jq -r '.id')
      CREATED=$(echo "${deploy}" | jq -r '.created_on')
      MODIFIED=$(echo "${deploy}" | jq -r '.modified_on')
      ENV=$(echo "${deploy}" | jq -r '.environment')
      ALIAS=$(echo "${deploy}" | jq -r '.aliases[0] // "none"')

      # Parse created_on to epoch
      CREATED_EPOCH=$(date -d "${CREATED}" +%s 2>/dev/null \
        || date -jf "%Y-%m-%dT%H:%M:%S" "${CREATED%%.*}" +%s)

      # Parse modified_on to epoch
      MODIFIED_EPOCH=$(date -d "${MODIFIED}" +%s 2>/dev/null \
        || date -jf "%Y-%m-%dT%H:%M:%S" "${MODIFIED%%.*}" +%s)

      # Skip if modified recently (someone revisited it)
      if [ "${MODIFIED_EPOCH}" -gt "${CUTOFF_EPOCH}" ]; then
        PROJECT_SKIPPED=$((PROJECT_SKIPPED + 1))
        continue
      fi

      # Skip if created after cutoff
      if [ "${CREATED_EPOCH}" -gt "${CUTOFF_EPOCH}" ]; then
        PROJECT_SKIPPED=$((PROJECT_SKIPPED + 1))
        continue
      fi

      # This deployment is old and not revisited — delete it
      if [ "${DRY_RUN}" = true ]; then
        echo "  [DRY RUN] Would delete: ${DEPLOY_ID} (${ENV}) created ${CREATED}"
      else
        echo "  Deleting: ${DEPLOY_ID} (${ENV}) created ${CREATED}"
        curl -s -X DELETE \
          "${BASE_URL}/${PROJECT}/deployments/${DEPLOY_ID}" \
          -H "Authorization: Bearer ${API_TOKEN}" > /dev/null
        sleep "${RATE_LIMIT}"
      fi

      PROJECT_DELETED=$((PROJECT_DELETED + 1))

    done <<< "${DEPLOYMENTS}"

    PAGE=$((PAGE + 1))
  done

  echo "  → Deleted: ${PROJECT_DELETED}  Skipped: ${PROJECT_SKIPPED}"
  echo ""
  TOTAL_DELETED=$((TOTAL_DELETED + PROJECT_DELETED))
  TOTAL_SKIPPED=$((TOTAL_SKIPPED + PROJECT_SKIPPED))
done

echo "═══════════════════════════════════════════════════════"
echo "  Complete. Deleted: ${TOTAL_DELETED}  Skipped: ${TOTAL_SKIPPED}"
echo "═══════════════════════════════════════════════════════"

How it works

Running it

# 1. Make it executable
chmod +x cleanup-old-deployments.sh

# 2. Dry run first — review the output
./cleanup-old-deployments.sh

# 3. When satisfied, edit DRY_RUN=false and run again
./cleanup-old-deployments.sh

# 4. Schedule monthly via cron (first Monday at 2am)
0 2 1-7 * 1 /path/to/cleanup-old-deployments.sh >> /var/log/cf-cleanup.log 2>&1

Getting your credentials

CI Integration Rather than a cron job, you can run this script as a scheduled GitHub Action. Use a repository secret for the API token, and trigger on schedule: cron: '0 2 1 * *' for monthly cleanup on the first of each month.

06 — GitHub + Cloudflare Integration

The GitHub integration is what turns Cloudflare Pages from a hosting platform into a deployment pipeline. Once connected, every git push becomes a live, globally-distributed build — no deploy commands, no CLI steps, no manual intervention. Here's how it works and how to configure it optimally for QiD.

What the integration gives you

When you connect a GitHub repo to a Cloudflare Pages project, four things happen automatically with zero additional configuration:

Initial setup

# Step-by-step:
# 1. Cloudflare Dashboard → Workers & Pages → Create project
# 2. Select "Connect to Git" → Authorize the Cloudflare GitHub App
# 3. Grant access to the QiD org (or specific repos)
# 4. Select the repo → configure build settings:
#
#    Production branch:   main
#    Build command:        npm run build
#    Build output dir:     dist/    (or build/, out/, etc.)
#
# 5. Deploy — Cloudflare runs the first build immediately.
#
# That's it. Every subsequent push triggers a build automatically.
One-way door If you set up a project with the Git integration, you cannot later switch it to Direct Upload (and vice versa). Choose Git integration for any project that lives in a repo. Use Direct Upload only for one-off static drops that don't have a repo behind them.

Optimal branch configuration for QiD

The default behavior — build every commit on every branch — burns through the free-tier deploy budget fast during active development. Lock it down to branches that matter:

# In Cloudflare Dashboard → your project → Settings → Builds → Branch control

Production branch:    main
Preview branches:     Custom
Include:              staging/*, feat/*, fix/*
Exclude:              (leave empty)

# This means:
#   - Pushes to main → production deploy
#   - Pushes to staging/*, feat/*, fix/* → preview deploy
#   - Pushes to any other branch (docs/*, chore/*, wip/*) → no build
#
# Wildcard (*) matches zero or more characters.
# A branch named "feat/oauth" matches "feat/*".

Recommended GitHub repo settings

Pair the Cloudflare integration with GitHub's own guardrails to create a clean deployment pipeline:

Layering GitHub Actions on top

The native Git integration handles builds and deploys. GitHub Actions handles everything around it — linting, testing, notifications, and cleanup. Here's a workflow that runs CI checks, waits for the Cloudflare build to complete, and posts the preview URL as a PR comment:

# .github/workflows/deploy.yml
name: QiD Deploy Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm run test

  deploy:
    needs: ci
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    permissions:
      contents: read
      deployments: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Cloudflare Pages
        id: deploy
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: >-
            pages deploy dist
            --project-name=YOUR_PROJECT
            --branch=${{ github.head_ref }}

      - name: Comment preview URL on PR
        uses: thollander/actions-comment-pull-request@v3
        with:
          message: |
            ## ⚡ Deployed to Cloudflare Pages
            | **Commit:** | `${{ github.event.pull_request.head.sha }}` |
            |:--|:--|
            | **Preview:** | ${{ steps.deploy.outputs.deployment-url }} |
            | **Branch:** | ${{ steps.deploy.outputs.pages-deployment-alias-url }} |
          comment-tag: cf-deploy
Why both? The native Git integration is simpler — connect and forget. But it doesn't run your test suite, doesn't post rich PR comments, and doesn't give you control over the deploy step. For QiD, the recommended setup is: use the native integration for quick-and-dirty prototype repos (connect, push, done), and layer GitHub Actions on top for any project that's heading toward production and needs CI gates.

Monorepo considerations

Cloudflare Pages allows only one project per GitHub repo via the native integration. If QiD ever consolidates multiple prototypes into a monorepo, you have two options:

The complete flow

Here's what the optimal QiD setup looks like end-to-end:

# Developer pushes a branch
git push origin feat/verify-endpoint

  
  ├──→  GitHub Actions: lint, type-check, test
  │       └── must pass before merge is allowed
  
  ├──→  Cloudflare Pages: build + deploy preview
  │       └── preview URL posted as PR check run
  
  └──→  Developer texts branch URL to customer:
          https://feat-verify-endpoint.qid-auth.pages.dev

# Customer gives feedback → developer pushes fix
git push origin feat/verify-endpoint

  
  └──→  Same branch URL auto-updates. Customer refreshes.

# PR approved → merge to main
  
  └──→  Cloudflare Pages: production deploy
          https://qid-auth.pages.dev  (+ custom domain)
Key takeaway GitHub is where the code lives and gets reviewed. Cloudflare is where it gets deployed and served. The integration between them is the reason QiD can go from a code change to a customer-visible URL in under 60 seconds — and that speed is the operational advantage that makes rapid prototyping viable as a product development strategy.