Skip to main content

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.tsALL_STORIES); a Firestore-backed admin builder is a separate authoring path. See Story Engine for the full picture — and note the compact stories/*.jsoncatalog.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

ProjectPathWhat it is
stretchedapps/stretchedThe 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-mobileapps/stretched-mobileIonic + Capacitor shell around the same components and Firebase. See Mobile App.
stretched-chrome-extensionapps/stretched-chrome-extensionMV3 extension: prices → hours on any page, framework-free, REST-only. See Chrome Extension.
firebase-functionsapps/firebase-functionsCloud Functions (feedback email, geocoding, etc.). See Feedback System and Secrets Management.
storybook / storybook-e2eapps/storybook*Component workbench with DI-level mocks; runs the whole dynamic system Firebase-free. See Testing.
stretched-e2e, stretched-mobile-e2eapps/*-e2eCypress suites against a fresh emulator. See Testing and Local Development.
stretched-componentslibs/stretched-componentsThe 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-typeslibs/stretched-typesShared type contracts (@stretched/types), no runtime code. Both Angular apps and Functions import from here. See Data Model.
firebase-permissionslibs/firebase-permissionsAll 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.tsALL_STORIES) wired straight into STORY_CATALOG_SOURCE; they skip the Authoring and Publish stages entirely, and the store → JSON export arrow 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:

  1. 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).
  2. Serialize & store — the builder's DynamicFormDefinition is serialized to a Story (the kind discriminator is stripped; it is re-derived from the registry on load) and saved through the STORY_STORE seam — Firestore in the app, in-memory JSON in Storybook (Story Engine).
  3. Publish — story definitions are content: versioned, cacheable JSON (bundled asset today, CDN later), read at runtime through STORY_CATALOG_SOURCEStoryCatalogService. No Firestore read on the user path (Story Engine, Story Categories).
  4. RenderStoryTeller declares the story's variables into the shared ApplicationContextService, parses madlib ${key} sections into text + inline inputs, and renders the root tree through the dynamic hosts (Dynamic Component System).
  5. Answer — inputs write to variable signals; { $ref }-bound displays, sectionGate conditions, and { $gen } chart series all react live. Answers snapshot into a per-user UserStoryEntry under 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:

TokenPurposeApp bindingDefault
DYNAMIC_REGISTRY_TOKENcomponent-key → registry entryALL_DYNAMIC_COMPONENTS per route
STORY_STOREauthoring writes (drafts)StoryStoreService (Firestore)LocalStoryStore (in-memory JSON)
STORY_CATALOG_SOURCEruntime reads (published stories)bundled array today, CDN fetch laterempty 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