Docs-Internal — final implementation plan

This supersedes plan.md in the same run directory, which is the working record of how we got here. Every decision below was ruled on by JH in session on 2026-09-09. Facts marked (unverified) are things I could not confirm from a primary source; each names the step that proves it.

During Phase 0 this file is committed into the new repo as content/engineering/docs-site-plan.md so the plan lives on the site it describes.


1. What we are building, in one paragraph

Bayze documentation moves out of Notion into a private GitHub repository, bayzeinc/Docs-Internal, published as a searchable website at docs.bayze.com behind our existing Microsoft sign-in. Writing a document is a plain save to the main branch — no pull request, no approval, no waiting — and the link works the moment it is saved. The site’s own machinery is protected so that a non-technical author cannot break it. Work tracking goes to Jira, customer records go to a CRM chosen separately, and everything else becomes a document. Cost is zero.

2. Ground truth (verified this session)

FactHow verified
bayzeinc/Docs-Internal exists, private, GitHub Team plan, default branch main, 2 commitsgh repo view
Local clone at ~/dev/worktrees/don/Docs-Internal, remote corrected after the renamegit remote -v, git pull
bayze.com DNS is on Cloudflare (ada/dane.ns.cloudflare.com); docs.bayze.com unuseddig
lab.bayze.com already runs behind Cloudflare AccessBayze-DS/lab/orchestrator.py:331-470, lab/infrastructure/cloudflared-config.yml
Lab is green/purple branded, product is orange — the inconsistency JH flaggedlab/portal/assets/logo_green.png, lab/portal/403.html
Marketing site is its own repo, bayzeinc/Bayze-Website (private, static bayze.com)gh repo list
Notion workspace is on a non-Business plan; AI search and meeting-note query are plan-gatedNotion connector self
Notion is at its free-plan block limit and effectively read-onlyJH

3. Decisions

3.1 Platform

  • Quartz 5 as the site generator. Version 4 stopped in April 2026; version 5 uses a YAML config and a different upgrade command. Config: address docs.bayze.com, graph view off, RSS and sitemap off, search on, navigation from folders, standard relative markdown links (not wikilinks, matching the workspace documentation standard).
  • Cloudflare Pages, connected to the private repo. Build command npx quartz plugin install && npx quartz build, output directory public, framework preset None, Node 22.16 (Cloudflare’s default matches Quartz’s pinned version). GitHub Pages is rejected: on the Team plan it publishes a private repo’s site publicly.
  • Cloudflare Access with Microsoft Entra ID, policy: email ends with @bayze.com. No individual exceptions — everyone including interns has a bayze.com account.
  • Two Access applications. One for docs.bayze.com, one for the pages.dev preview hostnames. Cloudflare’s one-click project toggle covers previews only; without the second application the sign-in page renders on the custom domain but does not work.
  • Branded sign-in, configured account-wide, which rebrands lab.bayze.com at the same time. Chosen over the instant-redirect option, which skips the branded page.

3.2 Permissions — no pull requests

Content is a direct push to main. The site is protected by a push ruleset restricting these paths to the docs-site-maintainers GitHub team: quartz/**, quartz.config.yaml, package*.json, .github/**, scripts/**, functions/**, .vscode/**. Force-push to main is blocked for everyone.

  • Maintainers at launch: John, Kai, Nate, Kristen. Membership is a GitHub team, so scaling is a membership change with no code edit.
  • A non-maintainer editing a site file is rejected with the filename named, including from the GitHub web editor.
  • Maintainers get a committed pre-commit confirmation (“this commit changes the site, not just content — continue?”), mirrored as a Claude Code hook. Pull requests remain available by choice for large site changes, never by rule.
  • Rejected: requiring pull requests with a bot merge (adds dead time and a moving part on the publish path); a separate content repository (two-repo dance on every publish); post-push revert bots (the change is already live).

Design principle, stated by JH: “clone the repo, use Claude, update — no other steps.” Any design requiring each contributor to hold a credential is wrong.

Every save triggers a Cloudflare rebuild of roughly one to two minutes (unverified — measure at the Phase 1 gate). We do not wait for it. The site carries a function that runs only when no built page exists at the requested address; on a miss it fetches the corresponding markdown file from GitHub, renders it with the site stylesheet, and serves it with a one-line note that navigation and search catch up shortly. When the build lands, the built page takes over and the function stops running for that path.

Consequences:

  • No per-person credentials at all. The one secret is a fine-grained, read-only GitHub token scoped to this repository, stored once in Cloudflare’s own project settings.
  • The browser editor gets instant links too, so non-technical authors are first-class.
  • bayze docs publish reduces to: validate front matter, links and sensitive-content warnings → commit → push → print the live address. Nothing to poll.
  • Page addresses are predictable, since Quartz lowercases and hyphenates the file path.

Unverified, proven at the Phase 1 gate: that the function runs only on a missing page; that a markdown renderer fits inside it; that images added in the same commit are served the same way; that GitHub API rate limits leave headroom. Fallback if any fails: move the staging render into a GitHub Action holding one repository secret — still zero per-person keys.

Claude artifacts are saved as their HTML file, served untouched at their docs address so they look exactly as designed, alongside a markdown twin carrying the summary, epic and stage so they are searchable and trackable.

3.4 The /docs command

/docs <artifact URL | Notion URL | file path | pasted text> [--epic BAYZE-###] [--kind …]

Reads the input, classifies it — post-mortem, meeting note, artifact needing an epic, handbook change that edits an existing page, or reference document — routes it through the right reviewing agent, files it in the correct folder, publishes, and replies with the live link. The habit becomes “add it to docs” instead of sharing a Claude artifact link.

Other verbs: bayze docs shipped --since, bayze docs new <kind>, bayze docs link <path>.

3.5 Agents

Registered in Bayze-Agents .claude/agents/ as docs-*. The workspace has one Claude configuration tree; peer repositories carry only a generated pointer file. Read the rules-sync skill before adding any of them.

  • docs-note-reviewer — the two-sided review. Not only a redactor.
    • Exclude side: quoted dollar values, named third parties, remarks that read as off the record. Default is keep; the author confirms.
    • Enrich side: what is missing that would make this findable — date, attendees, brand or company, which store, topic, decisions, action owners, related epic. Answers become front matter.
    • Returns a short list of questions, never silent edits.
    • Its rubric is a deliverable in its own right, drafted and reviewed before the agent is built. JH: “a little more thought needs to be put into exactly what should be raised as exclude versus what should be raised as a potential addition.”
  • docs-artifact-filer — enforces one template (Problem · Why now · Options considered · Decision · Investment case · Outcome) and the lifecycle front matter. Those sections are precisely what the code does not record and a blog post needs.
  • docs-release-storyteller — reads the artifacts here and the product repositories to see what actually shipped, then writes a draft post as a pull request into bayzeinc/Bayze-Website. Refuses any artifact not yet in production. Nothing blog-shaped is stored in this repo.

3.6 Artifact lifecycle — proposal to shipped, without a database

Artifact front matter carries: kind, epic, stage (proposal · approved · building · in-qa · in-prod · dropped), the date each stage was reached, the pull requests that built it, and announceable, true only in production.

A nightly script reads the Jira epic and the pull requests’ merge and promotion state — the workspace already computes what is live in QA but not production — and rewrites the stage. It never touches prose. Its only credential is a Jira one, held as a GitHub secret. bayze docs shipped --since 30d then lists what reached production in the window with each item’s Problem, Why and Decision sections: the blog-post candidate list.

3.7 Folder index pages — a general pattern

A folder may declare an index. Its index.md holds hand-written introduction text plus a region between <!-- INDEX:START --> and <!-- INDEX:END --> that scripts/reindex.py regenerates from sibling pages’ front matter, newest first. Publishing a page updates its parent index in the same commit; a check catches drift. Chosen over a build-time generated page so the index stays a real, editable, diffable file that also reads correctly in the repository.

Columns are declared per folder in content/<folder>/.index.yaml.

First uses: post-mortems (date, severity, time to detect, time to resolve — the metrics the post-mortem workflow already computes, .claude/skills/post-mortem/metrics.md:82,86), per-epic artifact lists, meeting years, customer research, and the shipped-this-month view.

3.8 Content sensitivity — rewritten for this repo only

The workspace rule that committed files carry no personal names, customer data or sales figures stays unchanged for the five product repositories. Docs-Internal is different by design: private, behind Microsoft sign-in, Bayze staff only, and built to hold exactly this knowledge.

  • Allowed and wanted here: customer and person names, store names and numbers, quoted dollar values, internal figures. Meeting notes are worthless without them.
  • Never, anywhere: credentials, tokens and keys; a customer’s own confidential documents; third-party confidences shared off the record.
  • The dollar warning stays — as a confirmation prompt, not a block.

Recorded in the documentation skill as a Docs-Internal exception clause, and in the portal-thumbnails-no-names memory.

3.9 Documentation boundary

docs.bayze.com holds company, cross-repository and human documents. Each product repository’s own docs/ folder keeps code-adjacent reference that must version with the code. docs/workspace-router.md gains a Docs-Internal row so there is still one map. This is the two-systems risk the capability register exists to catch, handled explicitly.

4. Repository structure

Docs-Internal/
├── README.md                      # one screen: what goes where, how to publish
├── CLAUDE.md                      # generated pointer only (config lives in Bayze-Agents)
├── content/                       # ── anyone at Bayze pushes here ──
│   ├── index.md                   # home tiles
│   ├── handbook/                  # mission, values, time off, holidays
│   ├── onboarding/                # new engineers, interns, service access
│   ├── engineering/               # dev-test-deploy, bug management, agentic engineering, privacy
│   ├── architecture/              # data routing, ML pipeline, Snowflake map
│   ├── product/
│   │   └── features/              # from the Notion Features Notes database
│   ├── strategy/                  # from the Notion Strategy Documents database
│   ├── data-quality/
│   ├── customer-success/          # PROCESS ONLY — never customer records
│   ├── customer-research/
│   │   ├── discovery/             # interviews, personas, concept notes, framework work
│   │   ├── feedback/              # what customers told us about the product
│   │   └── onboarding/            # customer onboarding meeting notes
│   ├── operations/
│   │   └── compliance-calendar.md # what each obligation is and how to file it
│   ├── postmortems/               # + index.md replacing the Notion register
│   ├── meetings/YYYY/
│   ├── artifacts/<EPIC-KEY>-slug/ # one folder per Jira epic, each with an index
│   └── assets/<section>/
├── quartz/  quartz.config.yaml  package.json     # ── maintainers only ──
├── functions/[[path]].js                         # the instant-link renderer
├── .github/CODEOWNERS  .github/workflows/        # link-check, sensitivity-check, nightly stage sync
├── scripts/                                      # publish.py, reindex.py, notion-normalise.py
└── .vscode/settings.json                         # paste an image, get a file and a link

5. Where every Notion database goes

Three destinations, per JH: Jira for direction, features, progress and OKRs; the future CRM for anything about prospects or customers; docs for everything else.

DatabaseDestinationNote
Feature BacklogJiraone-time import as issues under epics
Live Site IssuesDocsbecomes the post-mortems index table
Pre-Launch AlertsArchivealready marked deprecated; not migrated
Engineering StandupsDropdaily rows, no search value after the day
Customer MeetingsDocsdated pages under customer research
Customers · TouchpointsCRMout of scope, see §8
Features NotesDocscontent/product/features/; its Epic tags become front matter
Strategy DocumentsDocscontent/strategy/; Category becomes tags
Bayze Team CalendarDiscard, after confirmingappears to be an unused Notion template — its locations are Madison Park and a San Francisco office, and Bayze is in Honolulu
Finance & Operations CalendarSplitdates and reminders → a Microsoft 365 calendar; what each obligation is and how to file it → content/operations/compliance-calendar.md. Holds Delaware annual report, Hawaii general excise tax, payroll and franchise tax, each with jurisdiction, frequency and a seven-day reminder. Do not lose this in the migration.

6. Notion retirement sweep

The change is not done while a command still writes to Notion.

Repointed: post-mortem (writes the register — see below); feature-plan, feature-build, bug-fix, design-coach (all read specs from Notion pages) → read from content/artifacts/ and accept a docs URL.

Deleted at cutover: the notion-workspace skill and its four sub-files, the Notion MCP server registration, and its permission entries — each with a dated retirement marker, per the hard-delete rule. A notion rule is added to the legacy scanner so a new reference fails the test gate.

Rewritten: roughly twenty documentation files in Bayze-Agents and twenty-three across the product repositories (Platform 14, Data 6, Dashboard 3, DS 0, Pulse 0), mostly links to a Notion spec. One sweep pull request per repository, run after the export lands so every Notion link has a docs address to become.

Post-mortem sequencing: /post-mortem stops writing a Notion register entry and instead writes content/postmortems/<date>-<slug>.md plus its HTML artifact, then reindexes. Do this after the in-flight feat/post-mortem-workflow pull request merges, to avoid colliding with live work.

7. Phases

Phase 0 — Repository and rules · Claude · ~half a day

  1. First, before anything else: JH confirms in Notion whether Settings → General → Export all workspace content is available on the free plan. The whole migration depends on it and Notion’s own documentation does not say (unverified). If absent, fall back to per-page export with subpages, run on Bayze Home, Bayze Meetings and Customer Discovery.
  2. Quartz 5 skeleton, the content tree above with an index page per section, README, editor settings.
  3. Site configuration and the Bayze theme: orange accent, the home tiles from the approved design.
  4. docs-site-maintainers team; push ruleset restricting the site paths to it; force-push blocked.
  5. scripts/publish.py, scripts/reindex.py, functions/[[path]].js, the pre-commit confirmation.
  6. The /docs command and the three agents in Bayze-Agents.

Gate: site builds locally; a test account outside the maintainers team is rejected editing a site file and succeeds editing content.

Phase 1 — Live behind sign-in · JH clicks, Claude drives · ~half a day

Operator steps, in order. JH holds Entra and Cloudflare admin.

  1. Cloudflare Pages → Workers & Pages → Create → Pages → Connect to Git → bayzeinc/Docs-Internal. Production branch main, preset None, build npx quartz plugin install && npx quartz build, output public. Note the pages.dev address.
  2. Custom domain → project → Custom domains → docs.bayze.com. Cloudflare creates the record itself, since the zone is already there.
  3. Check for an existing Microsoft connection → Zero Trust → Integrations → Identity providers. If the lab already created one, skip step 4.
  4. Connect Entra → App registrations → New, name “Cloudflare Access”, redirect URI https://<team>.cloudflareaccess.com/cdn-cgi/access/callback → Certificates & secrets → new client secret → API permissions, delegated Microsoft Graph: email, offline_access, openid, profile, User.Read, Directory.Read.All, GroupMember.Read.All → Grant admin consent. Then Zero Trust → Identity providers → Add → Azure AD → paste Application ID, Directory ID, secret. Role required is Cloud Application Administrator, not Global Admin.
  5. Protect the domain → Access controls → Applications → Self-hosted → docs.bayze.com → policy Allow when email ends with @bayze.com → 24-hour session.
  6. Protect the previews → Pages project → Settings → General → Enable access policy.
  7. Store the GitHub read token as a Cloudflare Pages environment secret for the instant-link function.
  8. Brand the sign-in → Zero Trust → Reusable components → Custom pages → Access login page: organisation name Bayze, orange logo, header, footer, background. This rebrands lab.bayze.com too.

Credential handling: JH creates each token and writes it into the gitignored .bayze/user.toml himself using a !-prefixed command, so the value never enters a transcript. Never paste a token into chat.

Gate: anonymous visits to both docs.bayze.com and the pages.dev address hit the Bayze sign-in; a non-Bayze Microsoft account is refused; a /docs publish returns a link that opens within ten seconds showing the page in the site’s styling; the same address shows the fully built page a couple of minutes later; a deliberately broken preview build leaves production serving; the site reads correctly at a phone viewport (~390×844) before desktop.

Phase 2 — Content migration · JH exports, Claude converts

  1. JH runs the export (or the per-page fallback) and hands over the zip.
  2. Claude writes the mapping table first — every Notion page to its repository folder — for JH’s review before converting anything.
  3. Normalise: strip Notion’s id suffixes, rewrite links, relocate images, convert callouts and toggles, turn database exports into index pages, synthesise front matter recording the source page.
  4. Sensitivity pass under the rewritten rule, then transcribe text-only screenshots so search finds their content.
  5. Commit per section, direct to main.
  6. Notion gets a banner pointing at the new site, if the block limit still permits an edit.

Known losses: Notion comments (present only in an HTML export), non-default database views, and possibly AI meeting-note transcripts. Test one page of each before the full run.

Phase 3 — What Notion could not do

The note-review rubric, then the three agents; the nightly stage sync; link and front-matter checks; retention encoded in front matter with a monthly report; the Notion retirement sweep.

8. Out of scope

  • The CRM. The Notion Customers and Touchpoints databases do not move to docs. JH is running the product selection separately; the research prompt is in crm-research-prompt.md beside this file. Selection criteria: a connector Claude can use, LinkedIn support, low cost at three seats. This is now urgent rather than deferred: Notion is read-only, so those records are already frozen and the nightly PostHog sync into them may already be failing. Add the chosen product to the capability register so we do not grow two CRMs.
  • Work tracking. Roadmap, features, backlog and OKRs go to Jira, not here.

9. Unverified items and where each is proven

ClaimProven at
Full workspace export is available on Notion’s free planPhase 0 step 1 — before any building
Cloudflare rebuild takes ~1–2 minutesPhase 1 gate, measured
The site function runs only when a built page is missingPhase 1 gate
A markdown renderer fits inside that function; images are served the same wayPhase 1 gate
A failed build leaves the previous deployment servingPhase 1 gate, deliberate broken build
GitHub API rate limits leave headroomPhase 1 gate
The Bayze Team Calendar is an unused templatePhase 2, at export

10. Answers on record (JH, 2026-09-09)

Entra and Cloudflare admin is JH. Claude gets a scoped setup token, revoked after Phase 1; no per-person credentials anywhere. Maintainers are John, Kai, Nate and Kristen, as a GitHub team. Access is @bayze.com only — everyone including interns has an account. The CRM leaves scope. Customer research becomes its own section carried as pages, with status tracking excluded. The sensitivity rule is rewritten for this repository only, keeping the dollar warning. The note review is two-sided, exclude and enrich, and its rubric gets its own design pass. The post-mortem register becomes an index page, not a database. Compliance dates go to a calendar, compliance knowledge stays a document. Blog posts are generated from docs plus code into the website repository, never stored here. Notion has no cancellation date; it fades as docs takes over.

11. CORRECTION 2026-09-09 — export roots, and a hole in the access model

Workspace-wide export does not exist on the free plan (confirmed from the operator’s own settings screen). Per-page export is the route, and its dialog carries everything needed: format Markdown & CSV, page content Everything (this is what includes images and files), Include subpages on, Create folders for subpages on. Database views can only be current or default — non-default views are lost either way, as already recorded.

The sidebar revealed seven top-level roots the connector inventory never showed. Export must run once per root, not once from Bayze Home:

RootWhat is in itDestination
Bayze Homehandbook, engineering, product, strategy, marketing, metrics, data quality, customer successdocs (as planned)
Bayze MeetingsEngineering Standups + Customer Meetings databasesdrop / customer-research
📚 Customer Discoverythe book projectcontent/customer-research/discovery/
All About Customersa standalone pagecustomer-research
Teamspace Hometeamspace landing pagecheck, probably discard
Applicantsa database of named job candidatesNOT docs — see below
Investor Updatespage + database of investor updatesrestricted — see below
Exec Syncleadership meeting notesrestricted — see below
Finance & Operations Tracker2025 finances, R&D tax creditsrestricted — see below
Finance & Operations Calendarthe compliance databasesplit: dates → M365, knowledge → docs
Process Trackera database of issues, looks abandoned (Dec 2025)archive, confirm first
Meeting Notesa separate page from Bayze Meetingsfold into content/meetings/

The hole: one flat access policy, four restricted categories

The plan’s access model is a single rule — anyone with a bayze.com address reads everything. That is right for the handbook and engineering. It is not obviously right for recruiting records, investor updates, company finances and leadership notes, all of which every intern would be able to read.

Applicants is the sharpest case: it holds personal data about people who do not work here and never agreed to be filed in a company wiki. It should probably not migrate at all.

Options for the other three, needing an operator ruling:

  1. Do not migrate — they stay in Notion or move to a finance/board tool.
  2. A second Access application scoped to a path (e.g. docs.bayze.com/leadership/*) with a policy naming individuals. Cloudflare Access applications can be defined on a path as well as a hostname (U — verify at the Phase 1 gate before relying on it).
  3. A separate private repo and site for restricted material. Recommendation: (2) for investor updates, finances and exec sync; (1) for Applicants.