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)
| Fact | How verified |
|---|---|
bayzeinc/Docs-Internal exists, private, GitHub Team plan, default branch main, 2 commits | gh repo view |
Local clone at ~/dev/worktrees/don/Docs-Internal, remote corrected after the rename | git remote -v, git pull |
bayze.com DNS is on Cloudflare (ada/dane.ns.cloudflare.com); docs.bayze.com unused | dig |
| lab.bayze.com already runs behind Cloudflare Access | Bayze-DS/lab/orchestrator.py:331-470, lab/infrastructure/cloudflared-config.yml |
| Lab is green/purple branded, product is orange — the inconsistency JH flagged | lab/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-gated | Notion connector self |
| Notion is at its free-plan block limit and effectively read-only | JH |
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 directorypublic, 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 thepages.devpreview 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).
3.3 Publishing — the link works immediately
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 publishreduces 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 intobayzeinc/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.
| Database | Destination | Note |
|---|---|---|
| Feature Backlog | Jira | one-time import as issues under epics |
| Live Site Issues | Docs | becomes the post-mortems index table |
| Pre-Launch Alerts | Archive | already marked deprecated; not migrated |
| Engineering Standups | Drop | daily rows, no search value after the day |
| Customer Meetings | Docs | dated pages under customer research |
| Customers · Touchpoints | CRM | out of scope, see §8 |
| Features Notes | Docs | content/product/features/; its Epic tags become front matter |
| Strategy Documents | Docs | content/strategy/; Category becomes tags |
| Bayze Team Calendar | Discard, after confirming | appears to be an unused Notion template — its locations are Madison Park and a San Francisco office, and Bayze is in Honolulu |
| Finance & Operations Calendar | Split | dates 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
- 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.
- Quartz 5 skeleton, the content tree above with an index page per section, README, editor settings.
- Site configuration and the Bayze theme: orange accent, the home tiles from the approved design.
docs-site-maintainersteam; push ruleset restricting the site paths to it; force-push blocked.scripts/publish.py,scripts/reindex.py,functions/[[path]].js, the pre-commit confirmation.- The
/docscommand 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.
- Cloudflare Pages → Workers & Pages → Create → Pages → Connect to Git →
bayzeinc/Docs-Internal. Production branchmain, preset None, buildnpx quartz plugin install && npx quartz build, outputpublic. Note thepages.devaddress. - Custom domain → project → Custom domains →
docs.bayze.com. Cloudflare creates the record itself, since the zone is already there. - Check for an existing Microsoft connection → Zero Trust → Integrations → Identity providers. If the lab already created one, skip step 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. - Protect the domain → Access controls → Applications → Self-hosted →
docs.bayze.com→ policy Allow when email ends with@bayze.com→ 24-hour session. - Protect the previews → Pages project → Settings → General → Enable access policy.
- Store the GitHub read token as a Cloudflare Pages environment secret for the instant-link function.
- 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
- JH runs the export (or the per-page fallback) and hands over the zip.
- Claude writes the mapping table first — every Notion page to its repository folder — for JH’s review before converting anything.
- 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.
- Sensitivity pass under the rewritten rule, then transcribe text-only screenshots so search finds their content.
- Commit per section, direct to main.
- 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.mdbeside 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
| Claim | Proven at |
|---|---|
| Full workspace export is available on Notion’s free plan | Phase 0 step 1 — before any building |
| Cloudflare rebuild takes ~1–2 minutes | Phase 1 gate, measured |
| The site function runs only when a built page is missing | Phase 1 gate |
| A markdown renderer fits inside that function; images are served the same way | Phase 1 gate |
| A failed build leaves the previous deployment serving | Phase 1 gate, deliberate broken build |
| GitHub API rate limits leave headroom | Phase 1 gate |
| The Bayze Team Calendar is an unused template | Phase 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:
| Root | What is in it | Destination |
|---|---|---|
| Bayze Home | handbook, engineering, product, strategy, marketing, metrics, data quality, customer success | docs (as planned) |
| Bayze Meetings | Engineering Standups + Customer Meetings databases | drop / customer-research |
| 📚 Customer Discovery | the book project | content/customer-research/discovery/ |
| All About Customers | a standalone page | customer-research |
| Teamspace Home | teamspace landing page | check, probably discard |
| Applicants | a database of named job candidates | NOT docs — see below |
| Investor Updates | page + database of investor updates | restricted — see below |
| Exec Sync | leadership meeting notes | restricted — see below |
| Finance & Operations Tracker | 2025 finances, R&D tax credits | restricted — see below |
| Finance & Operations Calendar | the compliance database | split: dates → M365, knowledge → docs |
| Process Tracker | a database of issues, looks abandoned (Dec 2025) | archive, confirm first |
| Meeting Notes | a separate page from Bayze Meetings | fold 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:
- Do not migrate — they stay in Notion or move to a finance/board tool.
- 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). - A separate private repo and site for restricted material. Recommendation: (2) for investor updates, finances and exec sync; (1) for Applicants.