Documentation
docs portal
Docs Portal
Sam's note: this document was written by Maya and lightly edited by Sam. The voice is hers. Maya had strong opinions about the structure of this document and they were correct, so the structure is hers too.
What It Is
The docs portal is a React application running on port 4000 that serves as the single place to find everything about the Meridian CRM system. It exists because documentation scattered across markdown files in a repository is documentation that most people will never read. A developer knows to look in the repo. A product manager, a new joiner on their first day, a stakeholder who wants to understand what the notification service actually does — none of them will find it there.
The portal solves a discovery problem. It takes the same markdown files that already
exist in docs/, the OpenAPI specs that each service already exposes, and the system
architecture that Jan and Lyn designed — and it puts them somewhere with a URL that
anyone can open in a browser without cloning anything or running anything locally.
It is not a documentation generator in the style of tools that consume code comments. It is a small product built specifically for this project. Maya's position is that a documentation system built for a specific audience produces better results than a general one bolted on as an afterthought, and the docs portal is her evidence for that claim.
What It Serves
The portal has four main sections.
Documentation. All the markdown files in docs/ are rendered as HTML. This includes
the spec, the personas, the services reference, the observability guide, the simulation
document, the seed data guide, and the user guide. They are fetched at runtime from the
public/docs/ directory, where they are copied during the build step. The DocViewer
component renders them with consistent typography and a table-of-contents sidebar derived
from the headings.
API Documentation. Swagger UI panels for each of the six downstream services:
client-service, invoice-service, product-service, notification-service,
audit-service, and report-service. Each panel is driven by that service's
openapi.yaml file, which is fetched at runtime either directly from the service (if
CORS allows) or via a small proxy endpoint built into the portal. The Swagger UI shows
live request/response examples, not static screenshots.
Product Overview. The home page shows the system architecture diagram and a brief description of what the Meridian CRM system is and why it exists. This is the page Maya directs new team members to on their first day. It references the full spec for detail but does not require the reader to have read the spec first.
Team. A page of persona cards — one for each member of the team described in
docs/personas.md. The cards are not a mirror of that document; they are shorter
summaries with the person's name, role, and what they own. The full backstory lives in
the markdown document, which is linked from each card.
Tech Stack
The portal is a React 18 application built with Vite. The component library is shadcn/ui, which provides accessible, unstyled components that are styled with Tailwind CSS. Maya chose shadcn/ui because it gives her full control over the markup — she is not fighting against a component library's opinions about spacing and colour when she needs to match the portal's visual language to the project.
Navigation is handled by React Router. Light mode is the default. The colour tokens and
theme configuration live in docs-portal/src/theme.ts, which is the single source of
truth for the visual design. Dark mode is supported through Tailwind's dark: variant
and a toggle in the sidebar, but the default is light because most of the portal's
content is long-form text and light mode is more readable for sustained reading.
TypeScript is used throughout. The build output is static HTML, CSS, and JavaScript, served by nginx in the production container.
Structure
docs-portal/
├── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── theme.ts # colour tokens, light/dark mode config
│ ├── pages/
│ │ ├── Home.tsx # product overview + architecture diagram
│ │ ├── Docs.tsx # renders markdown docs
│ │ ├── ApiDocs.tsx # Swagger UI for a selected service
│ │ ├── Team.tsx # persona cards
│ │ └── Services.tsx # service health + links
│ ├── components/
│ │ ├── Sidebar.tsx # navigation
│ │ ├── DocViewer.tsx # markdown renderer
│ │ ├── SwaggerPanel.tsx # Swagger UI wrapper
│ │ └── ServiceCard.tsx
│ └── lib/
│ └── docs.ts # fetches markdown files at runtime
├── public/
│ └── docs/ # markdown files copied here at build time
├── openapi/ # symlinks or copies of all service openapi.yaml files
├── index.html
├── vite.config.ts
├── tailwind.config.ts
├── components.json # shadcn/ui config
├── tsconfig.json
├── package.json
└── Dockerfile
The pages/ directory contains the top-level route components. The components/
directory contains the reusable pieces. lib/docs.ts handles fetching the markdown
files at runtime — it builds the list of available documents from a manifest and fetches
each one as plain text, passing it to DocViewer for rendering.
The openapi/ directory contains either symlinks to the openapi.yaml file in each
service directory, or copies of them, depending on whether the build environment supports
symlinks. The Vite dev server proxies requests to the local service instances when
running in development mode, so the Swagger UI shows real responses.
Integration with OpenAPI Specs
Each downstream service exposes its contract as an openapi.yaml file in the root of
its source directory. Maya is the owner of the OpenAPI spec review process: when Jan or
another engineer adds a new endpoint or changes a response shape, the spec update is part
of the same pull request, and Maya reviews it for completeness and clarity before it
merges.
At runtime, the Swagger UI in the portal fetches each service's spec from the
openapi/ directory (served as static files) and renders the interactive documentation.
If the service is running — which it will be if the full stack is up — the "Try it out"
feature in Swagger UI sends real requests and shows real responses. This is intentional:
the API documentation should be live documentation, not a snapshot that drifts from
reality.
In environments where CORS prevents the Swagger UI from calling the services directly,
the portal includes a small proxy endpoint that forwards requests to the appropriate
service and adds the necessary headers. This is configured in vite.config.ts for
development and in the nginx configuration for production.
Testing
Maya writes unit tests for every component and end-to-end tests for the main navigation flows. This is partly because Jan insisted and partly because she found that she agreed with him once she started doing it. A docs portal with broken navigation is worse than no docs portal — it creates the impression that documentation exists while making it inaccessible — and the tests catch that class of failure before it reaches anyone.
Unit tests use Jest and React Testing Library. Every component in components/ has
a corresponding test. The tests verify that components render the expected content given
their props, that navigation links point to the correct routes, and that the markdown
renderer handles common edge cases (empty content, long headings, code blocks) without
breaking the layout. Run them with:
npm testEnd-to-end tests use Playwright. They verify the main navigation flows: loading the home page, navigating to a documentation page, switching between services in the API docs section, and loading the team page. They require the full stack to be running because they test the live Swagger UI integration. Run them with:
npm run test:e2eThe e2e tests are not part of the default CI run for the same reason the Go e2e tests are not: they require a running stack. They are run manually before releases and as part of the extended CI pipeline that runs on the main branch.
Docker
The portal is built with Vite and served by nginx on port 4000. The Dockerfile follows
a two-stage build: the first stage uses a Node image to run npm run build, and the
second stage copies the build output into nginx:alpine. The nginx configuration serves
the dist/ directory as static files and handles client-side routing by redirecting all
unmatched paths to index.html.
The container exposes port 80 internally. Docker Compose maps it to port 4000 on the
host. Adding docs-portal to the stack requires no changes to the other services — it
reads their OpenAPI specs and their markdown documentation at runtime, but they have no
dependency on it.
Why It Exists
Maya's reasoning, in her words: documentation is a product, not a byproduct. Products have users. Products are designed for those users. Products are tested. A markdown file in a repository is a document; a portal is a product. The distinction matters because a product requires you to think about who is going to use it and what they need to accomplish, rather than simply writing down what you know.
The docs portal is the place where the system explains itself to people who were not in the room when the decisions were made. That is what documentation is for.