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 CRMs (ServiceTitan, secondarily 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.
- 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”- ~65 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). - 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 theJBWSQSURLparameter) is the hand-off to theextractionJava Lambda:getTenantInfoenqueues 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.
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 Workiz handler source (src/jbwagreementsdatamatch/index.js and others) |
| 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”sam build # per-function Zip; Node deps from each function's package.jsonsam deploy # uses [prod.deploy.parameters] from samconfig.toml# post-deploy (manual): invoke jbwapiauthorizerupdate to re-attach the Cognito authorizerLocal 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
jbwdeleteagreementbyidonward (~lines 1511–1654) is indented 4 spaces instead of 2 —sam validate/deploy would fail on it as-is. jbwdealerpendingagreementsclaimsnotificationreferences!Ref jbwdealerpendingagreementsclaimlistbut the source dir is...claimslist(name mismatch), and two functions reference!Ref RDS_REGIONwhich 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.mdis SAM boilerplate; ~30 source dirs are unmanaged by the template.
See Also
Section titled “See Also”- Code reference — jbw-sam (per-Lambda)
- System overview