Share private GitHub repository documentation (Markdown and PDFs) behind a simple 6-digit access code. Built on Cloudflare's free tier with zero ongoing server costs.
Visitors see a landing page with a code input. After entering the correct code, they get a 24-hour session cookie and can browse the full rendered documentation site freely.
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β Visitor Browser ββββββΆβ Cloudflare Worker ββββββΆβ Cloudflare Pages β
β β β (travel.example.com) β β (*.pages.dev) β
β β β β β β
β 6-digit code βββΆβ β β’ Auth gate β β β’ Static HTML/PDF β
β Session cookie β β β’ Session cookies β β β’ Protected by β
β β β β’ Brute-force lockout β β Access + Service β
β β β β’ Proxy with service β β Token β
β β β token auth β β β
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
- Cloudflare Worker β intercepts all requests on the custom domain, enforces 6-digit code authentication, manages sessions, and proxies to Pages using a service token
- Cloudflare Pages β serves the rendered HTML documentation, PDFs, and images
- Cloudflare Access β blocks direct public access to
*.pages.dev; only the worker's service token can reach the origin - Terraform β provisions all Cloudflare infrastructure (Pages, Worker, KV, DNS, Access) with S3-backed remote state
- Cloudflare Provider v5 β uses the latest auto-generated Terraform provider
- Terraform >= 1.0
- Node.js >= 18 (for Wrangler CLI)
- Python 3 (for the Markdown build pipeline)
- A Cloudflare account (free tier works)
- A Cloudflare API token with Workers, Pages, DNS, and Zero Trust permissions
- An AWS S3 bucket for Terraform state storage
git clone <this-repo-url>
cd private-doc-viewerCreate terraform/terraform.tfvars:
cloudflare_account_id = "your-account-id"
cloudflare_api_token = "your-api-token"
access_code = "123456"
cookie_secret = "a-long-random-secret-string"
# Required for custom domain
cloudflare_zone_id = "your-zone-id"
custom_domain = "docs.example.com"
# Optional
webhook_url = "https://hooks.slack.com/services/..."Update the S3 backend in terraform/main.tf to point to your bucket.
cd terraform
terraform init
terraform applyThis creates:
- Cloudflare Pages project
- Worker script with KV bindings and secrets
- KV namespace for brute-force tracking
- Custom domain DNS record (CNAME)
- Access application + service token protecting the
pages.devorigin
Your Cloudflare API token needs these permissions:
| Scope | Permission | Access |
|---|---|---|
| Account | Workers Scripts | Edit |
| Account | Workers KV Storage | Edit |
| Account | Cloudflare Pages | Edit |
| Account | Access: Apps and Policies | Edit |
| Account | Access: Service Tokens | Edit |
| Zone | DNS | Edit |
There are two ways to deploy your source documentation to the site.
Use the included deploy.sh script to build and deploy from a local directory:
./deploy.sh ~/repos/my-private-docsThis will:
- Install Python build dependencies
- Convert all Markdown files to HTML (with link rewriting)
- Copy PDFs and images as-is
- Deploy the built
_site/to Cloudflare Pages via Wrangler
Your source repo should be a directory of Markdown files and assets:
my-private-docs/
βββ README.md β becomes index.html (home page)
βββ guide.md β becomes guide.html
βββ subfolder/
β βββ notes.md β becomes subfolder/notes.html
β βββ receipt.pdf β copied as subfolder/receipt.pdf
βββ images/
βββ photo.jpg β copied as images/photo.jpg
In your private documentation repository, add these GitHub Actions secrets:
| Secret | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
Cloudflare API token with Pages permission |
CLOUDFLARE_ACCOUNT_ID |
Your Cloudflare account ID |
Copy the workflow and build files into your source repo:
cp build/deploy-docs.yml <source-repo>/.github/workflows/deploy-docs.yml
cp -r build/ <source-repo>/build/Push to main and the workflow will build and deploy automatically.
The build pipeline (build/build.py):
- Converts
.mdfiles to styled HTML usingmarkdown-it-py - Rewrites relative
.mdlinks to.html(so inter-doc links work) - Copies PDFs, PNGs, JPGs, GIFs, and SVGs as-is
- Root
README.mdbecomesindex.htmlwith title "Home" - All other files keep their name with
.htmlextension
Update access_code in terraform/terraform.tfvars and re-apply:
cd terraform
terraform applyExisting sessions remain valid until their 24-hour cookie expires.
βββ build/
β βββ build.py # Markdown-to-HTML build pipeline
β βββ deploy-docs.yml # GitHub Actions workflow (copy to source repo .github/workflows/)
β βββ requirements.txt # Python build dependencies
βββ deploy.sh # Local deploy script
βββ terraform/
β βββ main.tf # S3 backend configuration
β βββ providers.tf # Cloudflare provider v5
β βββ resources.tf # Pages, Worker, KV, DNS resources
β βββ access.tf # Access application + service token
β βββ variables.tf # Input variables
β βββ outputs.tf # Output values (URLs, IDs)
βββ worker/
β βββ src/index.js # Auth Worker source code
βββ tests/ # Unit and property-based tests
- Access code auth β 6-digit code verified by the Worker
- Session cookies β HMAC-SHA256 signed, HttpOnly, Secure, SameSite=Strict, 24h expiry
- Brute-force protection β IP lockout after 8 failed attempts (30 min cooldown via KV)
- Cloudflare Access β blocks direct
pages.devaccess; only the worker's service token can reach the origin - Admin alerts β optional webhook notification on lockout events
MIT