Architecture Overview
See also: Story Engine · Dynamic Component System · Data Model · Local Development
Stretched ("translate money into hours of your life") is an NX monorepo: a zoneless, signals-first Angular 22 frontend on a Firebase backend. The core architectural idea is that a Story — variables, madlib-style ${} sections, and an optional component tree — is content the engine renders live, not bespoke page code per feature. The stories that ship today are hand-authored runtime Story objects in TypeScript (components/story-teller/foundation-stories.ts + cost-stories.ts → ALL_STORIES); a Firestore-backed admin builder is a separate authoring path. See Story Engine for the full picture — and note the compact stories/*.json → catalog.json set is a separate authoring-only artifact (no runtime consumer since the /stories/map viewer was removed 2026-07-29, and no compiler to the app), NOT the stories users play.
Monorepo layout
| Project | Path | What it is |
|---|---|---|
stretched | apps/stretched | The web app (Angular 22, zoneless, signal forms). CSR-only — SSR was removed 2026-07-19 (hosting always served the static browser/ output anyway). Installable PWA: Angular service worker + manifest.webmanifest, production builds only. Routes wire DI seams (registry, story store, catalog source) per feature. |
stretched-mobile | apps/stretched-mobile | Ionic + Capacitor shell around the same components and Firebase. See Mobile App. |
stretched-chrome-extension | apps/stretched-chrome-extension | MV3 extension: prices → hours on any page, framework-free, REST-only. See Chrome Extension. |
firebase-functions | apps/firebase-functions | Cloud Functions (feedback email, geocoding, etc.). See Feedback System and Secrets Management. |
storybook / storybook-e2e | apps/storybook* | Component workbench with DI-level mocks; runs the whole dynamic system Firebase-free. See Testing. |
stretched-e2e, stretched-mobile-e2e | apps/*-e2e | Cypress suites against a fresh emulator. See Testing and Local Development. |
stretched-components | libs/stretched-components | The component library and the whole dynamic rendering engine (src/dynamic-host/). Deliberately Firebase-free — backends plug in through injection tokens. See Component Library Guide and Design System and Foundations. |
stretched-types | libs/stretched-types | Shared type contracts (@stretched/types), no runtime code. Both Angular apps and Functions import from here. See Data Model. |
firebase-permissions | libs/firebase-permissions | All Firestore/Storage/RTDB security rules. See Firestore and Security Rules. |
The story pipeline, end to end
⚠️ This diagram is the designed admin-authoring pipeline — not how today's shipped stories got here. The 33 stories users play are hand-authored TypeScript (
foundation-stories.ts/cost-stories.ts→ALL_STORIES) wired straight intoSTORY_CATALOG_SOURCE; they skip the Authoring and Publish stages entirely, and the store → JSONexportarrow is not built yet. The Runtime half (catalog → StoryTeller → context → answer) is exactly how every story renders; the Authoring → Publish half is the intended future for admin-composed stories.
Step by step:
- Author — an admin composes a story on the canvas at
/admin/story-builder(Page Builder), declaring typed variables and building a component tree with{ $ref }bindings, gates, and generator specs (Context and Bindings, Expressions and Math). - Serialize & store — the builder's
DynamicFormDefinitionis serialized to aStory(thekinddiscriminator is stripped; it is re-derived from the registry on load) and saved through theSTORY_STOREseam — Firestore in the app, in-memory JSON in Storybook (Story Engine). - Publish — story definitions are content: versioned, cacheable JSON (bundled asset today, CDN later), read at runtime through
STORY_CATALOG_SOURCE→StoryCatalogService. No Firestore read on the user path (Story Engine, Story Categories). - Render —
StoryTellerdeclares the story's variables into the sharedApplicationContextService, parses madlib${key}sections into text + inline inputs, and renders the root tree through the dynamic hosts (Dynamic Component System). - Answer — inputs write to variable signals;
{ $ref }-bound displays,sectionGateconditions, and{ $gen }chart series all react live. Answers snapshot into a per-userUserStoryEntryunder the user's own Firestore document (Data Model, Auth and Users).
Key seams (why the library stays Firebase-free)
libs/stretched-components never imports Firebase. Every backend concern crosses an InjectionToken with a degraded default, so Storybook and tests run the full engine standalone:
| Token | Purpose | App binding | Default |
|---|---|---|---|
DYNAMIC_REGISTRY_TOKEN | component-key → registry entry | ALL_DYNAMIC_COMPONENTS per route | — |
STORY_STORE | authoring writes (drafts) | StoryStoreService (Firestore) | LocalStoryStore (in-memory JSON) |
STORY_CATALOG_SOURCE | runtime reads (published stories) | bundled array today, CDN fetch later | empty catalog |
The same pattern covers other services (LOCATION_SEARCH_FN, NUMBER_WORDS_PROVIDER): tokens carry a safe fallback so a story rendered without a provider degrades instead of crashing (see gotcha 8 in libs/stretched-components/src/components/CLAUDE.md).
Where to go next
- How components become JSON-renderable → Dynamic Component System
- The
Storyshape, catalog vs. store, gates, locales → Story Engine - Authoring UI → Page Builder · taxonomy → Story Categories
- Variables,
{ $ref }/{ $gen }→ Context and Bindings · expression language & finance math → Expressions and Math - Types & ER diagram → Data Model · rules → Firestore and Security Rules
- Auth, admin claim → Auth and Users · payments → Pricing and Subscriptions · partners → Partner Program · feedback → Feedback System
- Building UI → Component Library Guide · Design System and Foundations
- Working locally → Local Development · Tooling and Scripts · Testing · CI CD and Deploys · Secrets Management
- This wiki's publishing model → Wiki Publishing