jbw-sam — Serverless Backend (AWS SAM / Node.js)
Module path:
st-integration/jbw-sam/· CloudFormation stackjbw-prod-api(us-east-1) · template description “jbw-backend”
Purpose
Section titled “Purpose”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:
- Onboarding / tenant management — dealer signup (Cognito), ServiceTitan tenant registration (clientId/clientSecret/tenantId), Workiz account registration, credential + custom tag/field verification.
- 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 thejbw_st_dataSyncstored procedure to match ST locations ↔ JBW agreements. - 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). - Claims — potential-claim creation, bidirectional claim status sync (JBW
JB360ClaimStatusSQL view ↔tblClaims), staging claims for the ST push done by theextractionmodule. - Dashboards, notifications, email — admin/dealer KPI data, pending agreement/claim notification emails via SES.
- Frontend hosting — S3 bucket + CloudFront distribution serving the
jbwuiSPA.
Architecture at a Glance
Section titled “Architecture at a Glance”- 63 Lambda functions (Node.js 20,
Handler: index.handler, 128 MB default, 180 s default timeout) defined intemplate.yaml(~1,750 lines), fronted by one API Gateway (${Environment}_JBWApiGateway). A further 44 source dirs undersrc/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 theJBWSQSURLparameter) is the hand-off to theextractionJava Lambda. Two functions enqueue to it:getTenantInfo(one message per active ServiceTitan tenant) andjbwworkiztenantifno(one per active Workiz account, different message shape —dealerId/accountId/batchId/syncStartDate, noclientId/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.
Authentication (important quirk)
Section titled “Authentication (important quirk)”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 setsauthorizationType=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: '*').
API Route Inventory
Section titled “API Route Inventory”Full method+path → function table is in the code reference. Summary by domain:
| Domain | Routes (examples) |
|---|---|
| Dealers & onboarding | GET /dealer, GET /dealer/{id}/verify, /onboard, /cardsdata, POST /newdealeronboard, GET /dealeronboard, GET /dealer/nononboardedlist, GET /dealer/latestonboardeddealers |
| Registration & users | POST /dealerregister/add, GET /dealerregister, PUT /dealerregister/userstatus, POST /signup, /changepassword, /resetpassword |
| Tenants | GET /tenant/{tenantid}, GET /tenant/dealer/{dealerid}, PATCH /tenant/{tenantid}, GET /dealer/{id}/gettenantinfo (→ SQS) |
| Agreements | GET /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/matching | GET /agreementsync/{id}, GET /agreementsync/{id}/spcall (trigger sync), PUT /agreementsync/approve, PUT /agreementsync/{id}/location |
| Claims | GET /claims/{dealerId}, PATCH /claims/stupdate (JBW → DB), PATCH /claims/reject/{claimid}, PATCH /claims/claimidupdate/{id} |
| Invoices / products / locations | GET /invoices/invoicedetails/{invoiceid}, GET /agreements/dealer/{dealerid}/invoicesearch, GET /tblproducts/search, GET /stlocations/search/{dealerid} |
| Verification | POST /jbwservicetitan (ST credential + tag/field check), GET /workiz/accountverify |
| Dashboards / notifications | GET /admindashboard/cardsdata, GET /emailnotifications, PATCH /emailnotifications/{id} |
Configuration
Section titled “Configuration”Template parameters
Section titled “Template parameters”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)”| Secret | Where |
|---|---|
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 token | hardcoded 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/clientSecret | stored in DB table tblTenant (passed at runtime, not in the template) |
DB access itself uses IAM auth (no stored password).
Build & Deploy
Section titled “Build & Deploy”Build — per-function Zip; Node deps resolved from each function’s package.json:
sam buildDeploy — uses [prod.deploy.parameters] from samconfig.toml (stack jbw-prod-api, confirm_changeset=true):
sam deployPost-deploy checklist (manual, every deploy):
- Invoke
jbwapiauthorizerupdatewith{restApiId, authorizerId, region}to re-attach the Cognito authorizer — until this runs, the API is open. - Spot-check a protected route returns 401 without a JWT.
- If the SPA changed: sync the
jbwuibuild toWebSiteBucketand 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)”SQL injection — the July 2026 picture
Section titled “SQL injection — the July 2026 picture”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:
| Site | Shape |
|---|---|
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:28 | where (dealerId like '%${searchKey}%' or dealerName like '%${searchKey}%') from queryStringParameters.key |
src/jbwtenantbusinessunitsdatapull/index.js:131 | INSERT … ON DUPLICATE KEY UPDATE interpolating ServiceTitan API response fields (bunits.name, officialName) — injection via third-party data |
src/jbwworkiztenantifno/index.js:27 | AND 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-91cannot persist locations as written —const result = '';(:68) is reassigned at:75(TypeError), it uses a callback-styleconnection.queryon amysql2/promiseconnection, andcommit()/returnat:78-79race ahead of the insert callback whilefinally(:89) closes the connection underneath it. It also readsevent.body.accountidwithoutJSON.parse(:9).- Connection leak —
src/jbwtenantbusinessunitsdatapull/index.js:129assignsconnectionwith nolet/const(implicit global) and notry/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), shadoweddata(:56), and a Promise interpolated into an error string (:54, yields[object Object]).
README.mdis SAM boilerplate.
See Also
Section titled “See Also”- Code reference — jbw-sam (per-Lambda)
- System overview