A fullstack web application that renders personalized Growth Charter documents from CRM node data stored in MongoDB. Built for the DeepThought Full-Stack Developer assignment.
The Growth Charter is an auto-generated narrative document that mirrors a manufacturing company founder's growth ambition, execution gaps, and infrastructure reality back to them. It's assembled from structured CRM node data — not written by hand.
This app:
- Stores CRM node data in MongoDB — 10 classification nodes per company, each with 8 possible values
- Renders a Growth Charter — uses a markdown template, regex pattern, and render map to convert node values into readable narrative text
- (Part B) Extracts node values from a founder conversation transcript using Google's Gemini API
- Node.js (v18+)
- MongoDB (local install or Atlas free tier)
- (Optional) Gemini API key from Google AI Studio for Part B
# 1. Clone the repo
git clone <your-repo-url>
cd growth-charter-renderer
# 2. Install dependencies
npm install
# 3. Configure environment
cp .env.example .env
# Edit .env with your MongoDB URI (default: mongodb://localhost:27017/growth-charter)
# For Part B: add your GEMINI_API_KEY
# 4. Start MongoDB (if running locally)
mongod
# 5. Start the app
npm start
# Server runs at http://localhost:3000
# 6. Open browser → http://localhost:3000
# Click "Seed Database" to load sample data
# Select "Sureflow Formulations" → View Growth CharterFrontend (Vanilla JS) → Express API → MongoDB
| | |
index.html /api/seed accounts collection
charter.html /api/accounts nodes collection
extract.html /api/charter/:id
/api/extract
Frontend: Static HTML + vanilla JavaScript. No React or framework — this is a document renderer, not a SPA. The charter page fetches rendered HTML from the API and injects it into the DOM.
Backend: Node.js + Express. Four routes handle seeding, listing accounts, rendering charters, and (Part B) transcript extraction. The rendering logic lives in server/lib/renderCharter.js.
Database: MongoDB with Mongoose. Two collections: accounts (company info) and nodes (one document per classification dimension per company, linked by accountId).
Rendering happens server-side: The backend builds a placeholder map from node data + render map, replaces {{placeholders}} in the markdown template using a regex, converts to HTML with marked, and sends the final HTML to the frontend.
-
Node structure (Section "What Is a Node?") — Each node has a nodeId, name, value (1-8), optional companion fields, and optional verbatim fields. This directly maps to the
nodescollection schema. -
Companion fields summary table — K1 has name/title/background, F2 has actualRevenue/revenueSource, C7 has systemsInUse, D2 has primaryMetric/currentValue/targetValue. These are stored as a flexible
Mixedtype in Mongoose because each node has different companion fields. -
The placeholder-to-data mapping table (Step 3) — This table in context.md is basically the spec for
renderCharter.js. Each placeholder maps to either an account field, a render-map lookup, or a companion field. I built the placeholder map exactly from this table. -
Verbatim table handling (Step 5) — context.md says verbatim table replacement must happen BEFORE the main regex pass. This drove the order of operations in the rendering function.
-
Compound unique index
{ accountId: 1, nodeId: 1 }— Directly from the MongoDB schema section. Prevents duplicate node entries.
The assignment says "any framework." But the app has two pages: a form and a document display. React would be overkill and would signal "I used a generator" rather than "I understood the problem." The rendering complexity is server-side (template + regex), not client-side.
-
Render map key type mismatch —
render-map.jsonuses string keys ("1") butseed-data.jsonstores values as numbers (1). I convert to string before lookup (String(node.value)) and assert the result isn't undefined. -
Verbatim table replacement order —
context.mdexplicitly says to replace{{verbatimTable}}before the main regex pass. I do this in two separate steps inrenderCharter.js. -
Gemini response parsing — LLMs often wrap JSON in markdown code fences. I strip
```jsonwrappers before parsing and validate the response structure.
-
Add a node editor — Let a DT consultant score nodes directly in the browser instead of only seeding from JSON. A form with 10 dropdowns (one per node), companion field inputs, and a save button. This would make the app actually usable in a workflow.
-
Charter version history — Store each rendered charter as a snapshot with a timestamp, so you can see how a company's charter changed as nodes were re-scored over time.
-
Structured validation for Part B — After Gemini extracts nodes, show each extracted value alongside the actual transcript quote that supported it, and let the user correct any misclassifications before saving. Right now the review step just shows node IDs and values.
-
PDF export — Add a "Download as PDF" button using a library like
puppeteerorhtml-pdf. The Growth Charter is meant to be shared with founders, and PDF is the natural format for that. -
Will be pasting the google drive link for the video walkthrough!
growth-charter-renderer/
├── server/
│ ├── index.js # Express server entry point
│ ├── models/
│ │ ├── Account.js # Mongoose schema for accounts
│ │ └── Node.js # Mongoose schema for nodes
│ ├── routes/
│ │ ├── seed.js # POST /api/seed — load sample data
│ │ ├── accounts.js # GET /api/accounts — list all
│ │ ├── charter.js # GET /api/charter/:id — render charter
│ │ └── extract.js # POST /api/extract — Gemini extraction (Part B)
│ ├── lib/
│ │ ├── renderCharter.js # Core rendering logic (template + regex + render map)
│ │ └── gemini.js # Gemini API wrapper for Part B
│ └── data/
│ ├── seed-data.json # Sample company data from assignment
│ ├── render-map.json # Node value → text mapping from assignment
│ ├── sample-transcript.json # Sample transcript for Part B
│ └── charter-template.md # Markdown template with {{placeholders}}
├── public/
│ ├── index.html # Homepage — seed + account selector
│ ├── charter.html # Charter display page
│ ├── extract.html # Part B — transcript input
│ ├── css/style.css # Styles
│ └── js/
│ ├── main.js # Homepage logic
│ ├── charter.js # Charter display logic
│ └── extract.js # Part B logic
├── .env.example
├── .gitignore
├── package.json
└── README.md