Skip to content

jbw-sam — Serverless Backend (AWS SAM / Node.js)

Module path: st-integration/jbw-sam/ · CloudFormation stack jbw-prod-api (us-east-1) · template description “jbw-backend”

The serverless backend for the JBW360 portal (jbwui) and the integration layer between dealers’ Field Service Management Systems, FSMs (ServiceTitan, Workiz) and the JB Warranties platform (public-api.jbwarranties.com). It provides:

  1. Onboarding / tenant management — dealer signup (Cognito), ServiceTitan tenant registration (clientId/clientSecret/tenantId), Workiz account registration, credential + custom tag/field verification.
  2. Data sync & matching — pulls dealer agreements from the JBW public API and locations/equipment from ServiceTitan (or job locations from Workiz) into the MySQL staging DB (jbwst, via RDS Proxy with IAM auth), then runs the jbw_st_dataSync stored procedure to match ST locations ↔ JBW agreements.
  3. Agreement creation — builds agreement + equipment payloads and POSTs them to the JBW agreements API; writes agreement references back into ServiceTitan locations as notes/custom fields, and pushes claim/agreement status back into Workiz jobs (jbwworkizjobdatapush, added July 2026 — Workiz is no longer pull-only).
  4. Claims — potential-claim creation, bidirectional claim status sync (JBW JB360ClaimStatus SQL view ↔ tblClaims), staging claims for the ST push done by the extraction module.
  5. Dashboards, notifications, email — admin/dealer KPI data, pending agreement/claim notification emails via SES.
  6. Frontend hosting — S3 bucket + CloudFront distribution serving the jbwui SPA.
  • 63 Lambda functions (Node.js 20, Handler: index.handler, 128 MB default, 180 s default timeout) defined in template.yaml (~1,750 lines), fronted by one API Gateway (${Environment}_JBWApiGateway). A further 44 source dirs under src/ are not in the template — see caveats.
  • 44 additional function source dirs exist under src/ but are NOT in the template — deployed/managed manually in the AWS console (including the Cognito authorizer). See the code reference.
  • All DB-touching functions run inside a VPC and connect to MySQL through RDS Proxy with IAM auth (no DB password; token from AWS.RDS.Signer). Globals: RDS_DATABASE=jbwst, proxy hostname, RDS_USERNAME=admin.
  • SQS queue prodtenantList (outside this stack, referenced by the JBWSQSURL parameter) is the hand-off to the extraction Java Lambda. Two functions enqueue to it: getTenantInfo (one message per active ServiceTitan tenant) and jbwworkiztenantifno (one per active Workiz account, different message shape — dealerId/accountId/batchId/syncStartDate, no clientId/clientSecret/tenantId). The second is untemplated and was undocumented before July 2026.
  • S3 + CloudFront (WebSiteBucket, CloudFrontDistribution, Origin Access Control, geo-whitelist US/CA/GB/DE) host the SPA.
  • No DynamoDB, Step Functions, EventBridge rules (Scheduler IAM policy only), or Lambda layers in the template.

The API Gateway is deployed with DefaultAuthorizer: none. Cognito auth is attached post-deploy, out of band:

  • src/cognitoclienttokenauthorizer/ — a Lambda authorizer that validates Cognito JWTs (jsonwebtoken + jwks-rsa) against stage-mapped user pools and returns an IAM policy. Not in the template.
  • src/jbwapiauthorizerupdate/ — a utility Lambda that iterates every API Gateway method and sets authorizationType=COGNITO_USER_POOLS + authorizer id (OPTIONS left open). Invoked manually with {restApiId, authorizerId, region} after a deploy. Not in the template.

Any redeploy of the API therefore requires re-running the authorizer-attach step, or the API is open (CORS is also AllowOrigin: '*').

Full method+path → function table is in the code reference. Summary by domain:

DomainRoutes (examples)
Dealers & onboardingGET /dealer, GET /dealer/{id}/verify, /onboard, /cardsdata, POST /newdealeronboard, GET /dealeronboard, GET /dealer/nononboardedlist, GET /dealer/latestonboardeddealers
Registration & usersPOST /dealerregister/add, GET /dealerregister, PUT /dealerregister/userstatus, POST /signup, /changepassword, /resetpassword
TenantsGET /tenant/{tenantid}, GET /tenant/dealer/{dealerid}, PATCH /tenant/{tenantid}, GET /dealer/{id}/gettenantinfo (→ SQS)
AgreementsGET /agreements/dealer/{dealerid}, GET/POST/PATCH /agreements/{id}/equipments, PUT /agreements/{id}/equipmentsync (→ JBW API), POST /agreements/{id}/duplicateagreement, POST /agreements/dealer/{dealerid}/newinvoiceagreement
Agreement sync/matchingGET /agreementsync/{id}, GET /agreementsync/{id}/spcall (trigger sync), PUT /agreementsync/approve, PUT /agreementsync/{id}/location
ClaimsGET /claims/{dealerId}, PATCH /claims/stupdate (JBW → DB), PATCH /claims/reject/{claimid}, PATCH /claims/claimidupdate/{id}
Invoices / products / locationsGET /invoices/invoicedetails/{invoiceid}, GET /agreements/dealer/{dealerid}/invoicesearch, GET /tblproducts/search, GET /stlocations/search/{dealerid}
VerificationPOST /jbwservicetitan (ST credential + tag/field check), GET /workiz/accountverify
Dashboards / notificationsGET /admindashboard/cardsdata, GET /emailnotifications, PATCH /emailnotifications/{id}

Environment (dev|prod), APPNAME, JBWAPPURL, JBWADMINEMAIL, CognitoUserPoolId, RDSProxyResourceId, JBWSQSURL, JBWAgreementsURL, JBWAppLogoURL.

samconfig.toml — single environment: prod

Section titled “samconfig.toml — single environment: prod”

Stack jbw-prod-api, region us-east-1, CAPABILITY_IAM, confirm_changeset=true. Parameter overrides pin Environment=prod, APPNAME=jbw360, JBWAPPURL=https://jbw360.jbwarranties.com/, the Cognito portal pool, the RDS Proxy resource id, and the SQS queue URL (prodtenantList). No dev deploy config exists despite dev being an allowed value.

Secrets (⚠ all hardcoded — no SSM / Secrets Manager)

Section titled “Secrets (⚠ all hardcoded — no SSM / Secrets Manager)”
SecretWhere
ServiceTitan app key (ST_APP_KEY)env blocks of JBWClientServiceTitanVerification, jbwagreementsdatamatch, jbwgetstequipmentsfortenant in template.yaml
JBW public API token (JBW_API_TOKEN, also embedded in the JBWAgreementsURL parameter)template.yaml + samconfig.toml
Workiz API tokenhardcoded in 5 handler sources (was 3 before July 2026): src/jbwagreementsdatamatch/, src/jbwworkizaccountverify/, src/jbwworkizjobsdatafetch/, and newly src/jbwworkizjobdatapush/index.js:3, src/jbwworkizjoblocationsdatapull/index.js:3. In the two new ones it is interpolated into the request URL path (:4 of each), so it lands in any URL-logging surface
Per-dealer ServiceTitan clientId/clientSecretstored in DB table tblTenant (passed at runtime, not in the template)

DB access itself uses IAM auth (no stored password).

Build — per-function Zip; Node deps resolved from each function’s package.json:

sam build

Deploy — uses [prod.deploy.parameters] from samconfig.toml (stack jbw-prod-api, confirm_changeset=true):

sam deploy

Post-deploy checklist (manual, every deploy):

  1. Invoke jbwapiauthorizerupdate with {restApiId, authorizerId, region} to re-attach the Cognito authorizer — until this runs, the API is open.
  2. Spot-check a protected route returns 401 without a JWT.
  3. If the SPA changed: sync the jbwui build to WebSiteBucket and invalidate CloudFront.

Local testing: sam local invoke -e events/event.json — the only event fixture is the generic SAM hello-world API proxy stub; there are no domain-specific fixtures.

src/jbwdatapullfromJB/ (Python 3.12) ships vendored native deps (pymssql/pymysql, global-bundle.pem), so it must be built on Amazon Linux or in a container.

Caveats / Known Issues (as of current main)

Section titled “Caveats / Known Issues (as of current main)”

Improved in one place. src/jbwnewonboarding/index.js was rewritten (+271/−100) to fully parameterize all seven query helpers (getOnboardedDealer, getDealerInTenantColumn, addOnboardDealer, addWorkizAccount, updateDealerInfo, addTenant, addTransmissionDetails). It is currently the only handler that contradicts the blanket “string-interpolated” claim above.

Worsened overall — four new injection sites:

SiteShape
src/jbwclaimsdealeradmintoolsupdate/index.js:28... where id in (${ids})ids straight from the request body, unparameterized IN list on an UPDATE. The sharpest of the set.
src/jbwvwonboardeddealersearch/index.js:28where (dealerId like '%${searchKey}%' or dealerName like '%${searchKey}%') from queryStringParameters.key
src/jbwtenantbusinessunitsdatapull/index.js:131INSERT … ON DUPLICATE KEY UPDATE interpolating ServiceTitan API response fields (bunits.name, officialName) — injection via third-party data
src/jbwworkiztenantifno/index.js:27AND w.dealerId=${dealerId} from pathParameters.id

Other defects added by the July 2026 release

Section titled “Other defects added by the July 2026 release”
  • src/jbwworkizjoblocationsdatapull/index.js:62-91 cannot persist locations as writtenconst result = ''; (:68) is reassigned at :75 (TypeError), it uses a callback-style connection.query on a mysql2/promise connection, and commit()/return at :78-79 race ahead of the insert callback while finally (:89) closes the connection underneath it. It also reads event.body.accountid without JSON.parse (:9).
  • Connection leaksrc/jbwtenantbusinessunitsdatapull/index.js:129 assigns connection with no let/const (implicit global) and no try/finally, inside a loop over all tenants (:22-24) with no concurrency cap and no per-tenant error isolation.
  • src/jbwworkizjobdatapush/index.js — undeclared loop variable (:13), shadowed data (:56), and a Promise interpolated into an error string (:54, yields [object Object]).
  • README.md is SAM boilerplate.