05-Specifications / 05.12.Obsidian-Vault-Static-Site.spec

05.12.Obsidian Vault Static Site.spec

Spec: Publish the Obsidian Vault as an Auth-Gated Static Site on GCS

1. Objective

Make the entire bnc-cpt-doc Obsidian vault browsable in a web browser at a stable internal URL, while keeping the same files unmodified for use inside Obsidian. The site is auth-gated via Google Identity-Aware Proxy (IAP) — only authenticated members of the team's Google Workspace / IAM group can read it. Editing remains in Obsidian; the published site is read-only.

The pipeline is intentionally minimal: run a build command inside the vault, sync the output to a GCS bucket, and let an existing HTTPS Load Balancer + IAP enforce auth.

2. Architecture & Components

2.1 Static Site Generator: Quartz 4

2.2 Hosting: GCS Bucket Behind HTTPS LB + IAP

2.3 Build & Publish Trigger

3. Build & Publish Flow

3.1 Local Build

  1. Pre-req (one-time): npx quartz create initializes scaffolding, then npm install in bnc-cpt-doc/.
  2. Build: bash sudo -u ysg bash -c 'cd /opt/bnc/bnc-cpt/bnc-cpt-doc && npx quartz build' Produces bnc-cpt-doc/public/ (gitignored).
  3. Local preview (optional, for sanity-check before publish): bash sudo -u ysg bash -c 'cd /opt/bnc/bnc-cpt/bnc-cpt-doc && npx quartz build --serve' Serves on http://localhost:8080.

3.2 Publish

sudo -u ysg bash -c 'cd /opt/bnc/bnc-cpt/bnc-cpt-utl && ./run -a do_build_and_publish_docs'

The shell action: 1. Resolves ${ORG}, ${APP} from the existing do_set_vars_v205. 2. cd into bnc-cpt-doc/ and runs npx quartz build. 3. Activates the deployer service account from ~/.gcp/.${ORG}/key-${ORG_APP}-inf.json. 4. gsutil -m rsync -d -r public/ gs://${ORG}-${APP}-doc-site/-d removes objects no longer in source, -r recurses, -m parallelizes. 5. (If Cloud CDN is enabled in front of the LB) issues gcloud compute url-maps invalidate-cdn-cache for /*.

3.3 CI/CD

4. Privacy & Authentication (the key design choice)

4.1 Auth Model

4.2 Why no per-document filter is needed

4.3 Threat model & residual risks

5. Technical Requirements

5.1 Quartz Configuration (bnc-cpt-doc/quartz.config.ts)

5.2 Quartz Layout (bnc-cpt-doc/quartz.layout.ts)

5.3 Shell Action (bnc-cpt-utl/src/bash/run/build-and-publish-docs.func.sh)

5.4 Terraform / Infra Changes

A. Reusable static-site module enhancement (bnc-cpt-inf/src/terraform/modules/gcp-bucket-for-web-site-static-v01/):

B. New step bnc-cpt-inf/src/terraform/017-gcp-doc-site/:

C. DNS (extension to step 007-dns):

D. New Secret Manager entries (step 029-create-gcp-secrets):

E. Service Account (step 003-gcp-iam-users or alongside step 017):

5.5 CI/CD (.github/workflows/publish-docs.yml in bnc-cpt-doc)

name: Publish docs
on:
  push: { branches: [master] }
  workflow_dispatch:
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm ci && npx quartz build
      - id: leak-scan
        run: gitleaks detect --no-banner --source .
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
          service_account: ${{ secrets.DOC_DEPLOYER_SA }}
      - uses: google-github-actions/setup-gcloud@v2
      - run: gsutil -m rsync -d -r public/ gs://${{ secrets.DOC_BUCKET }}/
      - run: gcloud compute url-maps invalidate-cdn-cache ${{ secrets.DOC_URL_MAP }} --path "/*" --async

5.6 Vault-Side Hygiene

5.7 Security

6. Decisions Log

# Question Decision
1 Static site generator Quartz 4 — Obsidian-native (wikilinks, backlinks, graph, callouts, transclusions). MkDocs/Docusaurus rejected on Obsidian-feature parity grounds
2 Hosting Private GCS bucket behind HTTPS LB — reuses existing gcp-bucket-for-web-site-static-v01 module; cheaper than Cloud Run, no cold start
3 Authentication Auth-gated via Google IAP (Option 2) — public bucket rejected because vault contains internal-only material (project-health notes, security analyses, ad-hoc tracking)
4 Per-doc privacy filter Not needed — full vault is published behind IAP; per-doc filtering deferred until a need to share a subset publicly arises
5 Build trigger Manual shell action (do_build_and_publish_docs) + GitHub Actions on push to master — same code path; CI uses Workload Identity Federation (no key files)
6 Bucket naming / per-env Single env-agnostic bucket ${ORG}-${APP}-doc-site — docs are not environment-specific
7 Vault layout disturbance Zero — Quartz contentRoot points at doc/md/; no symlinks, no moves, Obsidian opens the same vault unchanged
8 Allow-list management Google Workspace group (e.g. csitea-developers@csitea.net) — never per-user IAM bindings
9 DNS hostname docs.${root_domain} via extension to existing step 007-dns
10 Search engine exposure Belt-and-suspenders: IAP redirects unauthenticated bots to sign-in; Quartz also emits <meta name="robots" content="noindex">
11 Pre-publish secret scan Required — gitleaks in CI before gsutil rsync; prevents accidental leakage even though access is gated
12 CI auth method Workload Identity Federation — no JSON keys checked into Actions secrets

Operational Specification v1.0.0 — Quartz 4 · Private GCS bucket behind HTTPS LB · IAP auth-gated · Workspace-group allow list · Zero vault disturbance · Manual + CI publish