Documentation Platform
Start simple: ship static HTML, watch for a signal that we need more, and only then expand. This page is that plan — the crawl we commit to now, and the approach if signal appears. Written for two readers: humans skimming, and Claude ingesting (the docs are also our LLM's knowledge base). Decision: ADR-0001.
Start simple — watch for signal
The crawl is deliberately boring: a static HTML docs site, private via SSO, holding what we already have. Then we watch:
| Signal we see | What it justifies |
|---|---|
| People actually use it; we're hand-copying docs between repos | Walk — auto-aggregate repo docs |
| Docs sprawl across many repos; discussions happen in Slack and get lost | Run — ADR comments in-place |
| "Where's the doc for X?" keeps getting asked | Run — an LLM assistant over our docs |
| No real usage | Stay static — that's a fine outcome |
No signal → we stop here, having spent ~$10/mo. If signal appears, here's the approach ↓.
Written for humans and Claude
Every page targets two readers at once, and it costs us nothing extra to serve both:
- Humans skim headings, diagrams, and tables (concise, visual — the house style).
- Claude ingests the same clean, semantic Markdown. The content store that builds the site is the exact source the LLM assistant retrieves from — so good docs are good RAG. No separate "AI content" to maintain.
This is why we keep docs as structured Markdown (not PDFs/screenshots) and aggregate them into one store: it's simultaneously the site's source, the search index, and the LLM's knowledge base.
The approach — if signal appears
Everything below is the expansion plan. We don't build any of it until a signal above justifies it — it's here so that when the time comes, the decision is already made.
Do we need a backend?
A static site can't hold an LLM key or shared comments. Any interactive feature forces a backend:
flowchart TD
Q{What does the
page need to do?}
Q -->|Read docs only| S[✅ Static only
host + SSO gate]
Q -->|LLM chat| B1[Backend required
keys + RAG run server-side]
Q -->|Persistent comments| B2[Backend required
shared database]
B1 --> STACK[Pick a hosting stack ↓]
B2 --> STACK
style S fill:#EDEDFF,stroke:#34D399
style STACK fill:#EDEDFF,stroke:#3B54F7
So: docs + ADRs alone = static. Add the LLM or comments and we need compute + data. The frontend can't do it alone (API keys can't ship to the browser; comments need shared storage).
Where should the backend live?
Three coherent stacks — all tunnel-free (resolving the earlier cloudflared uptime worry):
| Stack | Static + SSO | Backend + data | LLM | Cost / mo | Trade-off |
|---|---|---|---|---|---|
| A · All-Cloudflare | Pages + Access | Workers + D1 | Workers AI / Vectorize | ~$5–15 + usage | Simplest, cheapest, one vendor — but away from Bedrock & our AWS accounts |
| B · All-AWS ✅ | CloudFront + S3 + Cognito | API GW + Lambda + DynamoDB | Bedrock + vector store | ~$10–25 core + $43+ RAG + usage | One cloud/IAM, next to Bedrock & the client accounts we already query |
| C · Hybrid | CF Pages + Access | AWS (Access-JWT validated) | Bedrock | ~$10–30 + RAG | CF's easy SSO + AWS brain — but two vendors, two auth models |
✅ Recommendation: Stack B (all-AWS). Our LLM (Bedrock), the client accounts we query, and the platform are all AWS — one cloud, one IAM, no cross-vendor auth glue. Pick A only if simplest/cheapest matters more than Bedrock proximity; C rarely earns its split-auth cost.
On SSO specifically: Cloudflare Access gates a backend fine (it can sign requests an AWS origin validates — no tunnel). But if the backend is AWS anyway, Cognito keeps auth in the same cloud as the data and model.
Recommended architecture (Stack B)
flowchart TD
subgraph repos["Source repos (N)"]
R[repo /docs + generated
terraform-docs · TypeDoc · OpenAPI]
end
R -->|CI on merge| CB[(S3 content bucket
single source of truth)]
CB -->|Docusaurus build| SITE[(S3 site · private OAC)]
SITE --> CF[CloudFront + Lambda@Edge OIDC]
CF -->|Google SSO + MFA| USER([Propel engineer])
USER -->|comment / ask| API[API Gateway · Cognito JWT]
API --> FN[Lambda]
FN --> DDB[(DynamoDB · comments)]
FN --> BR[Bedrock RetrieveAndGenerate]
CB -.indexed.-> KB[Bedrock Knowledge Base] -.-> BR
style SITE fill:#EDEDFF,stroke:#3B54F7
style CB fill:#E4E4FF,stroke:#23233A
Getting docs in (per repo)
Each repo declares what to publish in a .propel-docs.yml config; a reusable GitHub Action reads it, runs the generators, and syncs the bundle to the content bucket. No hand-copying.
flowchart LR
subgraph repo["Each service repo"]
CFG[".propel-docs.yml
what + audience"]
SRC["/docs + code"]
end
CFG --> GHA[propel-docs
GitHub Action]
SRC --> GHA
GHA -->|generate + assemble| CB[(S3 content bucket
services//)]
CB --> BUILD[site build] --> SITE[published docs]
style CB fill:#E4E4FF,stroke:#23233A
style SITE fill:#EDEDFF,stroke:#3B54F7
# .propel-docs.yml — read by the reusable propel-docs Action
service: svc-gpr
audience: internal # internal · client · both → controls section + surface
section: Services
sources: [ docs/ ] # hand-written markdown
generate: # auto-generated reference
- { type: openapi, input: openapi.yaml }
- { type: terraform-docs, input: modules/ }
- { type: typedoc, input: src/ }
publish: { target: s3 } # s3 (decoupled, default) · pr (review-gated)
- Config + Action, not one or the other — the config declares intent; the shared Action (
propel-ai/docs-publish@v1) does the work on merge to main. One line of CI per repo. - Publish target:
s3pushes to the content bucket (decoupled, scales) — default.propens a PR to this repo instead, when you want doc changes reviewed/versioned in git. - Audience flag routes docs:
internal→ this Propel-only site;client→ a per-client surface (same content bucket, client-scoped access, mirroring the observability model);both→ published to each.
✅ This closes ADR-0001's open aggregation question: per-repo
.propel-docs.yml+ a shared Action publishing to the content bucket.
Components
| Layer | Choice | Note |
|---|---|---|
| Aggregation | Repo CI → S3 content bucket | Single source of truth; feeds both the site build and the LLM index |
| Site | Docusaurus (React/MDX) | Versioning, search, embeddable widgets (Astro Starlight = lighter alt) |
| Hosting | CloudFront + private S3 (OAC) | Bucket never public; no tunnel |
| Auth | Cognito → Google Workspace, Lambda@Edge | MFA at Google; same identity gates site + API + LLM |
| Comments | API GW + Lambda + DynamoDB | Scale-to-zero; per-section threads (v1), inline anchors (v2) |
| LLM | Bedrock Knowledge Base + chat endpoint | Vector store: pgvector ~$43 vs OpenSearch Serverless ~$345 |
Rollout
flowchart LR
P1[1 · Static docs
+ SSO] --> P2[2 · Repo doc
generation] --> P3[3 · ADRs
+ comments] --> P4[4 · LLM
assistant]
style P1 fill:#EDEDFF,stroke:#3B54F7
style P4 fill:#F3EDFF,stroke:#8A54E8
Core (static + comments) runs ~$10–25/mo; the LLM phase is the only material add and is opt-in. Full rationale and alternatives: ADR-0001.