///Design

Design spec

The locked decisions: what v1 delivers, the architecture, auth, data model, security posture, and rollout. Approved July 16, 2026.

Streamlink v2 — Design Spec

Date: 2026-07-16 Status: Approved (verbal, session of 2026-07-16) Companion doc: docs/design-input-brief.md — full Salesforce data inventory with exact API names, row counts, field allowlists/denylists, and security findings. That brief is the authority on which fields; this spec is the authority on what we build.

1. Why

Streamlink today is two Salesforce Experience Cloud sites (StreamlinkCustomer, StreamlinkVendor) that customers barely use: 117 customer logins from 19 distinct users in 6 months, 40% of login attempts failing, and the Customer Community Plus license pool at 180/180 — new portal users cannot be added at all. Meanwhile the marketing site sells "every site, every service order — one pane of glass," which the current product does not deliver.

Streamlink v2 is a customer portal that lives on AWS (like the checkout site at ~/aws-website), holds a synced read model of Salesforce data, and gives customers a real dashboard without Salesforce licenses, Salesforce UX, or Salesforce page loads.

2. Decisions (locked)

Decision Choice
Platform strategy Shared AWS↔SF backbone for both customer and vendor surfaces; customer surface first. Vendor phase later, separate project.
V1 interactivity Read + one write path: service requests → SF Case. No payments, no ordering.
Auth Magic link (email-based, passwordless). No SF community licenses.
Data flow Synced read model: poll worker copies allowlisted fields into portal Postgres; UI reads Postgres only.
Visibility Account-wide: any verified contact on an account sees all of that account's orders/billing/tickets.
Brand scope LDR Site Services only in v1; schema carries Company__c for later multi-brand theming.
Product scope All Job_Type__c values (dumpster, porta potty, fencing, generator, fuel, storage, forklift).
Domain streamlink.ldrsiteservices.com

3. Architecture

SALESFORCE (prod)                        AWS
┌──────────────┐   poll Δ (SystemModstamp) ┌──────────────────┐
│ Account       │   every 2 min             │ Sync worker       │
│ Contact       │──(allowlisted fields ───▶│ (Lambda, EventBridge│
│ Customer_Orders__c │  only)               │  Scheduler)       │
│ Order_Line_Item__c │                      └────────┬─────────┘
│ Case (slice)  │                                    ▼
│ Transaction__c│                          ┌──────────────────┐
│ Netsuite_Transactions__c │               │ Portal Postgres   │
│ Vendor_Delivery_Notification__c │        │ (RDS, Prisma)     │
└──────▲───────┘                           └────────┬─────────┘
       │                                            │
       │  staged idempotent write:         ┌────────┴─────────┐
       └── service request → Case ─────────│ Next.js portal    │
           (Origin='Customer Portal')      │ (Amplify SSR)     │
                                           └──────────────────┘

Four components, one repo (streamlink):

  1. Portal web app — Next.js 14 App Router, TypeScript, Tailwind, Prisma. Deployed on AWS Amplify Hosting (same ops as checkout). Secrets at runtime from AWS Secrets Manager via a getSecret() abstraction — never inlined into the build bundle (deliberate break from checkout's next.config.js env{} pattern, which is a flagged weakness).
  2. Portal database — dedicated RDS Postgres, Prisma-managed schema. Contains only allowlisted fields. Vendor rates, margins, costs, card numbers, tokens, CVVs physically never enter this database.
  3. Sync worker — same repo, bundled as a Lambda, run by EventBridge Scheduler every 2 minutes. Incremental sync keyed on SystemModstamp per object (nine objects: the eight in the diagram plus Credit_Card__c display fields for cards-on-file). Volume: ~1–2k changed rows/day. A separate one-time backfill job (paged SOQL, ORDER BY SystemModstamp with cursor) seeds history: ~250k orders, ~1M OLIs, plus billing/case slices.
  4. Write-back path — service requests create Case records (RecordType Customer_Support = 0123o000001cpHZAAY, Origin='Customer Portal' — existing picklist value). Uses the staged/resumable/idempotent write pattern from checkout's completedOrderOrchestrator. Requests are persisted locally first (service_requests table with sf_push_stage), so a SF outage never loses a customer request; a retry job drains the queue.

Salesforce access: a new, dedicated Connected App + integration user (OAuth 2.0 Client Credentials, same recipe as aws-website/lib/salesforce/clientCredentialsAuth.ts). Its profile/permission set grants read on exactly the synced objects and zero FLS on: Credit_Card__c.Security_Code__c, Credit_Card__c.Credit_Card_Number__c, all *_Token__c, Authorize_net_Profile_Id__c, Transaction__c.Payscape_Payload__c, Transaction__c.Expiration_Date__c, Transaction__c.Authorization_Code__c. Checkout's integration user is untouched — the two apps cannot break each other.

4. Data model (portal Postgres)

Synced mirrors (SF Id as natural key, sf_system_modstamp for incremental sync, soft-delete flag):

Portal-native tables:

The allowlist is code: one module (lib/sync/fieldAllowlist.ts) declares, per object, exactly the SF fields that may be selected and stored. The SOQL SELECT clauses are generated from it. A unit test asserts the allowlist has zero intersection with a maintained denylist (every Vendor_*, margin, cost, internal-note, token, and card-sensitive field catalogued in the design-input brief). Leaking a vendor field to the portal would require editing both lists in one reviewed diff.

Status mapping is code: lib/mapping/status.ts maps raw SF picklists → customer-safe statuses (orders: Open/Closed; units: Scheduled → Delivered → Removal requested → Completed → Cancelled; cases: Open/Closed). Raw values like "Vendor Queue", "Ready for Procurement", "Accounting" never render.

5. Auth flow

  1. Customer enters email at streamlink.ldrsiteservices.com/login.
  2. If email matches ≥1 synced contacts row on an eligible account: send magic link via SES. (Unknown email: same "check your inbox" response — no account enumeration.)
  3. Link click: verify single-use token (hashed at rest, 15-min expiry), create session (httpOnly secure cookie, 30-day rolling).
  4. If the email maps to multiple accounts: account picker; selection remembered, switchable in the header.
  5. Rate limits: per-email and per-IP on link requests.

Eligible population on day one: ~23,191 contacts with emails on accounts that ordered in the last 12 months. No provisioning step, no licenses, no passwords.

6. Features (v1)

Out of scope v1: payments, invoice PDFs, live GPS/en-route tracking (no GPS data exists in SF; Vendor_Delivery_Notification__c response rate is only ~44%, so delivery timeline shows confirmed events honestly rather than promising live tracking), vendor surface, new-service ordering (checkout site's job), multi-brand theming.

7. Security

Escalations outside this project (found during research, need owner action regardless):

  1. 🔴 PCI-critical: Credit_Card__c.Security_Code__c holds plaintext CVVs — 77,945 rows, 11,347 written in the last 12 months, still being written. Storing CVV post-auth is prohibited under PCI DSS in any form. Stop the writer, purge the column, audit.
  2. External_User__c.PasswordTxt__c holds plaintext passwords (477 rows, dead since 2021). Purge the object.
  3. Legacy formulas Customer_Orders__c.Customer_Acess__c/HasAccess__c use spoofable CSV substring matching; do not reuse, consider retiring.
  4. Checkout inlines SF_CLIENT_SECRET/DATABASE_URL/static IAM keys into its build bundle — rotate + migrate when convenient.

8. Ops & failure handling

9. Testing

10. Rollout

  1. Deploy to staging against SF UAT org (target_org='uat' alias exists) with a synthetic account.
  2. Backfill prod data, internal dogfood (LDR staff emails exist as contacts).
  3. Pilot: the ~19 active Experience Cloud customer users + 1–2 national accounts, personally invited.
  4. Experience Cloud customer site stays live until parity confidence, then redirect ldrsiteservices.my.site.com/StreamlinkCustomer → new portal. ~24 named customer licenses freed.
  5. Vendor phase: separate spec, same backbone. Holds 156 of 180 licenses and deep ops coupling (vendor pool dispatch, payment manager) — explicitly not a copy of the customer build.

Coordination prerequisites (human to-dos):