Why Fastlane
Fastlane exists because QTI failed to scale. The legacy Quote to Issue system — driven by TypeScript config files full of closures and a server-to-client code execution pipeline — turned every carrier, state, and LOB change into a developer task. Fastlane replaces that dead-end architecture with a CDM-driven platform where product managers own configuration and engineers own code. This document lays out exactly why.
What QTI Is
QTI (Quote to Issue) is the legacy quote-to-bind system that runs inside Client Rater Node (CRN) and Digital Agent (DA). It was built to handle the full flow from quoting to policy issuance across carriers.
- CRN (
legacy/client-rater-node): Node.js/Express backend. Houses all QTI business logic, config files, and carrier integrations. - DA (
legacy/goosehead-apps/apps/da): The customer-facing frontend built on Next.js 14 with React 18. Despite the modern frontend framework, DA still depends on CRN's closure-based config pipeline — it fetchesrouteConfigfrom CRN, deserializes it viaJSONfn.parse()(which usesnew Function()), and renders forms dynamically. The frontend framework is modern; the underlying architecture is not. - PaP (Purchase a Policy): QTI with client overrides — the client-facing binding flow. Same architecture, same problems.
The fundamental issue: QTI embeds business configuration inside application code. Every change to field visibility, validation rules, dropdown options, or carrier-specific behavior requires a developer to modify TypeScript files, pass code review, and deploy. This is not a tooling problem. It is an architectural one.
QTI's Architecture: The Problem
Config-as-Code Anti-Pattern
QTI's configuration lives in TypeScript files organized into a deep hierarchy:
pages → steps → forms → layout.rows.columns.fields
Every field in this hierarchy carries dataModelGetter and dataModelSetter closures — JavaScript functions embedded directly in config objects. The normalized export of a single carrier config (docs/safeco-legacy-config.json) is over 6,600 lines, and it is littered with entries like:
{
"dataModelGetter": "[Function dataModelGetter]",
"dataModelSetter": "[Function dataModelSetter]",
"validation": { "validationType": "required" }
}
These closures are non-serializable, which means:
- Config cannot be stored in a database
- Config cannot be diffed or audited outside of git
- Config cannot be edited by non-developers
- Config cannot be unit tested without executing the entire runtime context
Over 460 field definitions across QTI use this closure-based pattern. Every single one is a liability.
Fragmented Configuration Surface
There is no single source of truth for QTI configuration. Constants, options, and business rules are scattered across:
| File | Contents |
|---|---|
safeco.constants.ts | XML mappings, payment types, validation constants |
fieldOptions.ts | Dropdown options for form fields |
qtiDisclaimers.constants.ts | Carrier disclaimers and legal copy |
driversPage.config.ts | Driver form layout and field definitions |
vehiclesConfig.ts | Vehicle form layout and field definitions |
liabilityCov.ts | Coverage configuration |
ConfigService/data/ | Per-carrier bundles (Progressive, Safeco, Travelers, Mercury) |
A product manager who wants to change a single dropdown option must know which of these files contains the value, file a ticket, wait for a developer to find it, modify it, get it reviewed, and deploy it. For a dropdown change.
Internal Inconsistency
QTI could not even maintain a consistent internal structure. Two competing config trees exist for the same carrier:
services/qti/carriers/Safeco/SafecoAuto/configs/— legacy Safeco configservices/qti/carriers/SafecoV3_1/SafecoAutoV3_1/routeConfigs/— V3.1 rewrite
Same carrier, same LOB, two entirely different config structures. This is what happens when an architecture makes refactoring so expensive that the team builds a parallel system instead of fixing the original.
Scalability Failure
QTI's config model scales multiplicatively. For N carriers, M states, and K LOBs, the system requires up to N × M × K sets of config files, override logic, and constants — all maintained by developers. Adding a new state to an existing carrier means:
- Create new config files (or clone existing ones)
- Write state-specific override logic
- Update constants and field options
- Developer code review
- Full regression testing
- Code deploy
Every step requires engineering time. The platform's capacity to support new markets is gated entirely by developer headcount.
Frontend Modernization Did Not Fix the Architecture
DA runs Next.js 14 with React 18 — a modern rendering layer. But none of that matters because it still depends on CRN's closure-based config pipeline: CRN serializes closure-laden config via JSONfn.stringify(), DA deserializes it via JSONfn.parse() (backed by new Function()), and the entire config-as-code pipeline remains intact. The frontend framework is modern; the architecture is not.
Security Debt
During the QTI-to-Fastlane migration, 8 security weaknesses were identified and remediated:
| Vulnerability | QTI | Fastlane |
|---|---|---|
| SQL injection | String interpolation in Salesforce queries (with basic sanitization); Knex query builder for Postgres (parameterized but no ORM) | Prisma ORM with parameterized queries |
| XSS | JSONfn.parse() / new Function() executes server-provided code in the browser; dynamicImportFromBlob() imports arbitrary JS | React's automatic output encoding; no executable code from server |
| CSRF | Per-route CSRF via csrf-csrf; not globally enforced; bypassable via env var | Global token-based CSRF protection |
| Input validation | Client-side only | Server-side validation (class-validator) + client-side (Zod) |
| Session management | Weak session cookies | Redis-backed sessions with proper expiry and rotation |
| Authorization | No RBAC | Role-based access control |
| Data exposure | PII in application logs | Structured logging with PII redaction |
| Dependencies | Outdated packages with known CVEs | Actively maintained dependency tree |
These are not theoretical risks. They are documented findings from a production system that was handling real customer data.
Server-Side Code Exposed to the Client
Beyond the eight weaknesses above, QTI has a structural security flaw that deserves its own section.
Every carrier service in QTI serializes its entire route configuration — including JavaScript function source code — and sends it to the client. The flow:
-
Server: Each carrier service (Safeco Auto, Safeco Home, Progressive, Root, Clearcover, SageSure, Travelers) calls
JSONfn.stringify(config)to convert the config object — closures and all — into a string. Thejson-fnlibrary converts everydataModelGetter,dataModelSetter,validation, andisVisiblefunction to its raw source code representation. -
Transport: The serialized config is delivered to the browser over Server-Sent Events (
Content-Type: text/event-stream) via theGET /quotes/:quoteId/schemasendpoint inqti.controller.v2.ts. -
Client: The DA frontend calls
JSONfn.parse(configuration)inconfig-utilities.tsto reconstruct the functions.JSONfn.parse()internally usesnew Function()— functionally equivalent toeval()— to convert strings back into executable JavaScript. -
Config bundles: When config bundling is enabled, CRN serves pre-built JavaScript bundle files from
/bundlesas public static assets (express.static) with a one-year cache header and CORS enabled. The client usesdynamicImportFromBlob()— a bareimport()call — to execute these bundles directly in the browser.
This means:
- Server-side business logic is readable in the browser. Every validation rule, data model field name, getter/setter implementation, and conditional visibility function is exposed in the client. Anyone with browser DevTools can read the full internal configuration of every carrier flow.
- Executable code is transmitted from server to client. The
JSONfn.parse()path reconstructs functions vianew Function(), and the bundling path dynamically imports and executes JavaScript from the server. Both are code execution vectors. - Config bundles are publicly served. The
/bundlesroute serves carrier-specific JavaScript files (Safeco, Progressive, Root, Travelers, etc.) with no authentication — just CORS andexpress.static.
Fastlane's config is declarative data in the database. No functions. No eval. No executable code sent to the client. The client receives JSON field definitions and renders forms accordingly.
Operational Debt
Beyond architecture and security, QTI carries operational problems that compound over time.
Mixed Module Systems
CRN is a .js/.ts/ESM hybrid. Constants, models, middleware, and routers are split across both file types with no consistent pattern:
| Layer | .js files | .ts files |
|---|---|---|
| Constants | coverage.constants.js, errors.constants.js, deductibles.constants.js, property.constants.js, orion180.constants.js | common.constants.ts, validation.constants.ts, pg.constants.ts, sf.constants.ts |
| Models | sfContact.model.js, branch.model.js, index.model.js | pgAccount.model.ts, pgDriver.model.ts, pgVehicle.model.ts |
| Middleware | csrf.middleware.js, cors.middleware.js, helmet.middleware.js, morgan.middleware.js, qtiValidators.middleware.js, rateLimiters.middleware.js | authentication.middleware.ts, dtc-auth.middleware.ts, session.middleware.ts |
| Routers | quotes.router.js, driver.router.js, agent.router.js, qti.router.js, property.router.js | agentLead.router.ts, featureFlag.router.ts, extender.router.ts |
| Services | session.service.js, vehicles.service.js, carrier.service.js, drivers.service.js, quotes.service.js | (QTI services are .ts) |
This is not a migration in progress — it is the steady state. New developers must understand both module systems, both import styles, and the implicit boundaries between them. Tooling (linting, type checking, bundling) must accommodate both, adding build complexity for zero business value.
Fastlane is TypeScript strict mode end to end. One language, one module system, one set of tooling.
Dependency Bloat
QTI's dependency tree includes frameworks the team actively needs to migrate away from:
| Dependency | Problem |
|---|---|
| InversifyJS | DI framework requiring full refactor to move to NestJS |
| Braintree (via Forward API) | Payment processing for Home via external API integration; Auto needs Hydra — two payment stacks for the same platform |
| Moment.js + moment-timezone | Deprecated date library; date-fns or native Intl preferred |
| Puppeteer | Headless Chrome for PDF generation — CPU-heavy, complicates Heroku builds |
| Redis 3.x | Two major versions behind; missing modern features (streams, modules) |
| module-alias | Brittle runtime path aliasing (_moduleAliases in package.json maps 17 top-level paths) vs TypeScript path mappings |
| reflect-metadata | Required for InversifyJS decorators — dead weight once InversifyJS is removed |
The Braintree/Hydra split deserves emphasis. Legacy Safeco Home uses Braintree for payments. Safeco Auto in Fastlane uses Hydra. The same platform, the same carrier — two entirely different payment integrations. This is what happens when there is no shared abstraction layer.
Two Migration Systems
CRN maintains three separate database migration directories:
db/migrations/— Raw SQL files executed viapgclient indb/migration.service.js(current, 89 files)db/qtiMigrations/— QTI-specific raw SQL migrations (current, 24 files)migrations/— Knex JavaScript migrations (deprecated, "being phased out" with "limited support")
The deprecated Knex migration system cannot be removed because historical data depends on it. The two current SQL directories split migrations by domain with no clear boundary. A service file (db/migration.service.js) orchestrates the legacy path. Developers must know which directory to target, which to avoid, and how the three systems interact. This is cognitive overhead that delivers nothing.
Fastlane uses Prisma migrations exclusively. One system, one schema file, one migration history.
Deployment-Time Config Bundling
QTI configs are not loaded at runtime — they are bundled at build time via npm run bundle-configs during the Heroku post-build step. The heroku-postbuild script also moves Puppeteer's Chromium cache into the deployment slug. This means:
- Config changes require a full build and deploy cycle, not just a restart
- The build step is heavier than it needs to be (Puppeteer cache management)
- There is no way to update config without redeploying the entire application
Fastlane's config lives in the database. Changes are immediate. No build. No deploy. No Puppeteer cache dance.
Fastlane's Architecture: The Solution
CDM-Driven Configuration
Fastlane's Canonical Data Model (CDM) stores all field definitions, options, defaults, and validation rules in the database — not in code.
Base CDM → Carrier Override → LOB Override → State Override
Override resolution uses first-match specificity: a carrier + LOB + state override beats carrier + state, which beats carrier + LOB, which beats carrier alone, which beats the base CDM. This is a single, deterministic resolution path. No code. No closures. No guessing which file has the value you need.
The database tables:
| Table | Purpose |
|---|---|
ghcms_cdm_schemas | Base schema definitions |
ghcms_cdm_fields | Field definitions (type, options, defaults, validation) |
ghcms_cdm_carrier_overrides | Carrier × LOB × State overrides |
forms | Form definitions |
form_fields | Fields linked to CDM via cdmPath |
PM Self-Service
Fastlane Admin is an internal CMS where product managers configure the platform per carrier × state × LOB — without touching code or waiting for deploys.
PMs can:
- Toggle field visibility
- Change labels and placeholder text
- Modify dropdown options
- Adjust validation rules
- Set defaults per carrier/state/LOB
- Add or hide fields for specific markets
The change is live as soon as it is saved. No pull request. No deploy pipeline. No regression suite.
Separation of Concerns
Fastlane enforces a hard boundary between flow structure (owned by developers) and business configuration (owned by PMs):
| Concern | Owner | Location |
|---|---|---|
| Step sequence and navigation | Developer | flow-config.ts per carrier flow |
| Page components and UX | Developer | React components in apps/fastlane-portal/ |
| Field visibility, labels, options | PM | Fastlane Admin (CDM + Forms) |
| Validation rules and defaults | PM | Fastlane Admin (CDM overrides) |
| Carrier API integration | Developer | libs/apis/carriers/<carrier>/ |
Developers define what the flow looks like. PMs define what the flow contains. Neither steps on the other's work.
Modern Stack
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite 7, Radix UI, Tailwind CSS |
| Routing | React Router 7 |
| State | Zustand, TanStack React Query |
| Forms | React Hook Form + Zod (portal), class-validator (API) |
| API | NestJS, Express |
| Database | PostgreSQL with Prisma ORM |
| Cache | Redis |
| Monorepo | NX, PNPM |
TypeScript strict mode everywhere. No any. Parameterized queries only. Server-side validation on every endpoint.
Head-to-Head Comparison
| Dimension | QTI | Fastlane |
|---|---|---|
| PM empowerment | Config in code; every change needs a developer | PMs configure via Admin UI; zero code changes |
| Config source | TypeScript files with closures | Database (CDM + forms + overrides) |
| Override model | Code-based overrides scattered across files | DB overrides with carrier × LOB × state specificity |
| Field options | Hardcoded in fieldOptions.ts, safeco.constants.ts | CDM cdmPath; options resolved from database |
| Adding a carrier/state | New config files + override logic + deploy | Admin config only; no code changes |
| Security | 8 security vulnerabilities; inconsistent query safety; CSRF not globally enforced | Prisma ORM; global CSRF; server-side validation |
| Frontend | Next.js 14 / React 18; still coupled to CRN's JSONfn pipeline | React 19, Vite 7; declarative config only |
| Testability | Config contains closures; requires runtime context | Declarative config; trivially serializable and testable |
| Auditability | Git history on config files (if you can find the right one) | Database audit trail + Admin UI history |
| Developer onboarding | Learn CRN config hierarchy, JSONfn pipeline, closure patterns, mixed .js/.ts | Standard React + NestJS; DDD layer conventions |
| Separation of concerns | Business logic and config interleaved in TypeScript | Flow structure (code) cleanly separated from config (DB) |
| Deployment for PM changes | Full code deploy pipeline | Immediate — no deploy required |
| Module system | Mixed .js/.ts across constants, models, middleware, routers | TypeScript strict mode end to end |
| Dependencies | InversifyJS, Braintree + Hydra split, Moment.js, Puppeteer, Redis 3.x | NestJS DI, unified payment abstraction, modern deps |
| Database migrations | Three directories (two SQL + deprecated JS); migration.service.js orchestrator | Prisma migrations — one system, one schema |
| Config deployment | Bundled at build time; requires full Heroku redeploy | Database-driven; changes are live immediately |
| Code exposure | Server-side functions serialized via JSONfn.stringify() and sent to browser over SSE; config bundles served as public static JS | Declarative JSON config only; no executable code sent to client |
Adding a New State: QTI vs Fastlane
QTI: 6 steps, multiple developers, days to weeks. Fastlane: 3 steps, one PM, minutes.
Conclusion
QTI was an architecture that scaled with developer headcount. Every carrier, state, and LOB combination required hand-crafted config files maintained by engineers. The result: 460+ closure-laden field definitions with non-serializable config, 8 security vulnerabilities in production, two competing config trees for the same carrier, a mixed .js/.ts codebase with multiple migration systems, build-time config bundling that blocks rapid iteration, a payment stack split across Braintree and Hydra with no shared abstraction, and a server-to-client code execution pipeline that exposes business logic in the browser.
These are not growing pains. They are the predictable consequences of an architecture that treats business configuration as code. Every operational problem QTI has — from the deployment complexity to the dependency bloat to the module system fragmentation — traces back to this single design decision.
Fastlane eliminates it. Business configuration lives in the database, managed by product managers through Fastlane Admin. Engineers own flow structure, page components, and carrier integrations — not dropdown options, field labels, and config file merges. Adding a new state is a PM task that takes minutes, not a developer task that takes weeks.
The platform scales with product requirements, not engineering capacity. That is why Fastlane exists.
See Canonical Data Model and Fastlane Admin for technical details on the configuration system.