Plan: Deploy Vault to brain.ehq.tech
Status: Draft Owner: @bilal Last Updated: 2026-02-15 Objective: Publish the EHQ Obsidian vault as an auth-gated static site at
brain.ehq.techusing Quartz, Cloudflare Pages, Cloudflare Access, and Terraform
Architecture
Obsidian vault (docs/)
↓ Quartz reads directly via --directory docs
Quartz repo (envo-docs/)
↓ GitHub Actions
Cloudflare Pages (static hosting, edge CDN, auto SSL)
↓ CNAME
brain.ehq.tech
↓ Cloudflare Access (Zero Trust)
Auth gate — team-only, email OTP, 7-day sessions
Why These Choices
| Decision | Rationale |
|---|---|
| Quartz | Purpose-built for Obsidian vaults, supports wikilinks, graph view, search |
| Cloudflare Pages | Free tier (500 builds/mo), edge CDN, auto SSL, git-triggered deploys |
| Cloudflare Access | Zero Trust auth gate, free for <50 users, email OTP — no public exposure |
| Terraform | IaC-managed infra, reproducible, Cloudflare provider is first-party |
brain.ehq.tech | Avoids collision with future docs.ehq.tech (API docs) or app.ehq.tech |
IaC Tooling: Terraform + Cloudflare Provider
Terraform is the pragmatic choice — the Cloudflare provider is official and well-documented. State lives locally initially, can move to Terraform Cloud or S3 backend later if needed.
Phase 1: Repository Setup
1.1 Create envo-docs repo
git clone https://github.com/jackyzha0/quartz.git envo-docs
cd envo-docs
npm i1.2 Content layout
Vault content lives in docs/. Quartz reads it directly via the --directory docs CLI flag — no symlinks, no copying.
envo-docs/
├── docs/ # vault content (Quartz reads via --directory docs)
│ ├── 00-Vision/
│ ├── 01-Architecture/
│ ├── 02-Workstreams/
│ ├── 03-Ops/
│ ├── 04-Team/
│ ├── 05-Specs/
│ └── index.md # landing page
├── infra/ # Terraform (Pages + DNS + Access)
├── quartz.config.ts
├── quartz.layout.ts
├── .github/workflows/
└── ...
1.3 Configure Quartz
Edit quartz.config.ts:
const config: QuartzConfig = {
configuration: {
pageTitle: "EHQ Brain",
baseUrl: "brain.ehq.tech",
ignorePatterns: ["Templates", "private", ".obsidian"],
// ...
},
// ...
}1.4 Content sync
Vault content lives in docs/ within the repo. Vault repo (ehq) triggers envo-docs rebuild via repository_dispatch. GitHub Action clones vault content into docs/ at build time. Vault stays private, envo-docs is the build/deploy repo.
Phase 2: IaC — Terraform
2.1 Directory structure
envo-docs/
├── infra/
│ ├── main.tf # Pages + DNS + Cloudflare Access
│ ├── variables.tf
│ ├── outputs.tf
│ ├── terraform.tfvars # (gitignored — contains API token)
│ └── .terraform.lock.hcl
├── docs/ # vault content (Quartz reads via --directory docs)
├── quartz.config.ts
├── .github/
│ └── workflows/
│ └── deploy.yml
└── ...
2.2 Terraform config
# infra/main.tf
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5.0"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# --- Data: look up existing zone ---
data "cloudflare_zone" "ehq" {
filter = {
name = "ehq.tech"
}
}
# =============================================
# Cloudflare Pages — static site hosting
# =============================================
resource "cloudflare_pages_project" "brain" {
account_id = var.cloudflare_account_id
name = "ehq-brain"
production_branch = "main"
build_config = {
build_command = "npx quartz build --directory docs"
destination_dir = "public"
}
source = {
type = "github"
config = {
owner = var.github_owner
repo_name = "envo-docs"
production_branch = "main"
}
}
deployment_configs = {
production = {
environment_variables = {}
}
}
}
# =============================================
# DNS — CNAME brain.ehq.tech -> Pages
# =============================================
resource "cloudflare_dns_record" "brain" {
zone_id = data.cloudflare_zone.ehq.zone_id
name = "brain"
type = "CNAME"
content = "${cloudflare_pages_project.brain.name}.pages.dev"
proxied = true # must be proxied for Access to work
}
resource "cloudflare_pages_domain" "brain" {
account_id = var.cloudflare_account_id
project_name = cloudflare_pages_project.brain.name
domain = "brain.ehq.tech"
}
# =============================================
# Cloudflare Access (Zero Trust) — auth gate
# =============================================
# Every request to brain.ehq.tech hits the Access login
# before reaching the Pages site. Free for up to 50 users.
resource "cloudflare_zero_trust_access_application" "brain" {
zone_id = data.cloudflare_zone.ehq.zone_id
name = "EHQ Brain"
domain = "brain.ehq.tech"
type = "self_hosted"
session_duration = "168h" # 7 days before re-auth
auto_redirect_to_identity = false # show login page
app_launcher_visible = true
}
# Policy: allow specific team emails
resource "cloudflare_zero_trust_access_policy" "brain_team" {
zone_id = data.cloudflare_zone.ehq.zone_id
application_id = cloudflare_zero_trust_access_application.brain.id
name = "EHQ team"
precedence = 1
decision = "allow"
include = [{
email_list = {
emails = var.team_emails
}
}]
}
# Policy: deny everyone else (explicit, defence in depth)
resource "cloudflare_zero_trust_access_policy" "brain_deny_all" {
zone_id = data.cloudflare_zone.ehq.zone_id
application_id = cloudflare_zero_trust_access_application.brain.id
name = "Deny all others"
precedence = 2
decision = "deny"
include = [{
everyone = {}
}]
}# infra/variables.tf
variable "cloudflare_api_token" {
type = string
sensitive = true
}
variable "cloudflare_account_id" {
type = string
}
variable "github_owner" {
type = string
default = "bilal-tariq" # adjust to your GitHub username/org
}
variable "team_emails" {
type = list(string)
description = "Email addresses allowed to access brain.ehq.tech"
default = [
"bilal@ehq.tech",
"deen@ehq.tech",
"danny@ehq.tech",
]
}# infra/outputs.tf
output "pages_url" {
value = "https://${cloudflare_pages_project.brain.name}.pages.dev"
}
output "custom_domain" {
value = "https://brain.ehq.tech"
}
output "access_app_id" {
value = cloudflare_zero_trust_access_application.brain.id
}2.3 Secrets
# Create a Cloudflare API token with these permissions:
# - Zone:DNS:Edit (for ehq.tech zone)
# - Account:Cloudflare Pages:Edit
# - Account:Account Settings:Read
# - Account:Access: Apps and Policies:Edit
# - Account:Access: Organizations, Identity Providers, and Groups:Edit
# infra/terraform.tfvars (gitignored!)
cloudflare_api_token = "your-token-here"
cloudflare_account_id = "your-account-id"2.4 Lock down the .pages.dev bypass
Cloudflare Access only protects the custom domain (brain.ehq.tech). The default ehq-brain.pages.dev URL bypasses Access and is publicly reachable. To close this hole:
Option A: Terraform (preferred, if supported by provider version)
# Not yet supported in all provider versions — check docs
# If available, add to cloudflare_pages_project:
# preview_deployment_setting = "none"Option B: _headers file in Quartz build output
Add a static/_headers file to the Quartz project:
https://:project.pages.dev/*
X-Robots-Tag: noindex
This doesn’t block access but prevents indexing. True lockdown requires the dashboard toggle or Wrangler:
# Via Wrangler CLI (can be run in CI after deploy)
npx wrangler pages project update ehq-brain --production-branch mainOption C: Cloudflare Dashboard (manual fallback)
Pages project → Settings → General → Disable
pages.devsubdomain
Do this once after the first deploy. Not IaC-managed yet, but closes the gap.
2.5 Apply
cd envo-docs/infra
terraform init
terraform plan # review: should show Pages project + DNS + Access app + policies
terraform apply # creates everythingPhase 3: CI/CD — GitHub Actions
3.1 Deploy workflow
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
repository_dispatch:
types: [vault-updated]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Build Quartz
run: npx quartz build --directory docs
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy public --project-name=ehq-brain3.2 Vault sync trigger
Add to the vault repo (ehq) to auto-rebuild when vault content is pushed:
# .github/workflows/trigger-site.yml
name: Trigger site rebuild
on:
push:
branches: [main]
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.SITE_REPO_PAT }}
repository: your-org/envo-docs
event-type: vault-updated3.3 GitHub Actions secrets to configure
| Secret | Where | Value |
|---|---|---|
CLOUDFLARE_API_TOKEN | envo-docs repo | Same token as Terraform |
CLOUDFLARE_ACCOUNT_ID | envo-docs repo | Your CF account ID |
SITE_REPO_PAT | ehq vault repo | GitHub PAT with repo scope for envo-docs |
Checklist
Prerequisites
- Cloudflare account with ehq.tech zone active
- Cloudflare API token created (DNS + Pages + Access permissions)
- Terraform installed (
brew install terraform) - GitHub repo
envo-docscreated - ehq.tech email addresses set up (for Access OTP) — or use personal emails initially
Phase 1: Repo
- Clone Quartz into
envo-docs - Set up
docs/directory with vault content - Configure
quartz.config.ts(baseUrl, ignorePatterns) - Test locally:
npx quartz build --serve --directory docs
Phase 2: Terraform
- Write
infra/main.tf,variables.tf,outputs.tf - Create
terraform.tfvars(gitignored) -
terraform init && terraform plan -
terraform apply— creates Pages + DNS + Access - Disable
.pages.devsubdomain (dashboard or CLI)
Phase 3: CI/CD
- Add GitHub Actions secrets to
envo-docs - Push deploy workflow, verify auto-deploy
- Add vault sync trigger to
ehqrepo - Verify end-to-end: vault push → site rebuild → deploy
Verification
-
brain.ehq.techshows Cloudflare Access login gate - Team emails receive OTP and can authenticate
- After auth, Quartz site renders with vault content
-
ehq-brain.pages.devis disabled or inaccessible - SSL working on custom domain
Post-deploy
- Update
03-Ops/Domain & Email Setup.mdwithbrain.ehq.tech - Update
03-Ops/Infrastructure.mdwith Cloudflare Pages + Access entry - Update
04-Team/Ways of Working.md— publishing section is now live
Decisions Still Needed
| Decision | Options | Default |
|---|---|---|
| Terraform state backend | Local / Terraform Cloud / S3 | Local initially |
| Excluded folders | Which vault folders to hide from site | Templates, .obsidian |
| GitHub owner/org | Personal account or org for envo-docs | TBD |
| Access auth method | Email OTP / Google / GitHub | Email OTP (simplest) |
.pages.dev lockdown | Terraform / Wrangler CLI / Dashboard | Dashboard (one-time) |
Future: Vercel for envo-dashboard
This plan is only for the vault site. The envo-dashboard (Next.js) should go to Vercel separately. When that time comes:
- Terraform has a Vercel provider
- Can live in
envo-dashboard/infra/or a sharedinfra/repo app.ehq.techordashboard.ehq.tech→ Vercelbrain.ehq.tech→ Cloudflare Pages (this plan)
Both managed via Terraform, same IaC pattern.