Streamlink v2 — Design Input Brief
Synthesis of four research briefs (SF orders, SF billing, SF support/accounts, Streamlink internals, aws-website codebase). All Salesforce data from prod, queried 2026-07-16. Direction: shared AWS<->SF platform, customer surface first, vendor later on the same backbone.
1. Data inventory
1.1 Orders view ("My sites / my orders")
Objects: Customer_Orders__c (250,474 rows; 32,011 last 365d; 184 fields) ← parent of Order_Line_Item__c (1,025,085 rows; 146,539 last 365d; 468 fields; ~4 OLIs/order) via Order_Line_Item__c.Order__c.
Customer scoping: Customer_Orders__c.Account__c → Account is THE customer tie. OLI has NO real Account lookup — Order_Line_Item__c.Account__c is a formula string; always scope OLIs through Order__r.Account__c. Contact-level fields (Customer_Authorized_by__c on order, Customer_Contact__c/Authorized_By__c on OLI) are inconsistently populated — do not rely on them for access control.
Order-header fields to expose:
- Identity:
Name(order number),Company__c(9-brand picklist — must filter/brand) - Site:
Shipping_Street__c/City__c/State__c/Zip__c,Site_Address__c,Geo_code__c,Store_Name__c(~58 retail chains),Store_Number__c,Location_Name_Other__c,Site_Address_Order_Level__c,Site_Contact__c,Site_Contact_Telephone__c,Setting_Instructions__c - Status:
Customer_Order_Status__c(Open|Closed|Audit — best customer signal),Order_Complete__c,Has_Open_OLI__c; map/hideStatus__c(contains "Ready for Procurement", "Accounting") andAccounting_Status__c - Dates:
Delivery_Date__c,First_OLI_Delivery_Date__c,Latest_OLI_End_Date__c,Order_Completed_Date__c - Money (correct fields only):
LIne_Total_Amount__c,Tax_Amount_Oli__c,Total_Amount_After_Tax__c,Charged_Amount__c,Credited_Amount__c,NewBalance__c,Customer_PO__c,Customer_Ref__c,Notes_to_Customer__c. NEVER use the deprecated twins:Amount_After_Tax__c,Tax_Amount__c,Balance__c(note:Transaction__c.Order_Balance__cformula references it — reconcile which balance field is authoritative),Tax_Amount_NEW__c,TotalAmountAfterTaxesOli__c.
OLI fields to expose:
- Product:
RecordType(critical: "Additional Tonnage"/"Extended Rental"/"Other Charges" are charge adjustments, not new units — group under the placement OLI or site views show phantom dumpsters),Job_Type__c(Dumpster|Porta Potty|Fork Lift|Fencing|Fuel|Generator|Storage Container),Product__c,Size__c,Quantity__c,Material__c,Service__c+Service_Day__c..Service_Day_4__c - Lifecycle (customer-facing status = composite of):
KAM_Deliveries_Removals__c(Unconfirmed Delivery → Delivery Executed → Removal Requested → Removal Complete/Job Complete),Delivery_Confirmed__c/_Date__c,Removal_Confirmed__c/_Date__c,Follow_Up__c,If_cancelled_why__c. Hide rawOrder_Line_Item_Status__c("Vendor Queue", "Procurement" leak internals). - Dates:
Delivery_Date__c,Expected_Delivery_Time__c,End_Date__c,Projected_End_Date__c,Swap_Date__c,Rental_Days__c - Pricing:
Price__c,Delivery_Fee__c,Tons_Included__c,Price_Per_Ton__c,Price_Per_Additional_Days__c,Additional_Days__c/Additional_Ton__c/Additional_Fees__c,Swap_Charge__c,Line_Price__c,LIne_Total_Amount__c,Line_Tax_Amount__c,Line_Total_Amount_After_Tax__c,Amount_Charged__c,Open_Balance__c,Payment_Received__c,Line_Notes_to_Customer__c
NEVER expose (order + OLI): all Vendor_* fields (Vendor__c, Vendor_Contact__c/_OLI__c, Vendor_Email__c, Vendor_Phone__c, Vendor_Rate__c, Vendor_Delivery_Fee__c, Vendor_Line_Cost__c, Vendor_Additional_*, Vendor_Swap_Cost__c, Vendor_Invoice_Number__c, VendorsCSV__c, Line_Note_to_Vendor__c), all margin fields (Total_Proj_Marg_Sum__c, CO_Actual_Margin__c, Projected_Margin__c, Proj_Marg_Percentage__c, Total_Projected_Margin__c, Margin_LP_LC__c), Actual_Cost__c, Check_Request_Total__c, CO_Check_Request_Total__c, Procurement_Notes__c, Provider_Notes__c, Claimed_By__c, Related_Disposal_Facility__c, Internal_Notes__c, Internal_Order_Information__c, Internal_Account_Information_2__c, all Netsuite_* vendor/PO fields. Roughly a third of OLI's 468 fields are vendor/margin data interleaved with customer data — explicit field allowlist in the API layer is mandatory; FLS alone is insufficient.
Multi-site reality: top accounts are Target Corp (1,937 orders/yr), SPM Restoration (1,554), Republic Services (1,412), Rubicon (962). "My sites" grouping key = Store_Name__c + Store_Number__c / site address, NOT Account.ParentId (unused: 15/172,877).
1.2 Invoices & payments view
Build on: Netsuite_Transactions__c (93,772 rows; 48,101 last 12mo; syncing today) + Transaction__c (562,468 rows; 57,948 last 12mo). Do NOT build on Invoice__c/Invoice_Order_Line_Items__c — dead since 2024-10-21 (0 rows in 12mo, recent rows abandoned at Payment_Status__c='New' with misleading balances). NetSuite is the current ERP; QuickBooks sync is dormant (QB_Status__c frozen at 'Invoice Pending').
Payment history (Transaction__c): Name (T-######), Type__c (Payments|Credit|Decline|Review|Void — filter Decline/Void/Voided__c out of totals), Amount__c, Pre_tax_Amount__c, Tax__c, Credit_Amount__c, CreatedDate, Credit_Card_Last_4__c, Credit_Card_Type__c, Item_Description__c, Payment_Reference__c. Links: Order__c → Customer_Orders__c, Order_Line_Item__c. No direct Account lookup — scope via Order__r.Account__c. Hide: Payscape_Payload__c (raw gateway data), Expiration_Date__c, Authorization_Code__c, all Netsuite_*/QB_* plumbing.
Open invoices (Netsuite_Transactions__c): query shape WHERE Type__c='Invoice' AND Order__r.Account__c = :acctId (Status__c='Open' for balance due). Fields: Name, Amount_Total__c, Amount_Paid__c, Amount_Due__c, Tax_Amount__c, Transaction_Date__c, Transaction_Due_date__c, Status__c, Type__c, Payment_Method__c, Memo__c. Caveats: Account__c frequently null — always resolve via Order__r.Account__c; Type__c/Status__c are free strings not picklists; 12-mo mix is Payment 44,830 / Credit Memo 2,723 / Invoice 548 — so the view is payment-history-first with a small net-terms invoice section, not an invoice-centric AR portal. Live per-order balance also on Customer_Orders__c.Balance__c / NewBalance__c (see deprecated-field note above).
Saved cards (Credit_Card__c, 93,727 rows): expose ONLY Name, Card_Number_Last_4__c, Credit_Card_Type__c, Expiration_Date__c (display), Primary__c, filtered by Account__c + Company__c. The portal service must never read this object broadly — see Security flags §6.
No PDF field exists on any billing object. Invoice PDFs would come from NetSuite via NetSuite_Id__c/Transaction_Id__c or SF Files — unverified; do not promise downloadable invoices yet.
1.3 Tickets / support view
Build on Case only (455,673 created last 12mo; 248 fields). Service_Ticket__c is a trap — 4 rows/yr, legacy WorkWave dispatch.
- Record type:
Customer_Support=0123o000001cpHZAAY(151,497/yr — but ~130k are automation noise) - Mandatory filters: Origin allowlist (
Email,Phone,Web,Customer Portal— 'Customer Portal' and 'Vendor Portal' Origin values already exist) or Type allowlist; otherwise customers see Check In (49,842), Recharge (17,937), Vendor VC Reminder (14,540) machine cases. Genuinely customer-initiated volume: a few thousand/yr. - Customer-facing Types: Cancellation, Delivery ETA, Delivery Request, Removal Request, Swap Dumpster, Invoice Issue, Customer Inquiry, Potty Service, Final Removal, Refund
- Status: map 25+ raw values to Open/Closed (actual usage is effectively New → Closed); hide vendor-response stages
- Fields:
Subject,Description,Type,Status(mapped),Priority,Resolution__c,Update_s__c,ClosedDate,Dispute_Reason__c/Dispute_Details__c; order joinsOrder_Line_Item__c(72% populated on Customer_Support),Customer_Order__c, plus formula mirrorsOrder_Number__c,Site_Address__c,Delivery_Date__c - Scope by
AccountId(80% populated), notContactId(13%) — accept that any contact on an account sees all account cases, or history disappears - Hide: all
Vendor_*cost formula mirrors on Case, AP/finance record types - Prior art:
Created_by_External_user_Contact_ID__c+ 64 'Customer Portal'-origin cases — an external case-creation flow already exists
1.4 Delivery tracking
There is no live GPS in Salesforce. VehicleStop__c (153 rows, dead since 2017-11-22) and VehicleDailySummary__c (15 rows) are dead. Best available signal:
Vendor_Delivery_Notification__c(54,616 rows; 32,531 last 12mo — alive): binary vendor delivery confirmation. Fields:Order__c,Name__c(→ OLI),Is_Responded__c,Responded_on__c,Response__c(yes 17,439 / null 14,299 / no 793 — only ~44% response rate). HideVendor__c,Notification_Sent_to__c.- Plus OLI
KAM_Deliveries_Removals__c,Delivery_Confirmed__c/_Date__c,Expected_Delivery_Time__c. True "en route" tracking requires a WorkWave or vendor API integration outside SF — out of scope for v1 or an explicit add-on decision.
1.5 Identity / accounts
172,877 Accounts: portal scope = Business_Account (127,118) + PersonAccount (7,920, Person Accounts enabled); exclude Vendor (27,918) and Disposal_Facility (9,906) record types. Login mapping: email → Contact.Email → Contact.AccountId. Addressable population: 23,191 emailed Contacts on accounts with orders in last 12 months. Email is NOT unique across 159k emailed contacts — plan a disambiguation/account-selection step and handle IsPersonAccount=true contacts (system-managed).
2. Sync architecture inputs
Volumes/change rates (last 365d): 32k orders, 146k OLIs, 58k Transaction__c, 48k Netsuite_Transactions__c, 455k Cases (portal-relevant slice: low thousands), 32k Vendor_Delivery_Notification__c. Total portal-relevant write volume ≈ 300–700k changed rows/yr ≈ 1–2k/day — trivially small for either CDC or polling. Netsuite_Transactions__c syncs in batches (rows created today; Netsuite_Sync_Date__c null on fresh Transaction__c rows) — reconciliation must tolerate a lag window.
What aws-website already proves:
- Outbound web→SF writes are solid and battle-tested: OAuth 2.0 Client Credentials (
lib/salesforce/clientCredentialsAuth.ts), jsforce v61.0, sObject Tree + Composite Graph writes to the exact same objects (Customer_Orders__c,Order_Line_Item__c,Credit_Card__c,Transaction__c), DB-persisted staged/resumable push with idempotency (completedOrderOrchestrator.ts,OrderAttempt.sfPushStage). - Inbound SF→web is half-fiction: the four real-mode reads throw
'not implemented in real mode'(lib/salesforce/real.ts:507);jobs/sync-salesforce.tseffectively runs mock-only; production reference data flows through the external RDS pricing DB refreshed by an operator-runsf apex runscript. The read layer is greenfield. - No webhooks/Platform Events/CDC anywhere in the repo; no production scheduler (EventBridge is aspirational in comments).
CDC vs polling: CDC (Change Data Capture on Customer_Orders__c, Order_Line_Item__c, Case, Transaction__c, Netsuite_Transactions__c) is plausible at these volumes and gives near-real-time status; the org is actively maintained so enabling CDC channels is feasible. But polling is also entirely sufficient: SystemModstamp-based incremental SOQL every 1–5 min against ~1–2k changes/day is well within API limits. Recommended v1: poll into an AWS read model (Postgres, Prisma) keyed by AccountId, upgrade to CDC/Platform Events → EventBridge if freshness demands it. Do not serve customer dashboards by live SOQL fan-out: top accounts have ~2,000 orders/yr and 1M+ OLIs exist — a local read model with indexes on (Account__c, CreatedDate) plus pagination is the scalable shape, and it also enables the field-allowlist enforcement at ingest time (vendor fields never even land in the portal DB).
SOQL selectivity: queries must filter on indexed Account__c + date; the sync-salesforce.ts transactional-upsert + SyncRun audit pattern (lib/sync/runSync.ts) is the right skeleton but is explicitly not concurrency-safe — fix before scheduling.
3. Reuse map
Lift nearly verbatim from /Users/ldr/aws-website
| Asset | Path | Note |
|---|---|---|
| SF auth (Client Credentials) + Connected App setup recipe | lib/salesforce/clientCredentialsAuth.ts |
Header comment documents full SF-side setup; provision a separate Connected App + integration user for Streamlink so portal changes can't break checkout |
SF client factory, mock/real SF_MODE pattern |
lib/salesforce/client.ts, mock.ts, real.ts |
Connection helpers, SaveResult handling, assertValidSfId |
| Staged/resumable push + idempotency pattern | lib/salesforce/completedOrderOrchestrator.ts, OrderAttempt schema |
Template for any portal-initiated writes (case creation, removal requests) |
| Secrets abstraction | lib/secrets/getSecret.ts |
Keep the abstraction, swap the backend (see below) |
| Prisma singleton + Amplify binaryTargets | lib/db/index.ts, prisma/schema.prisma (rhel-openssl-3.0.x, explicit datasourceUrl) |
|
| Buildspec | amplify.yml |
Decide fatal-vs-non-fatal migrations consciously; bring observability |
| Security headers middleware | middleware.ts |
Rewrite CSP allowlists (checkout-specific: Authorize.net/Maps/reCAPTCHA); frame-ancestors 'none' blocks embedding |
| Logging / typed errors | lib/logger, lib/errors |
|
| RDS pricing client (if needed) | lib/aws/rdsClient.ts, lib/aws/rdsPricing.ts |
Only if the portal shows pricing |
| Test conventions | vitest.config.ts, tests/unit/ |
Hermetic SF_MODE: 'mock' |
| Sync skeleton | lib/sync/runSync.ts, jobs/sync-salesforce.ts |
Pattern only — real-mode reads must be written |
Net-new (nothing to copy)
- Customer auth/sessions/account model — no User table, no NextAuth/Cognito; only artifact is a single-operator HMAC cookie for
/admin(lib/admin/session.ts). Need full stack: signup/login, email verification, web-identity ↔ SF Contact/Account linking, email-collision disambiguation, Person Account handling. - Multi-tenant row-level authorization — every query scoped by AccountId (+
Company__cbrand); no pattern exists. Do NOT reuse the legacy SF formulasCustomer_Acess__c/HasAccess__c(CSV substring-CONTAINS onUser.AccountId__c— spoofable via 15/18-char ID prefixes). - SF read layer — SOQL read models for orders/OLIs/cases/transactions with explicit field allowlists; real-mode reads currently throw.
- Inbound sync (poll worker or CDC ingestion) + a real scheduler (EventBridge → Lambda/ECS task).
- Notification/email infra — none exists.
- Status-mapping layer — internal picklists → customer-safe statuses (orders, OLIs, cases).
- Connection pooling — both DBs are connection-per-query; portal dashboard fan-out needs RDS Proxy or a pool.
- Runtime secrets — do NOT copy the
next.config.js env{}+ENV_MAPinlining pattern (secrets baked into the build bundle; rotation requires rebuild). Use Secrets Manager/runtime env (thegetSecret()abstraction was designed for this swap) or different compute (ECS/App Runner).
4. Decommission notes
What exists: two live Experience Cloud networks — Streamlink - Vendor (0DBRg00000002jNOAQ, /StreamlinkVendor) and Streamlink - Customer (0DBRg00000002kzOAA, /StreamlinkCustomer) on ldrsiteservices.my.site.com. Usage last 6 months: Vendor 148 logins / 40 users; Customer 117 logins / 19 distinct users (~20/month). No Apex named "Streamlink" — backends are CustomerCommunityController / VendorCommunityController plus a large vendor-workflow layer (VendorPaymentManagerController 2026-07-01, VendorSelectorController, SendToVendorPoolController 52.6k chars, PublicVendorConfirmationController, VendorEmailService, vendor zone/ZIP classes). Aura bundles include order creation (newOrderCmp, NewOrderLineItemCO), payment entry (NewCreditCard, BillCCCmp), VendorOrders, VendorInventory, Pricing (touched 2025-12).
Licenses/cost: Customer Community Plus 180/180 = 100% consumed (156 Vendor Community Plus User + 24 Customer Community Plus User) — new users literally cannot be added today, likely a driver of this project. 3,500 Customer Community Plus Login licenses sit almost unused (45 login-profile users). Two implications: (a) replacing only the customer surface frees ~24 of 180 seats — the vendor portal is the bigger license consumer, so full savings require the vendor phase; (b) a cheaper interim fix is converting named licenses to login-based — worth pricing against the rebuild.
Migration considerations:
- Customer surface first affects only ~19 active users / 24 named licenses — small, personally contactable migration cohort.
- Active development is ongoing (Vineeth Shetty, Sylvia Koo; VendorPaymentManagerController modified 2026-07-01). Freeze portal-facing SF changes or the rebuild chases a moving target; also ask what created the 64 'Customer Portal'-origin cases and
Created_by_External_user_Contact_ID__c. - Vendor functionality is deeply coupled to internal ops (vendor pool dispatch, confirmation emails, delivery notifications, payment management shared between portal and internal users) — the vendor phase is not a thin CRUD rebuild.
- Do not break the public guest sites that live outside the two networks:
vendorconfirmation,vendordelivery, CSOPS credit application, PortaPotty, CTMILOPage, Marketing Landing Pages (/lp), LDR Payment Request. - Legacy networks (Vendor, Customer, Vendor Portal, Texas Dumpsters) are already retired/UnderConstruction; FLS Partner is effectively dead (4 logins/6mo).
- ExperienceBundle/CMS metadata (exact pages/objects exposed) was not enumerable via SOQL at API v42.0 — retrieve via Metadata API before finalizing the feature-parity checklist; the
/sLWR sub-sites may contain unenumerated LWCs.
5. Open design questions
- Account-wide visibility: any contact on an account sees all orders/cases/payments for that account (ContactId only 13% populated on cases; OLI contact fields unreliable). Acceptable for Target-sized accounts, or do we need per-contact/per-site scoping (e.g., by
Store_Number__c)? This decides the whole authorization model. - Email collisions: one email can map to multiple Contacts/Accounts. Disambiguation UX: account picker at login, admin-mediated invite, or verified-domain claiming?
- Multi-brand: 9
Company__cbrands. One portal with brand theming (aws-website already haslib/branding.ts+NEXT_PUBLIC_BASE_PATHsub-path precedent), or LDR-only v1? - Invoice PDFs: no PDF field exists anywhere. Fetch/regenerate from NetSuite via
NetSuite_Id__c, check ContentDocumentLink, or drop PDFs from v1? Needs verification before it's promised. - Payments in-portal: view-only billing vs pay-open-balance? Paying requires gateway integration (Authorize.net tokens exist per-brand on
Credit_Card__c) and writes toTransaction__c— the aws-website write layer supports it, but it expands PCI scope (see §6). - Delivery tracking honesty: ship "vendor confirmed delivery" (44% response rate) + OLI status as v1, or invest in a WorkWave/vendor API integration for real tracking? Set customer expectations accordingly.
- Freshness SLA: is 1–5-min polling acceptable, or does anything (delivery confirmations?) justify CDC/Platform Events on day one?
- License strategy: rebuild vs converting named → login-based licenses (3,500 sitting unused) as an interim; and when does the vendor phase start, since it holds 156 of the 180 consumed seats?
- Which balance field is authoritative:
NewBalance__cvsCustomer_Orders__c.Balance__c(labeled OLD but referenced by liveTransaction__c.Order_Balance__cformula) — needs a ruling from the org owner. - Job_Type__c scope: orders include fencing, generators, fuel, storage, forklifts. Support all in "my orders" or scope v1 to Dumpster + Porta Potty?
- Change freeze / coordination with the current SF dev team (Shetty/Koo) during the build.
- Compute choice: Amplify SSR (inherits the secrets-inlining footgun) vs ECS/App Runner with runtime secrets — decide before scaffolding.
6. Security flags (remediate regardless of this project)
- PCI CRITICAL — plaintext CVV storage:
Credit_Card__c.Security_Code__cis a plain string holding CVVs on 77,945 rows, 11,347 written in the last 12 months (still being written today). Storing CVV post-authorization is prohibited under PCI DSS in any form. Escalate to the org owner for immediate remediation: stop writing, purge the column, audit what writes it. - Full PANs in-org:
Credit_Card__c.Credit_Card_Number__cis Classic-encrypted (decryptable by any user with View Encrypted Data; weaker than Shield) and appears to hold full card numbers, not just tokens — alongside per-brand gateway tokens (Payment_Token__c,Authorize_net_Profile_Id__c,LDR_Site_Services_Token__c, etc.). The Streamlink integration user must have zero FLS onSecurity_Code__c,Credit_Card_Number__c, all*_Token__c,Authorize_net_Profile_Id__c,Transaction__c.Payscape_Payload__c,Transaction__c.Expiration_Date__c. - Spoofable legacy access formulas:
Customer_Orders__c.Customer_Acess__c/HasAccess__cuseCONTAINS($User.AccountId__c, Account__c)— substring matching over a CSV is vulnerable to 15/18-char ID prefix confusion. Do not reuse; if the legacy portals rely on these today, that is a live weakness worth flagging. - Secrets baked into build artifacts: aws-website inlines
SF_CLIENT_SECRET,DATABASE_URL, and long-lived IAM user keys (LDR_ACCESS_KEY_ID/LDR_SECRET_ACCESS_KEY— static credentials, not roles) into the compiled server bundle vianext.config.js env{}. Migrate to Secrets Manager/execution roles for the portal; consider rotating checkout's secrets too. - Vendor-margin leakage risk is structural: vendor cost/margin fields are interleaved on the same records customers will query (
Order_Line_Item__c~1/3 of 468 fields; Case formula mirrors likeVendor_Rate__c,Vendor_Line_Cost__c). Enforce an explicit field allowlist at the API/ingest layer — never describe-driven or SELECT-broad queries — in addition to integration-user FLS. - SOQL injection surface:
real.tsbuilds SOQL by string interpolation guarded only by an ID regex; a portal with user-driven filters needs parameterized/escaped SOQL from day one. - Tooling note for automation: the salesforce MCP read-only validator rejects any SOQL containing the substring "Update" (blocks e.g.
QB_Update_Log__c); the org's CLI queries run at API v42.0 (noLightningComponentBundlequeries) — use newer Metadata/Tooling API versions for inventory work.