///Research

Design input brief

What we found in the live Salesforce org: the data inventory behind every portal feature, sync inputs, what to reuse from the checkout site, what the current Streamlink actually contains, and the security flags. Field names are exact API names from production.

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:

OLI fields to expose:

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.

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:

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.EmailContact.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:

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)

  1. 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.
  2. Multi-tenant row-level authorization — every query scoped by AccountId (+ Company__c brand); no pattern exists. Do NOT reuse the legacy SF formulas Customer_Acess__c/HasAccess__c (CSV substring-CONTAINS on User.AccountId__c — spoofable via 15/18-char ID prefixes).
  3. SF read layer — SOQL read models for orders/OLIs/cases/transactions with explicit field allowlists; real-mode reads currently throw.
  4. Inbound sync (poll worker or CDC ingestion) + a real scheduler (EventBridge → Lambda/ECS task).
  5. Notification/email infra — none exists.
  6. Status-mapping layer — internal picklists → customer-safe statuses (orders, OLIs, cases).
  7. Connection pooling — both DBs are connection-per-query; portal dashboard fan-out needs RDS Proxy or a pool.
  8. Runtime secrets — do NOT copy the next.config.js env{} + ENV_MAP inlining pattern (secrets baked into the build bundle; rotation requires rebuild). Use Secrets Manager/runtime env (the getSecret() 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:


5. Open design questions

  1. 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.
  2. Email collisions: one email can map to multiple Contacts/Accounts. Disambiguation UX: account picker at login, admin-mediated invite, or verified-domain claiming?
  3. Multi-brand: 9 Company__c brands. One portal with brand theming (aws-website already has lib/branding.ts + NEXT_PUBLIC_BASE_PATH sub-path precedent), or LDR-only v1?
  4. 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.
  5. 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 to Transaction__c — the aws-website write layer supports it, but it expands PCI scope (see §6).
  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.
  7. Freshness SLA: is 1–5-min polling acceptable, or does anything (delivery confirmations?) justify CDC/Platform Events on day one?
  8. 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?
  9. Which balance field is authoritative: NewBalance__c vs Customer_Orders__c.Balance__c (labeled OLD but referenced by live Transaction__c.Order_Balance__c formula) — needs a ruling from the org owner.
  10. Job_Type__c scope: orders include fencing, generators, fuel, storage, forklifts. Support all in "my orders" or scope v1 to Dumpster + Porta Potty?
  11. Change freeze / coordination with the current SF dev team (Shetty/Koo) during the build.
  12. 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)

  1. PCI CRITICAL — plaintext CVV storage: Credit_Card__c.Security_Code__c is 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.
  2. Full PANs in-org: Credit_Card__c.Credit_Card_Number__c is 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 on Security_Code__c, Credit_Card_Number__c, all *_Token__c, Authorize_net_Profile_Id__c, Transaction__c.Payscape_Payload__c, Transaction__c.Expiration_Date__c.
  3. Spoofable legacy access formulas: Customer_Orders__c.Customer_Acess__c / HasAccess__c use CONTAINS($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.
  4. 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 via next.config.js env{}. Migrate to Secrets Manager/execution roles for the portal; consider rotating checkout's secrets too.
  5. 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 like Vendor_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.
  6. SOQL injection surface: real.ts builds SOQL by string interpolation guarded only by an ID regex; a portal with user-driven filters needs parameterized/escaped SOQL from day one.
  7. 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 (no LightningComponentBundle queries) — use newer Metadata/Tooling API versions for inventory work.