- Added @eslint/js dependency - Updated eslint.config.js for ESLint 9 compatibility - Added required globals (crypto, localStorage, React, etc.) - Fixed unused imports and variables - Disabled sort-imports temporarily - Formatted all files with Prettier
39 KiB
ByteLyst Ecosystem — Post-Refactor Architecture
Companion diagram:
ecosystem-after-refactor.drawio(open in draw.io or VS Code draw.io extension)Date: 2026-02-12 · Author: Cascade
Table of Contents
- Ecosystem Overview
- Three-Repo Structure
- Common Platform Packages — Detailed
- Component & Service Inventory
- Dependency Graph
- Migration Plan
- Advantages
- Cautions & Risks
- Versioning Strategy
- Testing Strategy
- CI/CD Impact
- Decision Log
1. Ecosystem Overview
The ByteLyst ecosystem consists of two product repos and one shared infrastructure repo:
┌─────────────────────────────────────────────────────────────────────────┐
│ CLIENT APPLICATIONS │
│ LysnrAI Desktop · iOS · Android · Admin · User Portal · Tracker │
│ MindLyst iOS · Android · Web │
└───────────────────────────────┬──────────────────────────────────────────┘
│
┌──────────────────┐ ┌────────┴────────┐ ┌──────────────────────────────┐
│ LysnrAI Repo │ │ Common Platform │ │ MindLyst Repo │
│ (voice_ai_agent)│←─│ (common_plat) │─→│ (multimodal_memory_agents) │
│ │ │ @bytelyst/* │ │ │
│ 4 Fastify svcs │ │ 8 npm packages │ │ KMP shared module │
│ 3 Next.js apps │ │ │ │ 3 native apps │
│ 1 FastAPI │ │ Design tokens │ │ 1 Next.js web │
│ 1 Desktop app │ │ (JSON → all) │ │ design-system/ │
│ 2 Mobile apps │ │ │ │ │
└────────┬─────────┘ └─────────────────┘ └──────────────┬───────────────┘
│ │
┌────────┴─────────────────────────────────────────────────┴───────────────┐
│ AZURE CLOUD INFRASTRUCTURE │
│ Cosmos DB · Blob Storage · Key Vault · Speech · OpenAI · Stripe │
│ Docker Compose (Traefik · Loki · Grafana) · GitHub Actions │
└──────────────────────────────────────────────────────────────────────────┘
2. Three-Repo Structure
2.1 learning_voice_ai_agent (LysnrAI)
Purpose: Cross-platform voice-to-text dictation platform.
| Component | Tech | Port | Description |
|---|---|---|---|
| Desktop App | Python 3.12, tkinter | — | macOS/Windows dictation app |
| FastAPI Backend | Python 3.12 | 8000 | REST API, cloud sync |
| platform-service | Fastify + TS | 4003 | Auth, audit, flags, notifications, blob, rate limiting |
| billing-service | Fastify + TS | 4002 | Subscriptions, plans, usage, licenses, Stripe |
| growth-service | Fastify + TS | 4001 | Invitations, referrals, promo codes |
| tracker-service | Fastify + TS | 4004 | Feature requests, bugs, votes, public roadmap |
| admin-dashboard-web | Next.js 16 | 3001 | Admin panel (users, tokens, audit, themes) |
| user-dashboard-web | Next.js 16 | 3002 | User portal (profile, billing, settings, SSO) |
| tracker-dashboard-web | Next.js | 3003 | Tracker board, kanban, public roadmap |
| iOS App | Swift + SwiftUI | — | Native (no KMP) |
| Android App | Kotlin + Compose | — | Native (no KMP) |
| Monitoring | Loki + Grafana | — | Health checks, logs, metrics |
2.2 learning_multimodal_memory_agents (MindLyst)
Purpose: Role-based Life OS — multimodal second-brain with AI triage.
| Component | Tech | Description |
|---|---|---|
| KMP Shared Module | Kotlin Multiplatform | Models, repositories, DI, triage pipeline, API clients |
| iOS App | SwiftUI + KMP | Native UI consuming shared Kotlin module |
| Android App | Jetpack Compose + KMP | Native UI consuming shared Kotlin module |
| Web Dashboard | Next.js 14 | Pages Router, CSS custom properties, landing + dashboard |
| design-system/ | JSON + CSS | Canonical tokens, component specs |
2.3 learning_ai_common_plat (Common Platform)
Purpose: Shared npm packages consumed by both product repos.
| Package | Type | Primary Consumers |
|---|---|---|
@bytelyst/errors |
TypeScript | All 4 Fastify services, dashboards |
@bytelyst/cosmos |
TypeScript | All 4 Fastify services, 2 Next.js dashboards |
@bytelyst/config |
TypeScript | All 4 Fastify services |
@bytelyst/auth |
TypeScript | All services + dashboards with JWT |
@bytelyst/fastify-core |
TypeScript | All 4 Fastify services |
@bytelyst/api-client |
TypeScript | All 3 Next.js dashboards |
@bytelyst/react-auth |
TypeScript/React | All 3 Next.js dashboards |
@bytelyst/design-tokens |
JSON → multi-format | MindLyst (all platforms), potentially LysnrAI dashboards |
3. Common Platform Packages — Detailed
3.1 @bytelyst/errors (P0)
What it replaces: 4 separate errors.ts files across services (157 LOC total).
// Unified error hierarchy
export class ServiceError extends Error {
constructor(public statusCode: number, message: string) { ... }
}
export class NotFoundError extends ServiceError { /* 404 */ }
export class BadRequestError extends ServiceError { /* 400 */ }
export class UnauthorizedError extends ServiceError { /* 401 */ }
export class ForbiddenError extends ServiceError { /* 403 */ }
export class ConflictError extends ServiceError { /* 409 */ }
export class TooManyRequestsError extends ServiceError { /* 429 */ }
Design decision: Superset of all error types currently used. Services import only what they need. The details property (used by billing-service's TooManyRequests) is added to the base class as optional.
Dependencies: None (leaf package).
3.2 @bytelyst/cosmos (P0)
What it replaces: 6 separate cosmos.ts files (306 LOC total).
// Core exports
export function getCosmosClient(): CosmosClient;
export function getDatabase(dbName?: string): Database;
export function getContainer(name: string): Container;
// Container registry (dashboards use this)
export function registerContainers(config: Record<string, ContainerConfig>): void;
export function initializeAllContainers(): Promise<void>;
// Types
export interface ContainerConfig {
partitionKeyPath: string;
defaultTtl?: number | null;
}
Design decision: The simple getContainer(name) used by microservices works alongside the full registry used by dashboards. Services call getContainer() directly; dashboards call registerContainers() first to set up the partition key map.
Dependencies: @azure/cosmos (peer dependency).
3.3 @bytelyst/config (P1)
What it replaces: 4 separate config.ts + 4 separate product-config.ts (142 LOC total).
// Base env schema — every Fastify service needs these
export const baseEnvSchema = z.object({
PORT: z.coerce.number().default(3000),
HOST: z.string().default('0.0.0.0'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
CORS_ORIGIN: z.string().optional(),
SERVICE_NAME: z.string(),
COSMOS_ENDPOINT: z.string().min(1),
COSMOS_KEY: z.string().min(1),
COSMOS_DATABASE: z.string().default('lysnrai'),
});
// Extend per-service
export function loadConfig<T extends z.ZodRawShape>(extension: T) {
return baseEnvSchema.extend(extension).parse(process.env);
}
// Product identity
export function loadProductIdentity(path?: string): ProductIdentity;
export interface ProductIdentity {
productId: string;
displayName: string;
licensePrefix: string;
configDirName: string;
envVarPrefix: string;
}
Design decision: Each service calls loadConfig({ STRIPE_SECRET_KEY: z.string(), ... }) to extend the base. The product identity reads from shared/product.json or falls back to env vars, eliminating the hardcoded product-config.ts copies.
Dependencies: zod (peer dependency).
3.4 @bytelyst/auth (P1)
What it replaces: 5 separate JWT/auth files (250 LOC total).
// JWT operations (configurable issuer + expiry)
export function createJwtUtils(options: {
issuer: string;
accessTokenExpiry?: string; // default "1h"
refreshTokenExpiry?: string; // default "30d"
}): {
createAccessToken(payload: TokenPayload): Promise<string>;
createRefreshToken(payload: { sub: string }): Promise<string>;
verifyToken(token: string): Promise<TokenPayload | null>;
};
// Fastify middleware
export function extractAuth(req: FastifyRequest): Promise<AuthPayload>;
export function requireRole(...roles: string[]): FastifyHook;
// Next.js server-side
export function getCurrentUser(
authHeader: string | null,
getUserById: Function
): Promise<UserDoc | null>;
// Password hashing
export function hashPassword(plain: string): Promise<string>;
export function verifyPassword(plain: string, hash: string): Promise<boolean>;
// Types
export interface AuthPayload {
sub: string;
email?: string;
role?: string;
productId?: string;
type?: string;
}
export interface TokenPayload {
sub: string;
email: string;
role: string;
type: 'access' | 'refresh';
}
Design decision: The createJwtUtils() factory pattern allows each service to configure its own issuer (lysnrai-admin, lysnrai-user, lysnrai) while sharing all the logic. Platform-service creates tokens; other services only verify. The requireRole() hook is a new addition that multiple services currently implement inline.
Dependencies: jose, bcryptjs (peer dependencies).
3.5 @bytelyst/fastify-core (P1)
What it replaces: 4 separate server.ts files (351 LOC total).
export async function createServiceApp(options: {
name: string;
version: string;
description: string;
port?: number;
corsOrigin?: string;
}): Promise<FastifyInstance> {
const app = Fastify({ logger: true });
// CORS
await app.register(cors, { origin: ... });
// OpenAPI / Swagger
await app.register(swagger, { openapi: { info: { title, version, description } } });
// Prometheus metrics
await app.register(metricsPlugin, { endpoint: "/metrics" });
// x-request-id propagation
app.addHook("onRequest", requestIdHook);
// Health endpoint
app.get("/health", healthHandler(options.name, options.version));
// ServiceError-aware error handler
app.setErrorHandler(serviceErrorHandler);
return app;
}
export async function startService(app: FastifyInstance, port: number, host?: string): Promise<void>;
Design decision: After calling createServiceApp(), each service just registers its route modules and calls startService(). The server.ts in each service shrinks from ~85 lines to ~15 lines. The healthHandler returns the standard { status, service, version, timestamp, requestId } shape that the monitoring health-check script expects.
Dependencies: fastify, @fastify/cors, @fastify/swagger, fastify-metrics, @bytelyst/errors, @bytelyst/config (peer dependencies).
3.6 @bytelyst/api-client (P2)
What it replaces: 5+ separate fetch wrapper implementations (200 LOC total).
export function createApiClient(options: {
baseUrl: string;
getToken?: () => string | null;
defaultHeaders?: Record<string, string>;
}): {
// Throws on error (for service-to-service)
fetch<T>(path: string, options?: RequestInit): Promise<T>;
// Returns { data, error } tuple (for UI components)
safeFetch<T>(
path: string,
options?: RequestInit
): Promise<{ data: T | null; error: string | null }>;
};
Design decision: Two fetch modes: throwing (used by server-side service clients like billing-client.ts, growth-client.ts) and safe (used by React components via api.ts). The getToken() callback handles auth header injection without coupling to any specific storage mechanism.
Dependencies: None (uses native fetch).
3.7 @bytelyst/react-auth (P2)
What it replaces: 3 separate auth-context.tsx files (450 LOC total).
export function createAuthProvider<TUser extends BaseUser>(config: {
storagePrefix: string; // "admin" | "portal" | "tracker"
loginEndpoint: string; // "/api/auth/login"
mapResponseToUser: (data: any) => TUser;
enableSSO?: boolean;
}): {
AuthProvider: React.FC<{ children: ReactNode }>;
useAuth: () => AuthContextValue<TUser>;
};
export interface BaseUser {
id?: string;
email: string;
name: string;
role: string;
}
export interface AuthContextValue<TUser> {
user: TUser | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<boolean>;
logout: () => void;
getAccessToken: () => string | null;
}
Design decision: Factory pattern produces a fully-typed AuthProvider + useAuth() pair. Each dashboard provides its own TUser type and storage prefix. SSO cookie support (used by user-dashboard) is opt-in via enableSSO. Registration support (also user-dashboard only) can be added as an optional config callback.
Dependencies: react (peer dependency), @bytelyst/api-client.
3.8 @bytelyst/design-tokens (P3)
What it replaces: 5 manually-synced token files across MindLyst.
packages/design-tokens/
├── tokens/
│ └── bytelyst.tokens.json ← SINGLE SOURCE OF TRUTH
├── generated/ ← Auto-generated (committed or gitignored)
│ ├── tokens.css ← CSS custom properties (--ml-*)
│ ├── tokens.ts ← TypeScript constants
│ ├── tokens.kt ← Kotlin object (MindLystTokens)
│ └── tokens.swift ← Swift structs (MindLystColors/Spacing/Radius)
├── scripts/
│ └── generate.ts ← Reads JSON, emits all formats
└── package.json
Design decision: The canonical bytelyst.tokens.json (migrated from MindLyst's existing mindlyst.tokens.json) is the single source. A generation script produces platform-specific files. Consumers either:
- Import the generated file directly (CSS, TS)
- Copy the generated file into their project as a build step (Kotlin, Swift)
This eliminates the manual sync problem where token changes require updating 5 files.
Dependencies: None at runtime; typescript for the generation script.
4. Component & Service Inventory
4.1 All Services (Post-Refactor)
| Service | Repo | Port | Shared Packages Used |
|---|---|---|---|
| platform-service | LysnrAI | 4003 | errors, cosmos, config, auth, fastify-core |
| billing-service | LysnrAI | 4002 | errors, cosmos, config, fastify-core |
| growth-service | LysnrAI | 4001 | errors, cosmos, config, fastify-core |
| tracker-service | LysnrAI | 4004 | errors, cosmos, config, auth, fastify-core |
| FastAPI Backend | LysnrAI | 8000 | (Python — separate concern) |
| admin-dashboard | LysnrAI | 3001 | cosmos, auth, api-client, react-auth |
| user-dashboard | LysnrAI | 3002 | cosmos, auth, api-client, react-auth |
| tracker-dashboard | LysnrAI | 3003 | api-client, react-auth |
| MindLyst Web | MindLyst | 3050 | design-tokens |
| MindLyst iOS | MindLyst | — | design-tokens (generated Swift) |
| MindLyst Android | MindLyst | — | design-tokens (generated Kotlin) |
4.2 Azure Infrastructure
| Service | Resource | Used By |
|---|---|---|
| Cosmos DB | cosmos-mywisprai (Serverless) | All TS services + dashboards |
| Blob Storage | bytelystblobs (6 containers) | platform-service, desktop, backend |
| Key Vault | kv-mywisprai | Desktop, mobile, backend |
| Speech | mywisprai-speech (F0) | Desktop, LysnrAI mobile |
| Azure OpenAI | mywisprai-openai-sweden (S0) | Desktop, backend |
| OpenAI API | api.openai.com | MindLyst KMP (triage, Whisper) |
| Stripe | Payments API | billing-service, growth-service |
4.3 DevOps Components
| Component | Location | Scope |
|---|---|---|
| Docker Compose | LysnrAI root | All services + Loki + Grafana + Traefik |
| Health Check | services/monitoring/ | Aggregates all /health endpoints |
| GitHub Actions | .github/workflows/ (9 files) | Per-service CI |
| run-local-all-services.sh | LysnrAI root | Dev startup script |
5. Dependency Graph
@bytelyst/design-tokens (standalone — no deps)
│
└─→ MindLyst iOS/Android/Web
@bytelyst/errors (standalone — no deps)
│
├─→ @bytelyst/fastify-core
├─→ All 4 Fastify services
└─→ All 3 Next.js dashboards (via API routes)
@bytelyst/cosmos (depends on: @azure/cosmos)
│
├─→ All 4 Fastify services
└─→ admin-dashboard, user-dashboard
@bytelyst/config (depends on: zod)
│
├─→ @bytelyst/fastify-core
├─→ @bytelyst/auth
└─→ All 4 Fastify services
@bytelyst/auth (depends on: jose, bcryptjs, @bytelyst/config)
│
├─→ @bytelyst/api-client
├─→ platform-service (creates tokens)
├─→ tracker-service (verifies tokens)
└─→ admin/user dashboards (server-side auth)
@bytelyst/fastify-core (depends on: fastify, @bytelyst/errors, @bytelyst/config)
│
└─→ All 4 Fastify services
@bytelyst/api-client (no runtime deps)
│
├─→ @bytelyst/react-auth
└─→ All 3 dashboards
@bytelyst/react-auth (depends on: react, @bytelyst/api-client)
│
└─→ All 3 Next.js dashboards
Key insight: The graph is a clean DAG (directed acyclic graph) with no circular dependencies. Leaf packages (errors, cosmos, design-tokens) can be extracted first with zero risk.
6. Migration Plan
Phase 1: Foundation (Week 1)
-
Initialize
learning_ai_common_platas pnpm workspace monorepopnpm init,pnpm-workspace.yaml,tsconfig.base.json- Shared
vitestconfig,ruff.toml(for future Python packages)
-
Extract
@bytelyst/errors(P0, 1 hour)- Copy
platform-service/src/lib/errors.tsas starting point - Add
ConflictError(from tracker) andTooManyRequestsError(from billing) - Add optional
detailsfield to baseServiceError - Write tests (port from service tests)
- Update all 4 services:
import { ServiceError, ... } from "@bytelyst/errors"
- Copy
-
Extract
@bytelyst/cosmos(P0, 2 hours)- Merge service version (simple) + dashboard version (registry + TTL)
- Parameterize default database name
- Write tests (mock Cosmos client)
- Update all 6 consumers
Phase 2: Service Infrastructure (Week 2)
-
Extract
@bytelyst/config(P1, 2 hours)- Create
baseEnvSchemafrom common fields - Add
loadConfig()extension function - Add
loadProductIdentity()fromshared/product.json - Update all 4 services + remove 4
product-config.tsfiles
- Create
-
Extract
@bytelyst/auth(P1, 3 hours)- Create
createJwtUtils()factory - Port
extractAuth()Fastify hook - Port
getCurrentUser()for Next.js - Port
hashPassword()/verifyPassword() - Critical: test all auth flows end-to-end after migration
- Create
-
Extract
@bytelyst/fastify-core(P1, 3 hours)- Create
createServiceApp()factory - Port request-id hook, health handler, error handler
- Refactor all 4
server.tsfiles to use factory - Verify Docker builds still pass
- Create
Phase 3: Dashboard Libraries (Week 3)
-
Extract
@bytelyst/api-client(P2, 2 hours)- Create
createApiClient()with both fetch modes - Update dashboard client files to use shared client
- Service-specific methods stay in their respective client files
- Create
-
Extract
@bytelyst/react-auth(P2, 3 hours)- Create
createAuthProvider()factory - Port SSO cookie logic as opt-in
- Update all 3 dashboard auth contexts
- Test login/logout flows in each dashboard
- Create
Phase 4: Design System (Week 4+)
- Extract
@bytelyst/design-tokens(P3, 4 hours)- Migrate
mindlyst.tokens.jsonas canonical source - Write generation script for CSS, TS, Kotlin, Swift
- Add to MindLyst build pipeline
- Consider adopting for LysnrAI dashboards (future)
- Migrate
7. Advantages
7.1 Immediate Benefits
| Benefit | Impact | Measurement |
|---|---|---|
| ~1,580 LOC eliminated | Less code to maintain | 8 fewer files per service feature |
| Single-point security fixes | JWT/auth vulnerability fixed once, applied everywhere | 1 PR instead of 5 |
| Consistent error handling | All services return identical error shapes | 0 divergence |
| Faster new service creation | createServiceApp() → 15 lines to launch |
Minutes vs hours |
| Cosmos DB config in one place | Container registry + TTL managed centrally | 1 file vs 6 |
7.2 Medium-Term Benefits
| Benefit | Impact |
|---|---|
| MindLyst backend bootstrap | When MindLyst adds backend services, it gets Fastify infra for free |
| Consistent design language | Token generation ensures iOS/Android/Web never drift |
| Cross-product features | Tracker service already product-agnostic; shared auth enables MindLyst to use it |
| Onboarding speed | New developer learns one pattern, applies to all services |
| Testability | Shared packages have isolated tests; consumer tests focus on business logic |
7.3 Architectural Benefits
| Benefit | Description |
|---|---|
| Separation of concerns | Infrastructure (how) separated from business logic (what) |
| Dependency inversion | Services depend on abstractions (@bytelyst/auth) not implementations |
| DRY principle | No more copy-paste-modify cycle for new services |
| Contract stability | Shared types ensure API contracts don't drift between services |
| Upgrade path | Update jose or @azure/cosmos in one place |
8. Cautions & Risks
8.1 HIGH RISK — Tightly Coupled Releases
| Risk | Description | Mitigation |
|---|---|---|
| Breaking change cascade | A breaking change in @bytelyst/auth breaks all services simultaneously |
Semantic versioning + file: references pin to exact commits. Run all consumer tests before publishing. |
| Diamond dependency | Two packages depend on different versions of a shared dep | Use peer dependencies + strict version ranges. pnpm handles this well. |
| Deployment ordering | Must deploy common-plat before consuming repos can use new features | Use git tags/releases. Consumers reference a specific version or commit. |
8.2 MEDIUM RISK — Development Friction
| Risk | Description | Mitigation |
|---|---|---|
| Cross-repo PRs | A feature spanning common-plat + LysnrAI requires 2 PRs | Use file:../learning_ai_common_plat/packages/X in dev, pin to version in CI. |
| Local dev setup | Developers need all 3 repos cloned side-by-side | Document in README. Add a setup script. Consider git submodules as alternative. |
| IDE navigation | "Go to definition" may not work across repos | TypeScript project references or workspace-level tsconfig.json can help. |
| Over-abstraction | Temptation to extract things that aren't truly shared | Rule: only extract when ≥2 consumers exist AND the code is >90% identical. |
8.3 LOW RISK — Operational
| Risk | Description | Mitigation |
|---|---|---|
| npm registry dependency | Publishing to GitHub Packages adds infra complexity | Start with file: references (zero infra). Upgrade to registry only if team grows. |
| Test isolation | Shared package tests may not catch consumer-specific issues | Each consumer repo keeps integration tests. Shared packages only unit-test their own API. |
| Python exclusion | The desktop app and FastAPI backend can't use TypeScript packages | Python remains independent. If patterns emerge (e.g., Cosmos client), create a Python equivalent later. |
| KMP exclusion | MindLyst's Kotlin code can't use npm packages directly | Only design-tokens applies to KMP (via generated Kotlin). All other packages are TS-only. |
8.4 Things to AVOID
- Don't extract too early — Wait until code is stable and duplicated in ≥2 places
- Don't create a "utils" dumping ground — Every package must have a clear, single responsibility
- Don't abstract configuration — Each service's
config.tsshould remain in the service; only the base schema is shared - Don't share React components — UI components are product-specific; only infrastructure (auth, fetch) is shared
- Don't break existing imports in one big PR — Migrate one package at a time, keep old imports working temporarily
9. Versioning Strategy
Recommended: File References (Phase 1)
// In LysnrAI's services/platform-service/package.json
{
"dependencies": {
"@bytelyst/errors": "file:../../../learning_ai_common_plat/packages/errors",
"@bytelyst/cosmos": "file:../../../learning_ai_common_plat/packages/cosmos",
},
}
Pros: Zero infrastructure, works locally, pnpm install resolves it.
Cons: All repos must be cloned side-by-side. Docker builds need a multi-stage approach or volume mount.
Future: GitHub Packages (Phase 2+)
{
"dependencies": {
"@bytelyst/errors": "^1.0.0",
"@bytelyst/cosmos": "^1.0.0",
},
}
Pros: True version pinning, CI-friendly, no local path dependency. Cons: Requires GitHub Packages setup, publish workflow, access tokens.
Versioning Rules
- MAJOR (1.0 → 2.0): Breaking change to public API (renamed export, removed function, changed return type)
- MINOR (1.0 → 1.1): New export, new optional parameter, new error subclass
- PATCH (1.0.0 → 1.0.1): Bug fix, internal refactor, dependency update
10. Testing Strategy
Package-Level Tests
Each @bytelyst/* package has its own vitest test suite:
| Package | Test Focus |
|---|---|
| errors | Error class hierarchy, statusCode correctness, instanceof checks |
| cosmos | Client singleton behavior, container registry, env var reading (mocked) |
| config | Zod schema validation, defaults, extension merging |
| auth | JWT sign/verify round-trip, expiry, issuer validation, bcrypt hashing |
| fastify-core | App factory produces correct hooks/routes, health endpoint shape |
| api-client | Fetch wrapper behavior with mock server, error handling, auth injection |
| react-auth | Provider renders, login/logout flows, localStorage persistence |
| design-tokens | Generated output matches expected format for each platform |
Consumer-Level Tests
Each service/dashboard keeps its existing test suite. After migration:
- Service tests verify that imported shared functions work correctly in context
- No need to re-test JWT internals — that's the package's job
- Focus on integration: "Does my route handler correctly call
extractAuth()and get the right payload?"
CI Matrix
common-plat CI:
→ Run all 8 package test suites
→ Run type-check (tsc --noEmit)
→ Run lint (ruff for future Python packages)
LysnrAI CI (per-service):
→ Install common-plat packages (file: or registry)
→ Run service tests
→ Run Next.js build
MindLyst CI:
→ Build KMP shared module
→ Build Next.js web
→ Verify generated tokens match source JSON
11. CI/CD Impact
Docker Build Changes
Services currently build independently. After extraction:
Option A: Multi-stage with copy (recommended for file: references)
# Copy common-plat packages into build context
COPY ../learning_ai_common_plat/packages/errors /common/errors
COPY ../learning_ai_common_plat/packages/cosmos /common/cosmos
# Adjust package.json to point to /common/*
Option B: Pre-publish (recommended for registry)
# Common-plat packages are already on npm registry
RUN npm install # resolves @bytelyst/* from GitHub Packages
Option C: Monorepo build context (if repos are merged or submoduled)
# Build context includes all repos
COPY . /workspace
WORKDIR /workspace/learning_voice_ai_agent/services/platform-service
RUN npm install && npm run build
docker-compose.yml Changes
The existing docker-compose.yml may need an additional volume mount or build context adjustment:
services:
platform-service:
build:
context: . # May need to widen to parent directory
dockerfile: services/platform-service/Dockerfile
GitHub Actions Changes
- New workflow:
ci-common-plat.yml— tests all shared packages on push tolearning_ai_common_plat - Modified workflows: Each service CI installs common-plat packages before running tests
- Potential: A "consumer test" job in
ci-common-plat.ymlthat runs key consumer tests after package changes
12. Decision Log
| Decision | Rationale | Alternatives Considered |
|---|---|---|
| pnpm workspace monorepo for common-plat | Simplest multi-package management, strict dep resolution | npm workspaces (less strict), Turborepo (overkill for 8 packages), Nx (too heavy) |
file: references initially |
Zero infrastructure, works immediately | git submodules (complex merge), npm registry (needs setup), copy-paste (defeats purpose) |
| Factory pattern for fastify-core, auth, react-auth | Services need slightly different config (port, issuer, user type) but identical boilerplate | Inheritance (fragile), config objects (less type-safe), template codegen (complex) |
| Peer dependencies for heavy libs | Prevents version conflicts (e.g., two versions of @azure/cosmos in one service) |
Direct deps (risk duplicate bundles), optional deps (too loose) |
| TypeScript-only packages (no Python) | Python desktop/backend have less duplication and different patterns | Shared Python package (only 2 consumers, not worth the packaging complexity yet) |
| Design tokens as JSON → generated code | JSON is the universal format; generation ensures consistency | Manual sync (error-prone), Figma API (adds dependency), CSS-only (excludes native) |
| NOT extracting React UI components | UI is product-specific; only infrastructure is truly shared | Shared component library (premature — products have different UX) |
| NOT merging repos | Three distinct products with different release cadences and teams | Monorepo (coupling), git submodules (merge conflicts) |
Appendix A: File Counts Per Service (Before vs After)
Before Refactor
services/platform-service/src/lib/
├── config.ts (32 lines)
├── cosmos.ts (25 lines)
├── errors.ts (38 lines)
├── product-config.ts (9 lines)
└── blob.ts (service-specific, stays)
services/billing-service/src/lib/
├── config.ts (34 lines) ← DUPLICATE
├── cosmos.ts (25 lines) ← DUPLICATE
├── errors.ts (41 lines) ← DUPLICATE
└── product-config.ts (9 lines) ← DUPLICATE
services/growth-service/src/lib/
├── config.ts (25 lines) ← DUPLICATE
├── cosmos.ts (25 lines) ← DUPLICATE
├── errors.ts (35 lines) ← DUPLICATE
└── product-config.ts (11 lines) ← DUPLICATE
services/tracker-service/src/lib/
├── auth.ts (52 lines) ← DUPLICATE
├── config.ts (24 lines) ← DUPLICATE
├── cosmos.ts (25 lines) ← DUPLICATE
├── errors.ts (44 lines) ← DUPLICATE
└── product-config.ts (7 lines) ← DUPLICATE
After Refactor
services/platform-service/src/lib/
├── blob.ts (service-specific, stays)
└── (all others → @bytelyst/*)
services/billing-service/src/lib/
└── (empty — all moved to @bytelyst/*)
services/growth-service/src/lib/
└── (empty — all moved to @bytelyst/*)
services/tracker-service/src/lib/
└── (empty — all moved to @bytelyst/*)
Each server.ts: ~85 lines → ~15 lines
Each config.ts: ~30 lines → ~5 lines (just the extension schema)
Appendix B: Ecosystem Metrics
| Metric | Before | After | Delta |
|---|---|---|---|
| Duplicated infrastructure LOC | ~1,856 | ~275 | -85% |
Number of errors.ts files |
4 | 1 | -75% |
Number of cosmos.ts files |
6 | 1 | -83% |
Number of auth/jwt files |
5 | 1 | -80% |
Number of server.ts boilerplate lines |
351 | ~60 | -83% |
| Time to create new Fastify service | ~2 hours | ~20 min | -83% |
| Time to fix a JWT vulnerability | 5 PRs | 1 PR | -80% |
| Design token sync effort (MindLyst) | Manual (5 files) | Automated (1 JSON) | -100% |