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.
| Type | Shape |
|---|---|
PartnerValueAngle | 'saves_time' | 'saves_money' | 'better_value' — the axes a product competes on. Display copy in PARTNER_VALUE_ANGLES. |
PartnerMarketSector | 11 sector ids (retail, food_beverage, technology, … other — always offered, always last). Display copy in PARTNER_MARKET_SECTORS. |
PartnerProduct | name, 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. |
PartnerAccount | ownerUid, 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: trueauth claim (Auth and Users). - Admins (
adminclaim) 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 fromPARTNER_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-loginorganism withisGoogleHidden(corporate accounts are deliberately email/password only — no Google/SSO — so the account is created with the business email); once signed in, aneffectloads 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 originalsubmittedAt. - Status badge —
pending→ "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 Date ⇄ Timestamp 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.productsis already an array.