Skip to main content

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 fetches routeConfig from CRN, deserializes it via JSONfn.parse() (which uses new 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:

FileContents
safeco.constants.tsXML mappings, payment types, validation constants
fieldOptions.tsDropdown options for form fields
qtiDisclaimers.constants.tsCarrier disclaimers and legal copy
driversPage.config.tsDriver form layout and field definitions
vehiclesConfig.tsVehicle form layout and field definitions
liabilityCov.tsCoverage 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 config
  • services/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:

  1. Create new config files (or clone existing ones)
  2. Write state-specific override logic
  3. Update constants and field options
  4. Developer code review
  5. Full regression testing
  6. 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:

VulnerabilityQTIFastlane
SQL injectionString interpolation in Salesforce queries (with basic sanitization); Knex query builder for Postgres (parameterized but no ORM)Prisma ORM with parameterized queries
XSSJSONfn.parse() / new Function() executes server-provided code in the browser; dynamicImportFromBlob() imports arbitrary JSReact's automatic output encoding; no executable code from server
CSRFPer-route CSRF via csrf-csrf; not globally enforced; bypassable via env varGlobal token-based CSRF protection
Input validationClient-side onlyServer-side validation (class-validator) + client-side (Zod)
Session managementWeak session cookiesRedis-backed sessions with proper expiry and rotation
AuthorizationNo RBACRole-based access control
Data exposurePII in application logsStructured logging with PII redaction
DependenciesOutdated packages with known CVEsActively 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:

  1. 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. The json-fn library converts every dataModelGetter, dataModelSetter, validation, and isVisible function to its raw source code representation.

  2. Transport: The serialized config is delivered to the browser over Server-Sent Events (Content-Type: text/event-stream) via the GET /quotes/:quoteId/schemas endpoint in qti.controller.v2.ts.

  3. Client: The DA frontend calls JSONfn.parse(configuration) in config-utilities.ts to reconstruct the functions. JSONfn.parse() internally uses new Function() — functionally equivalent to eval() — to convert strings back into executable JavaScript.

  4. Config bundles: When config bundling is enabled, CRN serves pre-built JavaScript bundle files from /bundles as public static assets (express.static) with a one-year cache header and CORS enabled. The client uses dynamicImportFromBlob() — a bare import() 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 via new Function(), and the bundling path dynamically imports and executes JavaScript from the server. Both are code execution vectors.
  • Config bundles are publicly served. The /bundles route serves carrier-specific JavaScript files (Safeco, Progressive, Root, Travelers, etc.) with no authentication — just CORS and express.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
Constantscoverage.constants.js, errors.constants.js, deductibles.constants.js, property.constants.js, orion180.constants.jscommon.constants.ts, validation.constants.ts, pg.constants.ts, sf.constants.ts
ModelssfContact.model.js, branch.model.js, index.model.jspgAccount.model.ts, pgDriver.model.ts, pgVehicle.model.ts
Middlewarecsrf.middleware.js, cors.middleware.js, helmet.middleware.js, morgan.middleware.js, qtiValidators.middleware.js, rateLimiters.middleware.jsauthentication.middleware.ts, dtc-auth.middleware.ts, session.middleware.ts
Routersquotes.router.js, driver.router.js, agent.router.js, qti.router.js, property.router.jsagentLead.router.ts, featureFlag.router.ts, extender.router.ts
Servicessession.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:

DependencyProblem
InversifyJSDI 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-timezoneDeprecated date library; date-fns or native Intl preferred
PuppeteerHeadless Chrome for PDF generation — CPU-heavy, complicates Heroku builds
Redis 3.xTwo major versions behind; missing modern features (streams, modules)
module-aliasBrittle runtime path aliasing (_moduleAliases in package.json maps 17 top-level paths) vs TypeScript path mappings
reflect-metadataRequired 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 via pg client in db/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:

TablePurpose
ghcms_cdm_schemasBase schema definitions
ghcms_cdm_fieldsField definitions (type, options, defaults, validation)
ghcms_cdm_carrier_overridesCarrier × LOB × State overrides
formsForm definitions
form_fieldsFields 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):

ConcernOwnerLocation
Step sequence and navigationDeveloperflow-config.ts per carrier flow
Page components and UXDeveloperReact components in apps/fastlane-portal/
Field visibility, labels, optionsPMFastlane Admin (CDM + Forms)
Validation rules and defaultsPMFastlane Admin (CDM overrides)
Carrier API integrationDeveloperlibs/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

LayerTechnology
FrontendReact 19, Vite 7, Radix UI, Tailwind CSS
RoutingReact Router 7
StateZustand, TanStack React Query
FormsReact Hook Form + Zod (portal), class-validator (API)
APINestJS, Express
DatabasePostgreSQL with Prisma ORM
CacheRedis
MonorepoNX, PNPM

TypeScript strict mode everywhere. No any. Parameterized queries only. Server-side validation on every endpoint.

Head-to-Head Comparison

DimensionQTIFastlane
PM empowermentConfig in code; every change needs a developerPMs configure via Admin UI; zero code changes
Config sourceTypeScript files with closuresDatabase (CDM + forms + overrides)
Override modelCode-based overrides scattered across filesDB overrides with carrier × LOB × state specificity
Field optionsHardcoded in fieldOptions.ts, safeco.constants.tsCDM cdmPath; options resolved from database
Adding a carrier/stateNew config files + override logic + deployAdmin config only; no code changes
Security8 security vulnerabilities; inconsistent query safety; CSRF not globally enforcedPrisma ORM; global CSRF; server-side validation
FrontendNext.js 14 / React 18; still coupled to CRN's JSONfn pipelineReact 19, Vite 7; declarative config only
TestabilityConfig contains closures; requires runtime contextDeclarative config; trivially serializable and testable
AuditabilityGit history on config files (if you can find the right one)Database audit trail + Admin UI history
Developer onboardingLearn CRN config hierarchy, JSONfn pipeline, closure patterns, mixed .js/.tsStandard React + NestJS; DDD layer conventions
Separation of concernsBusiness logic and config interleaved in TypeScriptFlow structure (code) cleanly separated from config (DB)
Deployment for PM changesFull code deploy pipelineImmediate — no deploy required
Module systemMixed .js/.ts across constants, models, middleware, routersTypeScript strict mode end to end
DependenciesInversifyJS, Braintree + Hydra split, Moment.js, Puppeteer, Redis 3.xNestJS DI, unified payment abstraction, modern deps
Database migrationsThree directories (two SQL + deprecated JS); migration.service.js orchestratorPrisma migrations — one system, one schema
Config deploymentBundled at build time; requires full Heroku redeployDatabase-driven; changes are live immediately
Code exposureServer-side functions serialized via JSONfn.stringify() and sent to browser over SSE; config bundles served as public static JSDeclarative 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.