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 CRMs (ServiceTitan, secondarily 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.
  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.
  • ~65 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).
  • Roughly 30 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: getTenantInfo enqueues one message per active tenant.
  • 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 Workiz handler source (src/jbwagreementsdatamatch/index.js and others)
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).

Terminal window
sam build # per-function Zip; Node deps from each function's package.json
sam deploy # uses [prod.deploy.parameters] from samconfig.toml
# post-deploy (manual): invoke jbwapiauthorizerupdate to re-attach the Cognito authorizer

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)”
  • Template indentation bug: the block from jbwdeleteagreementbyid onward (~lines 1511–1654) is indented 4 spaces instead of 2 — sam validate/deploy would fail on it as-is.
  • jbwdealerpendingagreementsclaimsnotification references !Ref jbwdealerpendingagreementsclaimlist but the source dir is ...claimslist (name mismatch), and two functions reference !Ref RDS_REGION which is not a declared parameter.
  • PATCH /agreements/{id}/equipments (jbwdeleteagreementbyid) collides with the add/get routes on the same path.
  • SQL in most handlers is string-interpolated (injection-prone).
  • API deployed authorizer-less by default (see Authentication); CORS *.
  • README.md is SAM boilerplate; ~30 source dirs are unmanaged by the template.