9/9 products audited and migrated: - NomGap: 9 mobile + 6 web files (+ 6 bug fixes) - ChronoMind: billing-client + feature-flags - MindLyst: billing-client + feature-flags - LysnrAI: feature-flags - JarvisJr: audited, no candidates (custom marketplace routes) - FlowMonk, NoteLett, ActionTrail: already fully migrated - PeakPulse: iOS-only, no web migration needed
41 KiB
Shared @bytelyst/* Client Packages — Extraction from Product Repos
Status: Packages Complete ✅ · All Product Migrations Complete ✅ Priority: High — eliminates duplication across 9+ products Estimated effort: 8–10 sessions Owner: Cascade agent working in
learning_ai_common_platProgress:
- ✅ All 9 packages built, tested (99 tests, 79/79 methods), committed (
be03efa)- ✅ NomGap web: all 9 packages wired — billing, celebrations, time-references, gentle-notifications, accessibility, quick-actions consumed (
abc13a5)- ✅ NomGap mobile: all 9 src/lib modules migrated to @bytelyst/* wrappers (
77cbf60)- ✅ NomGap mobile: 5 migration bugs fixed + stale test updated (
9a18746→1c7ed45)- ✅ NomGap web: billing-client userId fallback fixed (
9934e13)- ✅ ChronoMind web: billing-client + feature-flags migrated (
f49ef78)- ✅ MindLyst web: billing-client + feature-flags migrated (
f110668)- ✅ LysnrAI user-dashboard: feature-flags migrated (
7539c65)- ✅ JarvisJr: audited — marketplace uses custom /catalog routes, no migration candidates
- ✅ FlowMonk, NoteLett, ActionTrail: already fully migrated (no hand-rolled duplicates)
- ✅ PeakPulse: iOS-only (no web app to migrate)
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-idheader (read bygetRequestProductId(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:
getByEmail(referredEmail)→ finds existing pending referralupdateReferralStatus(id, referrerId, 'signed_up')→ marks as completed- 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 aPOST /subscriptions/restoreendpoint to platform-service that verifies Apple/Google receipts, or (b) products verify receipts client-side and callupdateSubscription()with the new status. Recommend (a) as a future platform-service enhancement. PlanConfig.featuresis the entitlement source —hasFeature('ai_coaching')checksPlanConfig.features.includes('ai_coaching')from the cached plan matching the user's subscription tier. Products define their own feature strings.PlanConfigfieldswords,dictations,tokensare LysnrAI-specific legacy fields. Other products should ignore them and rely onfeatures: string[]for entitlement checks. ThePlanConfigtype should be treated as extensible.- Subscription routes use
:userIdnot:id—GET /subscriptions/:userIdandPUT /subscriptions/:userId. The client must useconfig.userIdfor 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:
typescriptandvitestare provided by the pnpm workspace root and do NOT need to be listed as devDependencies in individual packages. The rootvitest.config.ts(withpassWithNoTests: true) handles all packages. Only add a per-packagevitest.config.tsif 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",.jsextensions 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
anytype — use explicit types or generics
After creating packages:
- Add each to
pnpm-workspace.yaml(already covered bypackages/*glob) - Run
pnpm buildto verify all compile - Run
pnpm testto verify all tests pass - Run
pnpm typecheckto verify no type errors
After packages are built, migrate product repos:
For each product (NomGap first, then others):
- Add
"@bytelyst/<package>": "file:../../learning_ai_common_plat/packages/<package>"to product'spackage.json - Replace product-specific module with thin wrapper that delegates to shared package
- Remove duplicated code
- 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-referencesis useful for ANY app with timers, sessions, streaks, or progress tracking — not just fasting apps. Products customize the reference database viaregisterReferences().org-clientis relevant for all products if they offer a B2B tier. Initially only products with active B2B plans need it.marketplace-clientis 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:
- Add
file:deps topackage.json:"@bytelyst/referral-client": "file:../../learning_ai_common_plat/packages/referral-client", etc. - For each file, replace the standalone implementation with a thin wrapper that delegates to the shared package
- Keep any NomGap-specific logic (fasting-specific celebrations, meal-specific suggestions) in the product repo
- Run
npm test && npm run typecheckafter each migration - 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) |