learning_ai_common_plat/docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md

40 KiB
Raw Blame History

Shared @bytelyst/* Client Packages — Extraction from Product Repos

Status: Packages Complete · NomGap Web Migration In Progress Priority: High — eliminates duplication across 9+ products Estimated effort: 810 sessions Owner: Cascade agent working in learning_ai_common_plat

Progress:

  • All 9 packages built, tested (99 tests, 79/79 methods), committed (be03efa)
  • NomGap web: first consumer — billing-client, celebrations, time-references, gentle-notifications wired (abc13a5)
  • NomGap web: remaining integrations (accessibility, quick-actions, referral, org, marketplace)
  • Other product migrations (8 remaining products)

Context & Motivation

During NomGap (learning_ai_fastgap) Phase 5 buildout, we created several pure-TS engine modules that are product-agnostic and should live in learning_ai_common_plat/packages/ as shared @bytelyst/* packages. The same patterns apply (or will apply) to all 9+ ByteLyst products: LysnrAI, MindLyst, ChronoMind, JarvisJr, NomGap, PeakPulse, FlowMonk, NoteLett, ActionTrail.

Additionally, platform-service already has backend modules for referrals, subscriptions, plans, orgs, and licenses — but no corresponding client packages exist for products to consume them. Each product has been hand-rolling its own fetch calls or building bespoke modules.

What already exists in platform-service (port 4003)

IMPORTANT: All platform-service endpoints require an x-product-id header (read by getRequestProductId(req)). Client packages MUST send this header on every request.

Backend Module Exact Endpoints Auth Cosmos Container
modules/referrals/ GET /referrals, POST /referrals, PUT /referrals/:id, GET /referrals/stats, GET /referrals/by-referrer/:referrerId, GET /referrals/by-email/:email JWT referrals (/referrerId)
modules/subscriptions/ GET /subscriptions/:userId, POST /subscriptions, PUT /subscriptions/:userId (NOTE: keyed by userId, not id) JWT subscriptions (/userId)
modules/plans/ GET /plans{ plans: [...] }, GET /plans/:name, POST /plans, PUT /plans/:id, POST /plans/seed JWT plans (/productId)
modules/orgs/ GET/POST /orgs, GET/PATCH /orgs/:id, workspaces: GET/POST /orgs/:id/workspaces, PATCH /orgs/:id/workspaces/:workspaceId, memberships: GET/POST /orgs/:id/memberships, PATCH /orgs/:id/memberships/:membershipId Admin-only (super_admin or admin JWT role) organizations (/productId), workspaces (/orgId), memberships (/orgId)
modules/licenses/ Generate, activate, deactivate, list by user JWT licenses (/userId)
modules/marketplace/ Listings CRUD + certification + reviews + installs + reports + votes (6 entity types, 309 lines of types) JWT (some public) marketplace_listings (/productId), marketplace_reviews (/listingId), marketplace_installs (/userId), etc.

What NomGap built as product-specific (should be shared)

NomGap File What It Does Should Become
src/lib/referral.ts Referral code share links, apply codes, stats @bytelyst/referral-client
src/lib/monetization.ts Plan definitions, Pro feature gate, trial, restore @bytelyst/monetization
src/lib/b2b-wellness.ts Org dashboard, team challenges, licenses, SSO @bytelyst/org-client
src/lib/influencer.ts Branded challenges, affiliate tracking, share cards @bytelyst/marketplace-client
src/lib/celebrations.ts 14 celebration triggers, haptics, confetti, sounds, positive messages @bytelyst/celebrations
src/lib/gentle-notifications.ts ND-friendly messaging, adaptive frequency, forbidden phrases @bytelyst/gentle-notifications
src/lib/accessibility.ts VoiceOver/TalkBack label generators for common UI patterns @bytelyst/accessibility
src/lib/quick-actions.ts Progressive disclosure, smart defaults, action definitions @bytelyst/quick-actions
src/lib/time-blindness.ts Familiar duration references ("About as long as a movie") @bytelyst/time-references

Packages to Create (Priority Order)

Tier 1 — Highest Leverage (backend already exists, every product needs these)

1. @bytelyst/referral-client

Purpose: Browser/React Native-safe client for platform-service /referrals/* endpoints.

Reference implementation: learning_ai_fastgap/src/lib/referral.ts (73 lines)

Backend already exists: platform-service/src/modules/referrals/ — full CRUD + webhook dispatch + Zod schemas.

Backend types (from referrals/types.ts):

interface ReferralDoc {
  id: string;
  productId: string;
  referrerId: string;
  referrerEmail: string;
  referredUserId: string | null;
  referredEmail: string;
  status: 'pending' | 'signed_up' | 'subscribed' | 'rewarded';
  referrerRewardTokens: number;
  referredRewardTokens: number;
  referrerRewarded: boolean;
  referredRewarded: boolean;
  createdAt: string;
  completedAt: string | null;
}

Public API to implement:

export interface ReferralClientConfig {
  baseUrl: string; // e.g. 'http://localhost:4003/api'
  productId: string; // sent as x-product-id header on every request
  getAccessToken: () => string | null;
  // Defaults used when creating referrals (so callers only provide referredEmail)
  defaultRewardTokens?: { referrer: number; referred: number }; // default: 1000 / 500
}

export interface ReferralClient {
  // Core CRUD (delegates to platform-service)
  listMyReferrals(referrerId: string): Promise<{ referrals: ReferralDoc[]; count: number }>;
  getReferralStats(): Promise<{ total: number; completed: number; rewarded: number }>;
  createReferral(input: {
    referrerId: string; // from auth context
    referrerEmail: string; // from auth context
    referredEmail: string; // user input
  }): Promise<ReferralDoc>;
  // NOTE: There is NO /referrals/apply endpoint in platform-service.
  // "Applying" a referral = calling PUT /referrals/:id to update status to 'signed_up'.
  // The client should expose this as a semantic method:
  updateReferralStatus(
    id: string,
    referrerId: string,
    status: ReferralDoc['status']
  ): Promise<ReferralDoc>;
  getByEmail(email: string): Promise<ReferralDoc | null>;

  // Client-side helpers (pure TS, no network)
  buildShareLink(code: string): string;
  buildShareMessage(code: string, productName: string): string;
  calculateEarnedDays(conversions: number, daysPerReferral?: number): number;
}

export function createReferralClient(config: ReferralClientConfig): ReferralClient;

⚠️ Backend gap: Platform-service has no "apply referral code" endpoint. The flow for a referred user is:

  1. getByEmail(referredEmail) → finds existing pending referral
  2. updateReferralStatus(id, referrerId, 'signed_up') → marks as completed
  3. Backend fires webhook via dispatchReferralStatusChanged()

If a single "apply code" endpoint is needed, a new POST /referrals/apply route should be added to platform-service first.

Pattern: Follow @bytelyst/feature-flag-client exactly — createXxxClient() factory, types.ts + client.ts + index.ts, globalThis.fetch, no Node.js deps. Every request MUST include x-product-id and Authorization: Bearer <token> headers.

Tests: 8+ Vitest tests — create, listMyReferrals, stats, updateStatus, getByEmail, buildShareLink, buildShareMessage, calculateEarnedDays, error handling.


2. @bytelyst/subscription-client

Purpose: Client for platform-service /subscriptions/* + /plans/* — check entitlements, manage trials, restore purchases.

Reference implementation: learning_ai_fastgap/src/lib/monetization.ts (200 lines)

Backend already exists: platform-service/src/modules/subscriptions/ + platform-service/src/modules/plans/

Backend types (from subscriptions/types.ts):

interface SubscriptionDoc {
  id: string;
  productId: string;
  userId: string;
  plan: 'free' | 'pro' | 'enterprise';
  status: 'active' | 'cancelled' | 'past_due' | 'trialing';
  currentPeriodStart: string;
  currentPeriodEnd: string;
  cancelAtPeriodEnd: boolean;
  monthlyPrice: number;
  tokensIncluded: number;
  tokensUsed: number;
  stripeCustomerId?: string;
  stripeSubscriptionId?: string;
  createdAt: string;
  updatedAt: string;
}

Backend types (from plans/types.ts):

interface PlanConfig {
  id: string;
  productId: string;
  name: string;
  displayName: string;
  price: number;
  tokens: number;
  words: number;
  dictations: number;
  features: string[];
  stripePriceId?: string;
  active: boolean;
  createdAt: string;
  updatedAt: string;
}

Public API to implement:

export interface SubscriptionClientConfig {
  baseUrl: string;
  productId: string; // sent as x-product-id header on every request
  userId: string; // REQUIRED — subscription routes are keyed by userId, not id
  getAccessToken: () => string | null;
  storage?: { getItem(k: string): string | null; setItem(k: string, v: string): void };
}

export interface SubscriptionClient {
  // Server-authoritative checks (all hit platform-service)
  getMySubscription(): Promise<SubscriptionDoc | null>;
  // NOTE: GET /plans returns { plans: [...] } — client must unwrap .plans
  getPlans(): Promise<PlanConfig[]>;
  startTrial(planName?: string): Promise<SubscriptionDoc>;
  cancelSubscription(): Promise<SubscriptionDoc>;
  // NOTE: PUT /subscriptions/:userId (not :id) — use config.userId
  updateSubscription(updates: Partial<SubscriptionDoc>): Promise<SubscriptionDoc>;

  // Client-side helpers (cached, offline-safe)
  isPro(): boolean; // cached: plan !== 'free' && status === 'active' | 'trialing'
  isTrialing(): boolean; // cached: status === 'trialing'
  hasFeature(feature: string): boolean; // cached: looks up PlanConfig.features.includes(feature)
  daysRemaining(): number | null; // cached: days until currentPeriodEnd
  getCachedSubscription(): SubscriptionDoc | null;
  getCachedPlans(): PlanConfig[];
  refresh(): Promise<void>; // re-fetch from server + update cache
}

export function createSubscriptionClient(config: SubscriptionClientConfig): SubscriptionClient;

⚠️ Backend gaps & notes:

  • No restore-purchase endpoint — Platform-service has no store receipt verification. restorePurchase() was removed from the client API. To support IAP restore, either: (a) add a POST /subscriptions/restore endpoint to platform-service that verifies Apple/Google receipts, or (b) products verify receipts client-side and call updateSubscription() with the new status. Recommend (a) as a future platform-service enhancement.
  • PlanConfig.features is the entitlement sourcehasFeature('ai_coaching') checks PlanConfig.features.includes('ai_coaching') from the cached plan matching the user's subscription tier. Products define their own feature strings.
  • PlanConfig fields words, dictations, tokens are LysnrAI-specific legacy fields. Other products should ignore them and rely on features: string[] for entitlement checks. The PlanConfig type should be treated as extensible.
  • Subscription routes use :userId not :idGET /subscriptions/:userId and PUT /subscriptions/:userId. The client must use config.userId for these calls.

Key design: Cache subscription + plans in storage for offline reads. isPro(), hasFeature(), isTrialing() read from cache (never block on network). refresh() fetches from server and updates cache.

Tests: 10+ Vitest tests — getMySubscription, getPlans (unwraps .plans), startTrial, isPro, hasFeature (with features array), isTrialing, daysRemaining, cache persistence, refresh, error handling.


3. @bytelyst/celebrations

Purpose: Product-agnostic celebration engine — milestone triggers, haptic configs, confetti, sounds, positive reinforcement messages.

Reference implementation: learning_ai_fastgap/src/lib/celebrations.ts (190 lines)

No backend needed — pure client-side TS.

Public API to implement:

export type CelebrationTrigger =
  | 'task_completed'
  | 'streak_continued'
  | 'streak_milestone'
  | 'achievement_unlocked'
  | 'level_up'
  | 'personal_best'
  | 'milestone_reached'
  | 'goal_completed'
  | 'first_action'
  | 'halfway'
  | 'session_completed'
  | 'session_started';

export interface Celebration {
  id: string;
  title: string;
  body: string;
  emoji: string;
  hapticType: 'light' | 'medium' | 'heavy' | 'success' | 'warning';
  confetti: boolean;
  sound: 'chime' | 'success' | 'level_up' | 'none';
}

export interface CelebrationConfig {
  // Products register their own trigger→message mappings
  customTriggers?: Record<string, Celebration>;
}

export function createCelebrationEngine(config?: CelebrationConfig): {
  getCelebration(trigger: CelebrationTrigger | string): Celebration;
  getTimedCelebrations(elapsedMs: number, targetMs: number, shownIds: Set<string>): Celebration[];
  isPersonalBest(current: number, previous: number): boolean;
  getPositiveMessage(progressPercent: number): string;
  // For early breaks / incomplete actions — NEVER negative
  getPositiveIncompleteMessage(progressPercent: number): string;
};

Key design: Products register custom triggers via customTriggers config. Default triggers are universal (streaks, achievements, levels). Messages are ALWAYS positive — never guilt-inducing.

Tests: 8+ tests — all trigger types, custom triggers, timed celebrations, personal best, positive messages, incomplete messages.


4. @bytelyst/gentle-notifications

Purpose: Neurodivergent-friendly notification messaging system — encouraging tone, adaptive frequency, forbidden phrases.

Reference implementation: learning_ai_fastgap/src/lib/gentle-notifications.ts (120 lines)

No backend needed — pure client-side TS.

Public API to implement:

export interface GentleNotificationConfig {
  maxPerHour: number;
  tone: 'encouraging' | 'neutral' | 'minimal';
  adaptiveFrequency: boolean;
  dismissCount: number;
  suppressThreshold: number;
}

export interface GentleMessage {
  title: string;
  body: string;
  tone: 'encouraging' | 'neutral' | 'minimal';
}

export function createGentleNotificationEngine(config?: Partial<GentleNotificationConfig>): {
  getDefaultConfig(): GentleNotificationConfig;
  getMessage(type: string, config?: GentleNotificationConfig): GentleMessage;
  shouldSuppress(config: GentleNotificationConfig): boolean;
  recordDismissal(config: GentleNotificationConfig): GentleNotificationConfig;
  resetDismissals(config: GentleNotificationConfig): GentleNotificationConfig;

  // Products register their own message pools
  registerMessages(type: string, messages: GentleMessage[]): void;

  // Ecosystem-wide forbidden phrases — NEVER use these in any notification
  getForbiddenPhrases(): readonly string[];
  containsForbiddenPhrase(text: string): boolean;
};

// Exported constant for lint rules / CI checks
export const FORBIDDEN_PHRASES: readonly string[];

Forbidden phrases (ecosystem-wide policy):

[
  "You haven't",
  'You forgot',
  "Don't forget",
  'You should have',
  "Why didn't you",
  'You missed',
  'You failed',
  'You need to',
];

Key design: Products register their own message pools via registerMessages(). The forbidden phrases list is an ecosystem-wide policy enforced across all products. containsForbiddenPhrase() can be used in CI/lint.

Tests: 8+ tests — getMessage, shouldSuppress, recordDismissal, resetDismissals, registerMessages, forbiddenPhrases, containsForbiddenPhrase, adaptive frequency reduction.


5. @bytelyst/accessibility

Purpose: VoiceOver/TalkBack/Dynamic Type accessibility label generators for common UI patterns used across all products.

Reference implementation: learning_ai_fastgap/src/lib/accessibility.ts (220 lines)

No backend needed — pure client-side TS.

Public API to implement:

export interface A11yProps {
  accessible: boolean;
  accessibilityLabel: string;
  accessibilityHint?: string;
  accessibilityRole?:
    | 'button'
    | 'header'
    | 'text'
    | 'timer'
    | 'progressbar'
    | 'image'
    | 'alert'
    | 'summary'
    | 'adjustable';
  accessibilityState?: {
    disabled?: boolean;
    selected?: boolean;
    checked?: boolean;
    busy?: boolean;
    expanded?: boolean;
  };
  accessibilityValue?: { min?: number; max?: number; now?: number; text?: string };
}

// Common UI pattern label generators
export function buttonLabel(label: string, hint?: string): A11yProps;
export function timerLabel(status: string, elapsedText: string, context?: string): A11yProps;
export function progressLabel(name: string, percent: number, description?: string): A11yProps;
export function sliderLabel(metric: string, value: number, max?: number): A11yProps;
export function alertLabel(severity: string, message: string): A11yProps;
export function achievementLabel(name: string, description: string, earned: boolean): A11yProps;
export function streakLabel(current: number, longest: number): A11yProps;
export function listItemLabel(title: string, subtitle?: string, badge?: string): A11yProps;

// Utilities
export function formatDurationForA11y(ms: number): string; // "16 hours 30 minutes"
export function formatNumberForA11y(n: number): string; // "1,234" → "one thousand two hundred thirty four"
export function buildAnnouncement(headline: string, detail: string): string;

// Positive messaging (never negative, complements celebrations package)
export function getPositiveBreakMessage(progressPercent: number): string;

Key design: React Native accessibilityLabel / accessibilityRole compatible. Web apps can map to aria-label / role. Every label function returns a complete A11yProps object ready to spread onto a component.

Tests: 10+ tests — each label generator, formatDurationForA11y, formatNumberForA11y, buildAnnouncement, positiveBreakMessage, edge cases (0%, 100%, negative).


Tier 2 — High Value (no backend dependency)

6. @bytelyst/quick-actions

Purpose: Progressive disclosure system, smart defaults, quick action definitions for reducing cognitive load.

Reference implementation: learning_ai_fastgap/src/lib/quick-actions.ts (170 lines)

Public API to implement:

export interface QuickAction {
  id: string;
  label: string;
  icon: string;
  shortLabel: string;
  action: string;
  requiresAuth: boolean;
}

export interface ProgressiveSection {
  id: string;
  title: string;
  defaultExpanded: boolean;
  priority: 'primary' | 'secondary' | 'detail';
}

export interface SmartDefault {
  key: string;
  value: unknown;
  source: 'last_used' | 'most_common' | 'recommendation' | 'system';
}

export function getVisibleSections(
  sections: ProgressiveSection[],
  expandedIds: Set<string>
): ProgressiveSection[];
export function getAvailableActions(
  actions: QuickAction[],
  context: { isActive?: boolean; isAuthenticated?: boolean }
): QuickAction[];
export function pickSmartDefault(candidates: SmartDefault[]): SmartDefault | null;

export const MAX_VISIBLE_ITEMS = 3;
export const MAX_VISIBLE_LIST = 5;

Tests: 6+ tests.


7. @bytelyst/time-references

Purpose: Familiar duration references for time-blindness aids ("About as long as a movie", "3 episodes of The Office").

Reference implementation: learning_ai_fastgap/src/lib/time-blindness.ts (175 lines)

Public API to implement:

export interface TimeReference {
  text: string;
  emoji: string;
  category: 'media' | 'activity' | 'travel' | 'nature';
}

export function getTimeReference(elapsedHours: number): TimeReference;
export function getEpisodeComparison(
  elapsedHours: number,
  showName?: string,
  episodeMins?: number
): string;
export function getEncouragingMessage(elapsedHours: number): string;

// Products can register custom reference databases
export function registerReferences(
  entries: Array<{ minHours: number; maxHours: number; references: TimeReference[] }>
): void;

Tests: 6+ tests.


Tier 3 — Backend Client Wrappers

8. @bytelyst/org-client

Purpose: Client for platform-service /orgs/* + /licenses/* — B2B org management, workspace management, membership management.

Reference implementation: learning_ai_fastgap/src/lib/b2b-wellness.ts (100 lines) — but the actual backend is far richer.

Backend already exists: platform-service/src/modules/orgs/ (3 entity types) + platform-service/src/modules/licenses/

⚠️ Auth requirement: All org routes require admin-only access (super_admin or admin JWT role via requireAdmin(req)). Regular user tokens will get 403 Forbidden.

Backend types (from orgs/types.ts):

interface OrganizationDoc {
  id: string; // org_<uuid>
  productId: string;
  name: string;
  slug: string;
  status: 'active' | 'disabled';
  ownerUserId: string;
  metadata?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}

interface WorkspaceDoc {
  id: string; // ws_<uuid>
  orgId: string;
  productId: string;
  name: string;
  slug: string;
  status: 'active' | 'archived';
  description?: string;
  metadata?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
}

interface MembershipDoc {
  id: string; // mbr_<orgId>_<userId>_<scope>
  orgId: string;
  productId: string;
  scope: 'org' | 'workspace';
  workspaceId?: string;
  userId: string;
  role: 'owner' | 'admin' | 'member' | 'viewer';
  status: 'active' | 'invited' | 'disabled';
  invitedBy?: string;
  createdAt: string;
  updatedAt: string;
}

interface LicenseDoc {
  id: string;
  productId: string;
  key: string; // LYSNR-XXXX-XXXX-XXXX format
  userId: string;
  plan: 'free' | 'pro' | 'enterprise';
  status: 'active' | 'revoked' | 'expired';
  activatedAt: string | null;
  expiresAt: string | null;
  deviceIds: string[];
  maxDevices: number;
  createdAt: string;
  updatedAt: string;
}

Public API to implement:

export interface OrgClientConfig {
  baseUrl: string;
  productId: string;
  getAccessToken: () => string | null;
}

export interface OrgClient {
  // Organizations
  listOrgs(query?: { status?: string; limit?: number }): Promise<OrganizationDoc[]>;
  createOrg(input: { name: string; slug: string; ownerUserId?: string }): Promise<OrganizationDoc>;
  getOrg(id: string): Promise<OrganizationDoc>;
  updateOrg(id: string, updates: Partial<OrganizationDoc>): Promise<OrganizationDoc>;

  // Workspaces
  listWorkspaces(orgId: string): Promise<WorkspaceDoc[]>;
  createWorkspace(
    orgId: string,
    input: { name: string; slug: string; description?: string }
  ): Promise<WorkspaceDoc>;
  updateWorkspace(
    orgId: string,
    workspaceId: string,
    updates: Partial<WorkspaceDoc>
  ): Promise<WorkspaceDoc>;

  // Memberships
  listMemberships(
    orgId: string,
    query?: { scope?: string; limit?: number }
  ): Promise<MembershipDoc[]>;
  addMember(
    orgId: string,
    input: { userId: string; role?: string; scope?: string; workspaceId?: string }
  ): Promise<MembershipDoc>;
  updateMember(
    orgId: string,
    membershipId: string,
    updates: { role?: string; status?: string }
  ): Promise<MembershipDoc>;

  // Licenses
  generateLicense(input: {
    userId: string;
    plan: string;
    maxDevices?: number;
  }): Promise<LicenseDoc>;
  activateLicense(input: { key: string; deviceId: string }): Promise<LicenseDoc>;
  deactivateLicense(input: { key: string; deviceId: string }): Promise<void>;
}

export function createOrgClient(config: OrgClientConfig): OrgClient;

Tests: 10+ Vitest tests — listOrgs, createOrg, getOrg, listWorkspaces, createWorkspace, listMemberships, addMember, generateLicense, activateLicense, error handling (403 for non-admin).


9. @bytelyst/marketplace-client

Purpose: Client for platform-service /marketplace/* — template marketplace with listings, reviews, installs, certification, reports.

Reference implementation: learning_ai_fastgap/src/lib/influencer.ts (95 lines) covers a subset. The actual backend is a full marketplace with 6 entity types.

Backend already exists: platform-service/src/modules/marketplace/ — 309 lines of types, 6 entity types.

⚠️ Note: NomGap's influencer.ts (branded challenges, affiliate tracking) is a product-specific layer on top of the marketplace. The shared client should expose the generic marketplace API. Products build influencer/branded features on top.

Backend types (key entities from marketplace/types.ts):

interface MarketplaceListingDoc {
  id: string; // lst_<uuid>
  productId: string;
  templateType: string; // product-specific: 'fasting_protocol', 'agent_persona', 'timer_preset', etc.
  authorId: string;
  authorName: string;
  title: string;
  shortDescription: string;
  description: string;
  tags: string[];
  category: string;
  payload: Record<string, unknown>; // product-specific JSON
  pricingModel: 'free' | 'paid' | 'freemium';
  priceInCents: number;
  certificationStatus: 'draft' | 'submitted' | 'in_review' | 'approved' | 'rejected' | 'suspended';
  installCount: number;
  reviewCount: number;
  averageRating: number;
  visibility: 'private' | 'unlisted' | 'public';
  featured: boolean;
  version: string;
  createdAt: string;
  updatedAt: string;
}

interface MarketplaceReviewDoc {
  id: string; // rev_<uuid>
  listingId: string;
  productId: string;
  authorId: string;
  rating: number; // 1-5
  title: string;
  body: string;
  verified: boolean;
  createdAt: string;
}

interface MarketplaceInstallDoc {
  id: string; // inst_<uuid>
  listingId: string;
  productId: string;
  userId: string;
  version: string;
  installedAt: string;
  uninstalledAt: string | null;
}

Public API to implement:

export interface MarketplaceClientConfig {
  baseUrl: string;
  productId: string;
  getAccessToken: () => string | null;
}

export interface MarketplaceClient {
  // Listings
  listListings(query?: {
    templateType?: string;
    category?: string;
    tags?: string;
    pricingModel?: string;
    sortBy?: string;
    q?: string;
    limit?: number;
    offset?: number;
  }): Promise<{ listings: MarketplaceListingDoc[]; total: number }>;
  getListing(id: string): Promise<MarketplaceListingDoc>;
  createListing(input: CreateListingInput): Promise<MarketplaceListingDoc>;
  updateListing(
    id: string,
    updates: Partial<MarketplaceListingDoc>
  ): Promise<MarketplaceListingDoc>;
  submitForCertification(id: string, notes?: string): Promise<MarketplaceListingDoc>;

  // Installs
  installListing(listingId: string): Promise<MarketplaceInstallDoc>;
  uninstallListing(listingId: string): Promise<void>;
  listMyInstalls(query?: { limit?: number; offset?: number }): Promise<MarketplaceInstallDoc[]>;

  // Reviews
  listReviews(
    listingId: string,
    query?: { sortBy?: string; limit?: number }
  ): Promise<MarketplaceReviewDoc[]>;
  createReview(
    listingId: string,
    input: { rating: number; title: string; body: string }
  ): Promise<MarketplaceReviewDoc>;

  // Reports
  reportListing(listingId: string, input: { reason: string; details: string }): Promise<void>;
}

export function createMarketplaceClient(config: MarketplaceClientConfig): MarketplaceClient;

Tests: 10+ Vitest tests — listListings (with filters), getListing, createListing, submitForCertification, installListing, listMyInstalls, listReviews, createReview, reportListing, error handling.


Implementation Instructions

For each package, follow this exact pattern:

packages/<package-name>/
├── package.json          # @bytelyst/<name>, version 0.1.0, type: module
├── tsconfig.json         # extends ../../tsconfig.base.json
├── vitest.config.ts      # (optional) only if tests need special config; root config handles most cases
├── src/
│   ├── index.ts          # re-exports from client.ts + types.ts
│   ├── types.ts          # all interfaces + config types
│   └── client.ts         # createXxxClient() factory or pure functions
└── tests/
    └── client.test.ts    # Vitest tests

package.json template:

{
  "name": "@bytelyst/<package-name>",
  "version": "0.1.0",
  "type": "module",
  "description": "<one-line description>",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "test": "vitest run"
  }
}

Note on devDependencies: typescript and vitest are provided by the pnpm workspace root and do NOT need to be listed as devDependencies in individual packages. The root vitest.config.ts (with passWithNoTests: true) handles all packages. Only add a per-package vitest.config.ts if tests need special setup (e.g., custom globals or test environment).

tsconfig.json template:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

Conventions (MUST follow):

  • ESM everywhere: "type": "module", .js extensions in imports
  • No Node.js deps: Use globalThis.fetch, not node-fetch
  • No React/RN deps: Pure TS only — consumed by web, mobile, Node.js
  • Factory pattern for API clients: createXxxClient(config) returning an interface
  • Storage optional: Accept { getItem, setItem } for cache/persistence
  • Vitest tests: Every public function must have at least one test
  • No console.log — silent by default
  • No any type — use explicit types or generics

After creating packages:

  1. Add each to pnpm-workspace.yaml (already covered by packages/* glob)
  2. Run pnpm build to verify all compile
  3. Run pnpm test to verify all tests pass
  4. Run pnpm typecheck to verify no type errors

After packages are built, migrate product repos:

For each product (NomGap first, then others):

  1. Add "@bytelyst/<package>": "file:../../learning_ai_common_plat/packages/<package>" to product's package.json
  2. Replace product-specific module with thin wrapper that delegates to shared package
  3. Remove duplicated code
  4. Run product's npm test && npm run typecheck

Execution Order

# Package Tier Est. Lines Backend Dep Tests
1 @bytelyst/referral-client 1 ~150 Yes (referrals) 8
2 @bytelyst/subscription-client 1 ~220 Yes (subscriptions + plans) 10
3 @bytelyst/celebrations 1 ~150 No 8
4 @bytelyst/gentle-notifications 1 ~130 No 8
5 @bytelyst/accessibility 1 ~200 No 10
6 @bytelyst/quick-actions 2 ~120 No 6
7 @bytelyst/time-references 2 ~150 No 6
8 @bytelyst/org-client 3 ~250 Yes (orgs + licenses) — admin-only 10
9 @bytelyst/marketplace-client 3 ~200 Yes (marketplace — 6 entity types) 10
Total ~1,570 76

Verification

After all packages are created:

cd learning_ai_common_plat
pnpm build && pnpm test && pnpm typecheck

After NomGap migration:

cd learning_ai_fastgap
npm test && npm run typecheck

Products That Will Consume These Packages

= immediate consumer, = primary driver (has existing code to migrate), — = not needed initially (can adopt later)

Package NomGap LysnrAI MindLyst ChronoMind JarvisJr PeakPulse FlowMonk NoteLett ActionTrail
referral-client
subscription-client
celebrations
gentle-notifications
accessibility
quick-actions
time-references
org-client
marketplace-client

Notes on matrix:

  • time-references is useful for ANY app with timers, sessions, streaks, or progress tracking — not just fasting apps. Products customize the reference database via registerReferences().
  • org-client is relevant for all products if they offer a B2B tier. Initially only products with active B2B plans need it.
  • marketplace-client is currently relevant for NomGap (fasting protocols) and JarvisJr (agent personas). Other products can adopt it when they add marketplace features.

NomGap Migration Plan (First Product)

After the shared packages are built, NomGap (learning_ai_fastgap) should be migrated first since it has the most existing code to replace.

Files to replace in NomGap:

NomGap File Replace With Action
src/lib/referral.ts (73 lines) @bytelyst/referral-client Rewrite as thin wrapper: import { createReferralClient } from '@bytelyst/referral-client'
src/lib/monetization.ts (200 lines) @bytelyst/subscription-client Rewrite: createSubscriptionClient(config) + product-specific plan names
src/lib/b2b-wellness.ts (100 lines) @bytelyst/org-client Rewrite: createOrgClient(config) + NomGap-specific wellness types on top
src/lib/influencer.ts (95 lines) @bytelyst/marketplace-client Rewrite: createMarketplaceClient(config) + influencer layer on top
src/lib/celebrations.ts (190 lines) @bytelyst/celebrations Rewrite: createCelebrationEngine({ customTriggers: nomgapTriggers })
src/lib/gentle-notifications.ts (120 lines) @bytelyst/gentle-notifications Rewrite: createGentleNotificationEngine() + registerMessages('fasting', [...])
src/lib/accessibility.ts (220 lines) @bytelyst/accessibility Rewrite: import shared label generators + NomGap-specific labels
src/lib/quick-actions.ts (170 lines) @bytelyst/quick-actions Rewrite: import shared functions + NomGap-specific action/section defs
src/lib/time-blindness.ts (175 lines) @bytelyst/time-references Rewrite: import { getTimeReference } from '@bytelyst/time-references'

Steps:

  1. Add file: deps to package.json: "@bytelyst/referral-client": "file:../../learning_ai_common_plat/packages/referral-client", etc.
  2. For each file, replace the standalone implementation with a thin wrapper that delegates to the shared package
  3. Keep any NomGap-specific logic (fasting-specific celebrations, meal-specific suggestions) in the product repo
  4. Run npm test && npm run typecheck after each migration
  5. Total estimated reduction: ~1,340 lines of NomGap-specific code replaced with ~300 lines of thin wrappers

Other product migrations:

After NomGap is migrated, other products can adopt packages as needed. Each product adds file: deps and creates thin wrappers. No product needs to migrate all 9 packages at once — adopt incrementally.


Known Backend Gaps (Require Platform-Service Changes)

These gaps were identified during the review. They should be addressed in platform-service BEFORE or IN PARALLEL with the client packages:

Gap Impact Recommendation
No POST /referrals/apply endpoint Referred users can't "apply" a code in one step Add a POST /referrals/apply route that finds by email + updates status atomically
No POST /subscriptions/restore endpoint IAP restore requires receipt verification Add a store receipt verification endpoint (Apple/Google JWKS)
PlanConfig has LysnrAI-specific fields (words, dictations, tokens) Other products ignore these fields Consider making plan schema extensible with metadata: Record<string, unknown>
Org routes require admin JWT role Regular users can't view their own org Consider adding user-facing org endpoints (e.g., GET /my/org)