Skip to main content

Partner Program

See also: Firestore and Security Rules · Data Model · Auth and Users

Corporate accounts for businesses that register their products with Stretched. Partners compete on the axes Stretched itself cares about: their products save people time, save them money, or deliver more value for the money.

The model

libs/stretched-types/src/partner/partner.ts — copy (labels/blurbs) lives with the model, mirroring the tier model in subscription.ts (see Pricing and Subscriptions); pages own only visual identity.

TypeShape
PartnerValueAngle'saves_time' | 'saves_money' | 'better_value' — the axes a product competes on. Display copy in PARTNER_VALUE_ANGLES.
PartnerMarketSector11 sector ids (retail, food_beverage, technology, … other — always offered, always last). Display copy in PARTNER_MARKET_SECTORS.
PartnerProductname, url?, price? (typical USD retail — lets Stretched translate it into hours), description, valueAngles (at least one).
PartnerStatus'pending' | 'approved' | 'rejected' — review lifecycle. Clients can only ever write pending.
PartnerAccountownerUid, businessName, website?, marketSector, contactTitle, contactFirstName, contactLastName, contactEmail, description, products: PartnerProduct[], status, submittedAt, updatedAt.

isBusinessEmail(email) (with PERSONAL_EMAIL_DOMAINS) also lives in partner.ts: a corporate account must be registered with an address at the business the contact represents, so addresses at personal providers (Gmail, Outlook, iCloud, …) are rejected client-side. Every application is still human-reviewed.

Stored at partners/{ownerUid} — one document per account, keyed by the Firebase Auth UID of the person who registered it. See Data Model for the generated ERD.

Security: pending-only client writes

libs/firebase-permissions/firestore.rules:

match /partners/{userId} {
allow read: if request.auth != null
&& (request.auth.uid == userId || request.auth.token.admin == true);
allow create, update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.status == 'pending';
allow delete: if false;
}
  • Owners read/write their own document; every client write must carry status == 'pending' — a partner cannot approve themselves.
  • Approval/rejection happens out-of-band via the Admin SDK (which bypasses rules), exactly like the admin: true auth claim (Auth and Users).
  • Admins (admin claim) may read all applications for review.
  • More detail in Firestore and Security Rules.

The /partners page

apps/stretched/src/app/pages/partners/partners.component.ts (route partners in apps/stretched/src/app/app.routes.ts).

  • Pitch pillars — Price / Quality / Value marketing copy (PILLARS).
  • Signup form — signal-forms fields for the business (name, website, market sector select from PARTNER_MARKET_SECTORS, description) plus the contact representing it (job title, first/last name, business email) and a first product (name, url, price, description, value-angle checkbox group built from PARTNER_VALUE_ANGLES). Requires ≥20-char descriptions, a business email (isBusinessEmail — personal providers rejected), a sector, and at least one value angle (isFormValid).
  • Sign-in gate — uses the shared sc-user-login organism with isGoogleHidden (corporate accounts are deliberately email/password only — no Google/SSO — so the account is created with the business email); once signed in, an effect loads any existing application for the caller's uid and prefills the form (falling back to auth profile name/email for fresh signups; a personal auth email is never prefilled).
  • Resubmission — saving an existing application overwrites it with status: 'pending' again ("we'll take another look"), preserving the original submittedAt.
  • Status badgepending → "Under review", approved → "Approved partner", rejected → "Not approved".

Persistence is PartnerService (apps/stretched/src/app/core/services/partner.service.ts): a thin Firestore upsert/load on partners/{ownerUid} that converts DateTimestamp at the boundary (the wire caveat from Data Model). Empty optional fields are omitted, not written as undefined — Firestore rejects undefined values. The same boundary migrates documents written before the corporate-contact expansion (2026-07-27): a legacy contactName splits into first/last, missing marketSector reads as other.

The /admin/partners review page

apps/stretched/src/app/pages/admin/partner-review/ — status-filtered queue of applications (contact name/title, sector, products); Approve / Reject / Reopen write only status + updatedAt, exactly what the rules allow the admin claim.

Open items

  • Partner subdomain Hosting — a dedicated Hosting target for partners is planned but not configured.
  • Only one product is captured in the form today, though PartnerAccount.products is already an array.