docs: add common platform analysis, ecosystem architecture, and drawio diagram

- COMMON_PLATFORM_ANALYSIS.md: identifies 8 shared packages to extract from LysnrAI and MindLyst repos (~1,580 LOC duplication eliminated)
- ECOSYSTEM_ARCHITECTURE.md: detailed post-refactor architecture with components, services, migration plan, advantages, cautions, versioning, testing, and CI/CD impact
- ecosystem-after-refactor.drawio: 4-layer architecture diagram (clients, repos, common platform, Azure infra)
This commit is contained in:
saravanakumardb1 2026-02-12 10:40:28 -08:00
commit a874a4332b
3 changed files with 1667 additions and 0 deletions

View File

@ -0,0 +1,381 @@
# Common Platform Analysis
> **Purpose:** Identify duplicated code across `learning_voice_ai_agent` (LysnrAI) and `learning_multimodal_memory_agents` (MindLyst) that should be extracted into `learning_ai_common_plat` as shared packages.
>
> **Date:** 2026-02-12
---
## Executive Summary
After scanning both repos, there are **8 high-value extraction candidates** across 3 tiers (TypeScript services, Next.js dashboards, design system). The heaviest duplication is in the LysnrAI monorepo where 4 Fastify microservices each contain near-identical copies of 5 foundational files. The MindLyst repo shares the same design token values and will benefit from a shared design system package.
---
## 1. Duplicated Components — TypeScript Microservices
### 1.1 Cosmos DB Client Singleton
**Duplication:** 4 near-identical copies across LysnrAI services + 2 in dashboards = **6 copies**
| Location | Lines |
|----------|-------|
| `services/platform-service/src/lib/cosmos.ts` | 25 |
| `services/billing-service/src/lib/cosmos.ts` | 25 |
| `services/growth-service/src/lib/cosmos.ts` | 25 |
| `services/tracker-service/src/lib/cosmos.ts` | 25 |
| `admin-dashboard-web/src/lib/cosmos.ts` | 112 |
| `user-dashboard-web/src/lib/cosmos.ts` | 94 |
**Pattern:** All do: lazy singleton `CosmosClient`, read `COSMOS_ENDPOINT` + `COSMOS_KEY` + `COSMOS_DATABASE` from env, expose `getContainer(name)`. Dashboard versions add container registry with partition key definitions and TTL config.
**Proposed shared package:** `@bytelyst/cosmos`
```
packages/cosmos/
src/
client.ts — getCosmosClient(), getDatabase(), getContainer()
containers.ts — container registry (names, partition keys, TTL)
types.ts — ContainerConfig interface
index.ts
package.json
```
---
### 1.2 Service Error Classes
**Duplication:** 4 copies across LysnrAI services, each slightly different
| Location | Error Classes |
|----------|--------------|
| `platform-service/src/lib/errors.ts` | ServiceError, NotFound, BadRequest, Unauthorized, Forbidden |
| `billing-service/src/lib/errors.ts` | ServiceError, NotFound, BadRequest, Forbidden, TooManyRequests |
| `growth-service/src/lib/errors.ts` | ServiceError, NotFound, BadRequest, Forbidden |
| `tracker-service/src/lib/errors.ts` | ServiceError, NotFound, BadRequest, Unauthorized, Forbidden, Conflict |
**Pattern:** All share the same `ServiceError` base class with `statusCode` + `message`. Subclasses vary per service (some have Unauthorized, some have Conflict, etc.).
**Proposed shared package:** `@bytelyst/errors`
```
packages/errors/
src/
service-error.ts — base ServiceError class
http-errors.ts — NotFound, BadRequest, Unauthorized, Forbidden, Conflict, TooManyRequests
index.ts
package.json
```
---
### 1.3 JWT Auth Middleware
**Duplication:** 3 copies in LysnrAI services + 2 in dashboards = **5 copies**
| Location | Variant |
|----------|---------|
| `platform-service/src/modules/auth/jwt.ts` | Full: create + verify (issuer service) |
| `tracker-service/src/lib/auth.ts` | Verify-only (consumer service) |
| `billing-service` (via internal key) | Different pattern |
| `admin-dashboard-web/src/lib/auth-server.ts` | create + verify + bcrypt + authenticateUser |
| `user-dashboard-web/src/lib/auth-server.ts` | create + verify + bcrypt + authenticateUser (near-identical) |
**Pattern:** All use `jose` library, HS256, same `getSecret()``TextEncoder.encode(JWT_SECRET)`. Dashboards add bcrypt password hashing and `getCurrentUser()`.
**Proposed shared package:** `@bytelyst/auth`
```
packages/auth/
src/
jwt.ts — createAccessToken, createRefreshToken, verifyToken (configurable issuer/expiry)
middleware.ts — extractAuth (Fastify), getCurrentUser (Next.js)
password.ts — hashPassword, verifyPassword (bcryptjs wrapper)
types.ts — AuthPayload, TokenPayload interfaces
index.ts
package.json
```
---
### 1.4 Fastify Server Bootstrap
**Duplication:** 4 copies in LysnrAI services
| Location | Lines |
|----------|-------|
| `platform-service/src/server.ts` | 87 |
| `billing-service/src/server.ts` | 101 |
| `growth-service/src/server.ts` | 79 |
| `tracker-service/src/server.ts` | 84 |
**Pattern:** All follow the same structure:
1. Create Fastify instance with logger
2. Register CORS (parse `CORS_ORIGIN`)
3. Register Swagger (service-specific title/description)
4. Register Prometheus metrics
5. Add `x-request-id` hook (propagate or generate UUID)
6. Add `/health` endpoint (same shape: `{ status, service, version, timestamp, requestId }`)
7. Set error handler (check `instanceof ServiceError`)
8. Register route modules with `/api` prefix
9. Start listener
**Proposed shared package:** `@bytelyst/fastify-core`
```
packages/fastify-core/
src/
create-app.ts — createServiceApp({ name, version, description }) → configured Fastify instance
request-id.ts — x-request-id hook (reusable plugin)
health.ts — health check route factory
error-handler.ts — ServiceError-aware error handler
index.ts
package.json
```
---
### 1.5 Zod Config Loader
**Duplication:** 4 copies in LysnrAI services
| Location | Shared Fields |
|----------|--------------|
| `platform-service/src/lib/config.ts` | PORT, HOST, NODE_ENV, CORS_ORIGIN, SERVICE_NAME, COSMOS_ENDPOINT, COSMOS_KEY, COSMOS_DATABASE |
| `billing-service/src/lib/config.ts` | Same base + STRIPE_*, BILLING_* |
| `growth-service/src/lib/config.ts` | Same base + STRIPE_*, WEBHOOK_* |
| `tracker-service/src/lib/config.ts` | Same base + JWT_SECRET, DEFAULT_PRODUCT_ID |
**Pattern:** Every service has 7 identical base fields (PORT, HOST, NODE_ENV, CORS_ORIGIN, SERVICE_NAME, COSMOS_ENDPOINT, COSMOS_KEY, COSMOS_DATABASE) and then service-specific extensions.
**Proposed shared package:** `@bytelyst/config`
```
packages/config/
src/
base-schema.ts — baseEnvSchema (Zod object with the 8 common fields)
loader.ts — loadConfig(extendedSchema?) merges base + service-specific
index.ts
package.json
```
---
### 1.6 Product Config / Identity
**Duplication:** 4 copies in LysnrAI services + 1 JSON canonical source
| Location | Content |
|----------|---------|
| `shared/product.json` | Canonical: `{ productId, displayName, licensePrefix, ... }` |
| `platform-service/src/lib/product-config.ts` | `PRODUCT_ID = "lysnrai"` (hardcoded) |
| `billing-service/src/lib/product-config.ts` | Same (hardcoded) |
| `growth-service/src/lib/product-config.ts` | Same (hardcoded) |
| `tracker-service/src/lib/product-config.ts` | `DEFAULT_PRODUCT_ID = env || "lysnrai"` |
**Pattern:** Every service hardcodes or reads the same product identity. Should read from a shared source.
**Proposed:** Include in `@bytelyst/config` — export `loadProductConfig()` that reads `shared/product.json` or env vars. Each consuming project provides its own `product.json`.
---
## 2. Duplicated Components — Next.js Dashboards
### 2.1 Auth Context (React)
**Duplication:** 3 copies across LysnrAI dashboards
| Location | Variant |
|----------|---------|
| `admin-dashboard-web/src/lib/auth-context.tsx` | AdminUser, localStorage keys: `admin_*` |
| `user-dashboard-web/src/lib/auth-context.tsx` | PortalUser, localStorage keys: `portal_*`, SSO cookie support |
| `tracker-dashboard-web/src/lib/` (auth context) | TrackerUser, localStorage keys: `tracker_*` |
**Pattern:** All implement: `AuthProvider`, `useAuth()`, localStorage-backed user + token storage, login/logout. Differ only in: user type, storage key prefix, and SSO support.
**Proposed shared package:** `@bytelyst/react-auth`
```
packages/react-auth/
src/
auth-context.tsx — generic AuthProvider<TUser>, configurable storage keys + login endpoint
use-auth.ts — typed useAuth() hook
types.ts — BaseUser interface, AuthConfig
index.ts
package.json
```
---
### 2.2 API Fetch Utility
**Duplication:** 4+ copies across dashboards and service clients
| Location | Pattern |
|----------|---------|
| `admin-dashboard-web/src/lib/api.ts` | `apiFetch<T>(path, options)``{ data, error }` |
| `user-dashboard-web/src/lib/platform-client.ts` | `request<T>(path, options)``T` (throws) |
| `user-dashboard-web/src/lib/billing-client.ts` | Same `request<T>` pattern |
| `user-dashboard-web/src/lib/growth-client.ts` | Same `request<T>` pattern |
| `tracker-dashboard-web/src/lib/tracker-client.ts` | `apiFetch<T>` with Bearer token |
**Pattern:** All wrap `fetch()` with: base URL, JSON headers, auth header injection, error parsing. Two variants: result-tuple `{ data, error }` vs throw-on-error.
**Proposed shared package:** `@bytelyst/api-client`
```
packages/api-client/
src/
client.ts — createApiClient({ baseUrl, getToken?, headers? }) → { fetch, safeFetch }
types.ts — ApiResult<T>, ApiError
index.ts
package.json
```
---
### 2.3 Utility Functions
**Duplication:** Identical `cn()` function in 2 dashboards
| Location |
|----------|
| `admin-dashboard-web/src/lib/utils.ts` |
| `user-dashboard-web/src/lib/utils.ts` |
**Pattern:** `cn(...inputs) → twMerge(clsx(inputs))` — standard shadcn/ui utility.
**Proposed:** Include in `@bytelyst/ui-utils` or just in `@bytelyst/react-auth` as a peer export.
---
## 3. Duplicated Components — Design System
### 3.1 Design Tokens
**Duplication:** Same color/spacing/typography values maintained across **5 formats** in MindLyst + referenced in LysnrAI dashboards
| Location | Format |
|----------|--------|
| `design-system/tokens/mindlyst.tokens.json` | Canonical JSON |
| `shared/src/.../theme/MindLystTokens.kt` | KMP Kotlin object |
| `iosApp/MindLystTheme.swift` | SwiftUI structs |
| `androidApp/.../MindLystTheme.kt` | Compose theme |
| `web/src/styles/globals.css` | CSS custom properties |
| `design-system/web/mindlyst.css` | CSS custom properties (duplicate of above) |
**Note:** LysnrAI's admin/user dashboards use TailwindCSS + shadcn/ui with a different color system currently. If both products converge on a shared design language, the token JSON can be the single source.
**Proposed shared package:** `@bytelyst/design-tokens`
```
packages/design-tokens/
tokens/
colors.json — palette, semantic dark/light, brain gradients
typography.json — font families, sizes, weights, line heights
spacing.json — 8pt grid scale
radius.json — border radius scale
motion.json — duration + easing
layout.json — breakpoints, gutter, max-width
generated/ — auto-generated from tokens JSON
tokens.css — CSS custom properties (--ml-*)
tokens.ts — TypeScript constants
tokens.kt — Kotlin object (for KMP)
tokens.swift — Swift structs (for iOS)
scripts/
generate.ts — reads JSON, outputs all formats
package.json
```
---
## 4. Potential Future Shared Components
These are not exact duplicates yet but represent patterns that will diverge if not unified:
| Component | LysnrAI | MindLyst | Shared Potential |
|-----------|---------|----------|------------------|
| **OpenAI API client** | Python `src/llm/` + Azure OpenAI | KMP `api/OpenAIClient.kt` | Shared TS client for backend services |
| **Whisper/STT client** | Python `src/audio/azure_stt.py` | KMP `api/WhisperClient.kt` | Both need speech-to-text |
| **Health check aggregator** | `services/monitoring/health-check.ts` | N/A (could reuse) | Generic multi-service health poller |
| **Docker Compose patterns** | Full stack compose | Planned | Shared compose fragments |
| **CI workflow templates** | 9 GitHub Actions | 1 CI workflow | Reusable workflow templates |
| **Python Cosmos client** | `src/cloud/cosmos_client.py` | N/A | If MindLyst adds a Python backend |
---
## 5. Recommended Package Structure
```
learning_ai_common_plat/
├── packages/
│ ├── cosmos/ — Azure Cosmos DB client singleton + container registry
│ ├── errors/ — Typed HTTP service errors (ServiceError hierarchy)
│ ├── auth/ — JWT create/verify + bcrypt + Fastify middleware
│ ├── fastify-core/ — Server bootstrap, request-id, health check, error handler
│ ├── config/ — Zod base env schema + product identity loader
│ ├── api-client/ — Typed fetch wrapper for dashboards/service clients
│ ├── react-auth/ — AuthProvider + useAuth() for Next.js dashboards
│ └── design-tokens/ — Canonical token JSON + generators for CSS/TS/Kotlin/Swift
├── docs/
│ ├── COMMON_PLATFORM_ANALYSIS.md (this file)
│ └── MIGRATION_GUIDE.md (how to adopt in each repo)
├── package.json — workspace root (npm/pnpm workspaces)
├── tsconfig.base.json — shared TypeScript config
└── README.md
```
---
## 6. Migration Priority
Ordered by **impact × ease**:
| Priority | Package | Impact | Effort | Reason |
|----------|---------|--------|--------|--------|
| **P0** | `@bytelyst/errors` | High | Low | Drop-in, no config, 6 consumers |
| **P0** | `@bytelyst/cosmos` | High | Low | 6 identical files, most-used utility |
| **P1** | `@bytelyst/config` | High | Medium | Eliminates 4 config files + 4 product-config files |
| **P1** | `@bytelyst/auth` | High | Medium | 5 copies of JWT logic, security-critical |
| **P1** | `@bytelyst/fastify-core` | High | Medium | 4 nearly identical server.ts files (~350 lines saved) |
| **P2** | `@bytelyst/api-client` | Medium | Low | 5+ fetch wrappers in dashboards |
| **P2** | `@bytelyst/react-auth` | Medium | Medium | 3 auth contexts, saves ~400 lines |
| **P3** | `@bytelyst/design-tokens` | Medium | High | Cross-platform token generation pipeline |
---
## 7. Consumption Model
### TypeScript packages (services + dashboards)
- Publish as **npm workspace packages** or use **git submodule** + `file:` references
- Each consuming repo adds `"@bytelyst/<pkg>": "file:../learning_ai_common_plat/packages/<pkg>"` to `package.json`
- Alternatively, publish to a private npm registry (GitHub Packages)
### Design tokens
- `design-tokens` package exports a CLI: `npx @bytelyst/design-tokens generate --format css,ts,kt,swift`
- Each consuming repo runs the generator as a build step or commits the generated output
### KMP (Kotlin Multiplatform)
- MindLyst's `MindLystTokens.kt` could be auto-generated from `design-tokens/tokens/*.json`
- Add a Gradle task in `shared/build.gradle.kts` that reads the JSON and emits Kotlin
---
## 8. Lines of Code Impact
| Category | Current Duplicated LOC | After Extraction |
|----------|----------------------|------------------|
| Cosmos client (6 files) | ~306 | ~50 (one shared) |
| Error classes (4 files) | ~157 | ~45 (one shared) |
| JWT auth (5 files) | ~250 | ~60 (one shared) |
| Server bootstrap (4 files) | ~351 | ~30 (per-service config only) |
| Config loader (4 files) | ~107 | ~10 (per-service extension only) |
| Product config (4 files) | ~35 | 0 (read from shared) |
| API fetch utils (5 files) | ~200 | ~30 (one shared) |
| Auth context (3 files) | ~450 | ~50 (one shared, configured per-app) |
| **Total** | **~1,856 LOC** | **~275 LOC** |
**Net savings: ~1,580 lines of duplicated code eliminated**, plus single-point maintenance for security-critical auth and database infrastructure.
---
## 9. Next Steps
1. Initialize `learning_ai_common_plat` as a pnpm/npm workspace monorepo
2. Start with P0 packages (`errors`, `cosmos`) — lowest risk, highest copy count
3. Add tests (port existing service tests that cover these utilities)
4. Update LysnrAI services to import from `@bytelyst/*` instead of local `./lib/*`
5. Wire MindLyst's Next.js web app to use shared design tokens
6. Set up CI to test the common platform packages independently

View File

@ -0,0 +1,808 @@
# 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
1. [Ecosystem Overview](#1-ecosystem-overview)
2. [Three-Repo Structure](#2-three-repo-structure)
3. [Common Platform Packages — Detailed](#3-common-platform-packages--detailed)
4. [Component & Service Inventory](#4-component--service-inventory)
5. [Dependency Graph](#5-dependency-graph)
6. [Migration Plan](#6-migration-plan)
7. [Advantages](#7-advantages)
8. [Cautions & Risks](#8-cautions--risks)
9. [Versioning Strategy](#9-versioning-strategy)
10. [Testing Strategy](#10-testing-strategy)
11. [CI/CD Impact](#11-cicd-impact)
12. [Decision Log](#12-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).
```typescript
// 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).
```typescript
// 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).
```typescript
// 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).
```typescript
// 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).
```typescript
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).
```typescript
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).
```typescript
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)
1. **Initialize `learning_ai_common_plat`** as pnpm workspace monorepo
- `pnpm init`, `pnpm-workspace.yaml`, `tsconfig.base.json`
- Shared `vitest` config, `ruff.toml` (for future Python packages)
2. **Extract `@bytelyst/errors`** (P0, 1 hour)
- Copy `platform-service/src/lib/errors.ts` as starting point
- Add `ConflictError` (from tracker) and `TooManyRequestsError` (from billing)
- Add optional `details` field to base `ServiceError`
- Write tests (port from service tests)
- Update all 4 services: `import { ServiceError, ... } from "@bytelyst/errors"`
3. **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)
4. **Extract `@bytelyst/config`** (P1, 2 hours)
- Create `baseEnvSchema` from common fields
- Add `loadConfig()` extension function
- Add `loadProductIdentity()` from `shared/product.json`
- Update all 4 services + remove 4 `product-config.ts` files
5. **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
6. **Extract `@bytelyst/fastify-core`** (P1, 3 hours)
- Create `createServiceApp()` factory
- Port request-id hook, health handler, error handler
- Refactor all 4 `server.ts` files to use factory
- Verify Docker builds still pass
### Phase 3: Dashboard Libraries (Week 3)
7. **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
8. **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
### Phase 4: Design System (Week 4+)
9. **Extract `@bytelyst/design-tokens`** (P3, 4 hours)
- Migrate `mindlyst.tokens.json` as canonical source
- Write generation script for CSS, TS, Kotlin, Swift
- Add to MindLyst build pipeline
- Consider adopting for LysnrAI dashboards (future)
---
## 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
1. **Don't extract too early** — Wait until code is stable and duplicated in ≥2 places
2. **Don't create a "utils" dumping ground** — Every package must have a clear, single responsibility
3. **Don't abstract configuration** — Each service's `config.ts` should remain in the service; only the base schema is shared
4. **Don't share React components** — UI components are product-specific; only infrastructure (auth, fetch) is shared
5. **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)
```jsonc
// 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+)
```jsonc
{
"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)
```dockerfile
# 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)
```dockerfile
# 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)
```dockerfile
# 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:
```yaml
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 to `learning_ai_common_plat`
- **Modified workflows:** Each service CI installs common-plat packages before running tests
- **Potential:** A "consumer test" job in `ci-common-plat.yml` that 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%** |

View File

@ -0,0 +1,478 @@
<mxfile host="app.diagrams.net" modified="2026-02-12T18:00:00.000Z" agent="Cascade" version="24.0.0" etag="common-plat-ecosystem" type="device">
<diagram id="main" name="Ecosystem After Refactor">
<mxGraphModel dx="3400" dy="2800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="4000" pageHeight="3200" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- TITLE -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="title" value="&lt;b&gt;ByteLyst Ecosystem — Post-Refactor Architecture&lt;/b&gt;&lt;br&gt;&lt;i&gt;3-Repo Monorepo Ecosystem with Shared Common Platform&lt;/i&gt;&lt;br&gt;v1.0 · Feb 2026" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;fontSize=22;fontFamily=Helvetica;" vertex="1" parent="1">
<mxGeometry x="900" y="-60" width="700" height="70" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- LEGEND -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="legend_box" value="" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;opacity=80;" vertex="1" parent="1">
<mxGeometry x="1780" y="-60" width="360" height="200" as="geometry" />
</mxCell>
<mxCell id="legend_title" value="&lt;b&gt;LEGEND&lt;/b&gt;" style="text;html=1;align=left;fontSize=12;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="1800" y="-50" width="80" height="20" as="geometry" />
</mxCell>
<mxCell id="leg1" value="" style="rounded=1;whiteSpace=wrap;fillColor=#FF6E6E;strokeColor=#b85450;opacity=40;" vertex="1" parent="1">
<mxGeometry x="1800" y="-20" width="18" height="18" as="geometry" />
</mxCell>
<mxCell id="leg1t" value="LysnrAI Repo (learning_voice_ai_agent)" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1826" y="-22" width="290" height="20" as="geometry" />
</mxCell>
<mxCell id="leg2" value="" style="rounded=1;whiteSpace=wrap;fillColor=#5A8CFF;strokeColor=#6c8ebf;opacity=40;" vertex="1" parent="1">
<mxGeometry x="1800" y="4" width="18" height="18" as="geometry" />
</mxCell>
<mxCell id="leg2t" value="MindLyst Repo (learning_multimodal_memory_agents)" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1826" y="2" width="310" height="20" as="geometry" />
</mxCell>
<mxCell id="leg3" value="" style="rounded=1;whiteSpace=wrap;fillColor=#34D399;strokeColor=#82b366;opacity=40;" vertex="1" parent="1">
<mxGeometry x="1800" y="28" width="18" height="18" as="geometry" />
</mxCell>
<mxCell id="leg3t" value="Common Platform (learning_ai_common_plat)" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1826" y="26" width="290" height="20" as="geometry" />
</mxCell>
<mxCell id="leg4" value="" style="rounded=1;whiteSpace=wrap;fillColor=#2EE6D6;strokeColor=#0097a7;opacity=40;" vertex="1" parent="1">
<mxGeometry x="1800" y="52" width="18" height="18" as="geometry" />
</mxCell>
<mxCell id="leg4t" value="Azure Cloud Infrastructure" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1826" y="50" width="200" height="20" as="geometry" />
</mxCell>
<mxCell id="leg5" value="" style="rounded=1;whiteSpace=wrap;fillColor=#FFD166;strokeColor=#d6b656;opacity=40;" vertex="1" parent="1">
<mxGeometry x="1800" y="76" width="18" height="18" as="geometry" />
</mxCell>
<mxCell id="leg5t" value="Client Applications (End User)" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1826" y="74" width="200" height="20" as="geometry" />
</mxCell>
<mxCell id="leg6_line" value="" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=2;dashed=1;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1800" y="110" as="sourcePoint" />
<mxPoint x="1860" y="110" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="leg6t" value="Depends on (npm/file: reference)" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1870" y="100" width="220" height="20" as="geometry" />
</mxCell>
<mxCell id="leg7_line" value="" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=2;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1800" y="132" as="sourcePoint" />
<mxPoint x="1860" y="132" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="leg7t" value="Network call (HTTP/REST)" style="text;html=1;align=left;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1870" y="122" width="200" height="20" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- LAYER 0: CLIENT APPLICATIONS (Top) -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="clients_group" value="&lt;b&gt;LAYER 0 — CLIENT APPLICATIONS&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFD166;strokeColor=#d6b656;opacity=15;verticalAlign=top;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" vertex="1" parent="1">
<mxGeometry x="40" y="40" width="2400" height="160" as="geometry" />
</mxCell>
<!-- LysnrAI Clients -->
<mxCell id="client_desktop" value="&lt;b&gt;LysnrAI Desktop&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Python 3.12 + tkinter&lt;br&gt;macOS / Windows&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="80" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="client_lysnr_ios" value="&lt;b&gt;LysnrAI iOS&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Swift + SwiftUI&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="240" y="80" width="130" height="60" as="geometry" />
</mxCell>
<mxCell id="client_lysnr_android" value="&lt;b&gt;LysnrAI Android&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Kotlin + Compose&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="390" y="80" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="client_admin" value="&lt;b&gt;Admin Dashboard&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Next.js 16 :3001&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="550" y="80" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="client_user" value="&lt;b&gt;User Portal&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Next.js 16 :3002&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="710" y="80" width="130" height="60" as="geometry" />
</mxCell>
<mxCell id="client_tracker" value="&lt;b&gt;Tracker Dashboard&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Next.js :3003&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="860" y="80" width="140" height="60" as="geometry" />
</mxCell>
<!-- MindLyst Clients -->
<mxCell id="client_ml_ios" value="&lt;b&gt;MindLyst iOS&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;SwiftUI + KMP&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="1680" y="80" width="130" height="60" as="geometry" />
</mxCell>
<mxCell id="client_ml_android" value="&lt;b&gt;MindLyst Android&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Compose + KMP&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="1830" y="80" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="client_ml_web" value="&lt;b&gt;MindLyst Web&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:10px'&gt;Next.js 14 :3050&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="1990" y="80" width="130" height="60" as="geometry" />
</mxCell>
<!-- Separator label -->
<mxCell id="sep_lysnr_clients" value="&lt;font color='#FF6E6E'&gt;&lt;b&gt;LysnrAI Clients&lt;/b&gt;&lt;/font&gt;" style="text;html=1;align=center;fontSize=10;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="380" y="155" width="120" height="20" as="geometry" />
</mxCell>
<mxCell id="sep_ml_clients" value="&lt;font color='#5A8CFF'&gt;&lt;b&gt;MindLyst Clients&lt;/b&gt;&lt;/font&gt;" style="text;html=1;align=center;fontSize=10;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="1830" y="155" width="120" height="20" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- LAYER 1: LYSNRAI REPO — SERVICES (Left) -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="lysnr_repo" value="&lt;b&gt;LAYER 1 — learning_voice_ai_agent (LysnrAI)&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FF6E6E;strokeColor=#b85450;opacity=12;verticalAlign=top;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" vertex="1" parent="1">
<mxGeometry x="40" y="240" width="960" height="620" as="geometry" />
</mxCell>
<!-- Python Backend -->
<mxCell id="python_backend" value="&lt;b&gt;FastAPI Backend&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Python 3.12 :8000&lt;br&gt;auth · config · cloud&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" vertex="1" parent="1">
<mxGeometry x="60" y="280" width="180" height="65" as="geometry" />
</mxCell>
<!-- Fastify Services -->
<mxCell id="svc_group" value="&lt;b&gt;Fastify Microservices (TypeScript)&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;dashed=1;verticalAlign=top;fontSize=11;fontStyle=1;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="60" y="370" width="920" height="200" as="geometry" />
</mxCell>
<mxCell id="svc_platform" value="&lt;b&gt;platform-service&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:4003&lt;br&gt;Auth · Audit · Flags&lt;br&gt;Notifications · Blob&lt;br&gt;Rate Limiting&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="80" y="410" width="150" height="90" as="geometry" />
</mxCell>
<mxCell id="svc_billing" value="&lt;b&gt;billing-service&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:4002&lt;br&gt;Subscriptions · Plans&lt;br&gt;Usage · Licenses&lt;br&gt;Stripe Webhooks&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="250" y="410" width="150" height="90" as="geometry" />
</mxCell>
<mxCell id="svc_growth" value="&lt;b&gt;growth-service&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:4001&lt;br&gt;Invitations&lt;br&gt;Referrals · Promos&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="420" y="410" width="140" height="90" as="geometry" />
</mxCell>
<mxCell id="svc_tracker" value="&lt;b&gt;tracker-service&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:4004&lt;br&gt;Items · Comments&lt;br&gt;Votes · Public API&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="580" y="410" width="140" height="90" as="geometry" />
</mxCell>
<!-- Service internal lib — BEFORE (crossed out) -->
<mxCell id="svc_old_lib" value="&lt;font color='#999999'&gt;&lt;s&gt;src/lib/ (per-service duplicate)&lt;/s&gt;&lt;br&gt;&lt;s&gt;cosmos.ts · errors.ts · config.ts&lt;/s&gt;&lt;br&gt;&lt;s&gt;auth.ts · product-config.ts&lt;/s&gt;&lt;/font&gt;&lt;br&gt;&lt;font color='#34D399'&gt;&lt;b&gt;→ NOW: @bytelyst/* packages&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=none;fontSize=10;align=left;" vertex="1" parent="1">
<mxGeometry x="740" y="400" width="220" height="70" as="geometry" />
</mxCell>
<!-- Dashboards internal lib — BEFORE (crossed out) -->
<mxCell id="dash_group" value="&lt;b&gt;Next.js Dashboards (TypeScript)&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#999999;dashed=1;verticalAlign=top;fontSize=11;fontStyle=1;align=left;spacingLeft=8;" vertex="1" parent="1">
<mxGeometry x="60" y="595" width="920" height="120" as="geometry" />
</mxCell>
<mxCell id="dash_admin" value="&lt;b&gt;admin-dashboard-web&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:3001 · Next.js 16&lt;br&gt;Users · Tokens · Audit&lt;br&gt;Usage · Invitations · Themes&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="80" y="630" width="180" height="70" as="geometry" />
</mxCell>
<mxCell id="dash_user" value="&lt;b&gt;user-dashboard-web&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:3002 · Next.js 16&lt;br&gt;Profile · Billing&lt;br&gt;Settings · SSO&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="280" y="630" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="dash_tracker" value="&lt;b&gt;tracker-dashboard-web&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;:3003 · Next.js&lt;br&gt;Board · Roadmap&lt;br&gt;Public Voting&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="460" y="630" width="160" height="70" as="geometry" />
</mxCell>
<mxCell id="dash_old_lib" value="&lt;font color='#999999'&gt;&lt;s&gt;src/lib/ (per-dashboard duplicate)&lt;/s&gt;&lt;br&gt;&lt;s&gt;cosmos.ts · auth-server.ts · auth-context.tsx&lt;/s&gt;&lt;br&gt;&lt;s&gt;api.ts · utils.ts&lt;/s&gt;&lt;/font&gt;&lt;br&gt;&lt;font color='#34D399'&gt;&lt;b&gt;→ NOW: @bytelyst/* packages&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=none;fontSize=10;align=left;" vertex="1" parent="1">
<mxGeometry x="640" y="630" width="320" height="70" as="geometry" />
</mxCell>
<!-- Monitoring -->
<mxCell id="monitoring" value="&lt;b&gt;Monitoring&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Loki · Grafana&lt;br&gt;Health Checks&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="260" y="280" width="130" height="65" as="geometry" />
</mxCell>
<!-- Shared JSON -->
<mxCell id="lysnr_product_json" value="&lt;b&gt;shared/product.json&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Product Identity&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="410" y="280" width="140" height="45" as="geometry" />
</mxCell>
<!-- Desktop app -->
<mxCell id="desktop_app" value="&lt;b&gt;Desktop App&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Python 3.12 · tkinter&lt;br&gt;Audio · STT · LLM · Paste&lt;br&gt;Hotkey · Cloud Sync&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="570" y="270" width="180" height="80" as="geometry" />
</mxCell>
<!-- Mobile apps -->
<mxCell id="lysnr_mobile" value="&lt;b&gt;Mobile Apps&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;iOS (Swift) + Android (Kotlin)&lt;br&gt;Fully native (no KMP)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="770" y="280" width="180" height="60" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- LAYER 1: MINDLYST REPO (Right) -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="ml_repo" value="&lt;b&gt;LAYER 1 — learning_multimodal_memory_agents (MindLyst)&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#5A8CFF;strokeColor=#6c8ebf;opacity=12;verticalAlign=top;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" vertex="1" parent="1">
<mxGeometry x="1560" y="240" width="580" height="620" as="geometry" />
</mxCell>
<!-- KMP Shared -->
<mxCell id="kmp_shared" value="&lt;b&gt;KMP Shared Module&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;commonMain/ (Kotlin)&lt;br&gt;Models · Repositories&lt;br&gt;DI · Triage Pipeline&lt;br&gt;OpenAI + Whisper Client&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1580" y="290" width="180" height="90" as="geometry" />
</mxCell>
<!-- MindLyst Design Tokens -->
<mxCell id="ml_tokens_old" value="&lt;font color='#999999'&gt;&lt;s&gt;MindLystTokens.kt&lt;/s&gt;&lt;br&gt;&lt;s&gt;MindLystTheme.swift&lt;/s&gt;&lt;br&gt;&lt;s&gt;globals.css&lt;/s&gt;&lt;/font&gt;&lt;br&gt;&lt;font color='#34D399'&gt;&lt;b&gt;→ NOW: @bytelyst/design-tokens&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=none;strokeColor=none;fontSize=10;align=left;" vertex="1" parent="1">
<mxGeometry x="1780" y="290" width="220" height="70" as="geometry" />
</mxCell>
<!-- MindLyst iOS -->
<mxCell id="ml_ios" value="&lt;b&gt;iOS App&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;SwiftUI&lt;br&gt;MindLystTheme.swift&lt;br&gt;HomeScreen · CaptureOrb&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1580" y="420" width="160" height="80" as="geometry" />
</mxCell>
<!-- MindLyst Android -->
<mxCell id="ml_android" value="&lt;b&gt;Android App&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Jetpack Compose&lt;br&gt;MindLystTheme.kt&lt;br&gt;HomeScreen · CaptureOrb&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1760" y="420" width="160" height="80" as="geometry" />
</mxCell>
<!-- MindLyst Web -->
<mxCell id="ml_web" value="&lt;b&gt;Web Dashboard&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Next.js 14 :3050&lt;br&gt;Pages Router · CSS vars&lt;br&gt;Landing · Dashboard&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1580" y="530" width="160" height="80" as="geometry" />
</mxCell>
<!-- MindLyst Design System -->
<mxCell id="ml_design_sys" value="&lt;b&gt;design-system/&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;tokens/ (JSON source)&lt;br&gt;web/ (mindlyst.css)&lt;br&gt;components/ (specs)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1760" y="530" width="160" height="80" as="geometry" />
</mxCell>
<!-- Future MindLyst services -->
<mxCell id="ml_future" value="&lt;font color='#999999'&gt;&lt;b&gt;Future: MindLyst Backend Services&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Will also use @bytelyst/cosmos,&lt;br&gt;@bytelyst/auth, @bytelyst/fastify-core&lt;/font&gt;&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#6c8ebf;dashed=1;fontSize=10;" vertex="1" parent="1">
<mxGeometry x="1580" y="650" width="340" height="55" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- LAYER 2: COMMON PLATFORM (Center) -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="common_repo" value="&lt;b&gt;LAYER 2 — learning_ai_common_plat (@bytelyst/*)&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#34D399;strokeColor=#82b366;opacity=15;verticalAlign=top;fontSize=14;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="1040" y="240" width="480" height="620" as="geometry" />
</mxCell>
<!-- P0 packages -->
<mxCell id="p0_label" value="&lt;font color='#dc2626'&gt;&lt;b&gt;P0 — Drop-in (do first)&lt;/b&gt;&lt;/font&gt;" style="text;html=1;align=left;fontSize=10;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="1060" y="270" width="180" height="18" as="geometry" />
</mxCell>
<mxCell id="pkg_errors" value="&lt;b&gt;@bytelyst/errors&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;ServiceError base class&lt;br&gt;NotFound · BadRequest&lt;br&gt;Unauthorized · Forbidden&lt;br&gt;Conflict · TooManyRequests&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1060" y="295" width="200" height="80" as="geometry" />
</mxCell>
<mxCell id="pkg_cosmos" value="&lt;b&gt;@bytelyst/cosmos&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;CosmosClient singleton&lt;br&gt;getContainer(name)&lt;br&gt;Container registry + TTL&lt;br&gt;initializeAllContainers()&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1280" y="295" width="200" height="80" as="geometry" />
</mxCell>
<!-- P1 packages -->
<mxCell id="p1_label" value="&lt;font color='#ea580c'&gt;&lt;b&gt;P1 — High impact&lt;/b&gt;&lt;/font&gt;" style="text;html=1;align=left;fontSize=10;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="1060" y="390" width="180" height="18" as="geometry" />
</mxCell>
<mxCell id="pkg_config" value="&lt;b&gt;@bytelyst/config&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Zod base env schema&lt;br&gt;(PORT, HOST, NODE_ENV,&lt;br&gt;COSMOS_*, SERVICE_NAME)&lt;br&gt;Product identity loader&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1060" y="415" width="200" height="80" as="geometry" />
</mxCell>
<mxCell id="pkg_auth" value="&lt;b&gt;@bytelyst/auth&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;JWT create/verify (jose)&lt;br&gt;Fastify extractAuth hook&lt;br&gt;Next.js getCurrentUser&lt;br&gt;bcrypt password hashing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1280" y="415" width="200" height="80" as="geometry" />
</mxCell>
<mxCell id="pkg_fastify" value="&lt;b&gt;@bytelyst/fastify-core&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;createServiceApp() factory&lt;br&gt;CORS · Swagger · Metrics&lt;br&gt;x-request-id hook&lt;br&gt;/health · Error handler&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1060" y="510" width="200" height="80" as="geometry" />
</mxCell>
<!-- P2 packages -->
<mxCell id="p2_label" value="&lt;font color='#ca8a04'&gt;&lt;b&gt;P2 — Medium impact&lt;/b&gt;&lt;/font&gt;" style="text;html=1;align=left;fontSize=10;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="1280" y="510" width="180" height="18" as="geometry" />
</mxCell>
<mxCell id="pkg_api_client" value="&lt;b&gt;@bytelyst/api-client&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;createApiClient() factory&lt;br&gt;Typed fetch + auth headers&lt;br&gt;{ data, error } or throw&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1280" y="535" width="200" height="70" as="geometry" />
</mxCell>
<mxCell id="pkg_react_auth" value="&lt;b&gt;@bytelyst/react-auth&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;AuthProvider&amp;lt;TUser&amp;gt;&lt;br&gt;useAuth() hook&lt;br&gt;localStorage token mgmt&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1060" y="610" width="200" height="70" as="geometry" />
</mxCell>
<!-- P3 package -->
<mxCell id="p3_label" value="&lt;font color='#4f46e5'&gt;&lt;b&gt;P3 — Cross-platform&lt;/b&gt;&lt;/font&gt;" style="text;html=1;align=left;fontSize=10;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="1280" y="620" width="180" height="18" as="geometry" />
</mxCell>
<mxCell id="pkg_tokens" value="&lt;b&gt;@bytelyst/design-tokens&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Canonical JSON source&lt;br&gt;→ CSS vars (--ml-*)&lt;br&gt;→ TypeScript constants&lt;br&gt;→ Kotlin object&lt;br&gt;→ Swift structs&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1280" y="645" width="200" height="85" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- LAYER 3: AZURE INFRASTRUCTURE (Bottom) -->
<!-- ═══════════════════════════════════════════════════════════ -->
<mxCell id="azure_layer" value="&lt;b&gt;LAYER 3 — Azure Cloud Infrastructure&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#2EE6D6;strokeColor=#0097a7;opacity=12;verticalAlign=top;fontSize=13;fontStyle=1;align=center;" vertex="1" parent="1">
<mxGeometry x="40" y="920" width="2100" height="180" as="geometry" />
</mxCell>
<mxCell id="az_cosmos" value="&lt;b&gt;Azure Cosmos DB&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;cosmos-mywisprai&lt;br&gt;Serverless · NoSQL&lt;br&gt;DB: lysnrai&lt;br&gt;13+ containers&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="80" y="960" width="170" height="90" as="geometry" />
</mxCell>
<mxCell id="az_blob" value="&lt;b&gt;Azure Blob Storage&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;bytelystblobs&lt;br&gt;6 containers: audio,&lt;br&gt;transcripts, attachments,&lt;br&gt;avatars, releases, backups&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="280" y="960" width="180" height="90" as="geometry" />
</mxCell>
<mxCell id="az_keyvault" value="&lt;b&gt;Azure Key Vault&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;kv-mywisprai&lt;br&gt;Secrets management&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="490" y="960" width="150" height="70" as="geometry" />
</mxCell>
<mxCell id="az_speech" value="&lt;b&gt;Azure Speech&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;mywisprai-speech&lt;br&gt;STT (F0 tier)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="670" y="960" width="140" height="70" as="geometry" />
</mxCell>
<mxCell id="az_openai" value="&lt;b&gt;Azure OpenAI&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;mywisprai-openai-sweden&lt;br&gt;swedencentral · S0&lt;br&gt;gpt-4o-mini&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="840" y="960" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="az_stripe" value="&lt;b&gt;Stripe&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Payments · Webhooks&lt;br&gt;Subscriptions&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1040" y="960" width="140" height="70" as="geometry" />
</mxCell>
<mxCell id="az_openai_public" value="&lt;b&gt;OpenAI API&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;api.openai.com&lt;br&gt;gpt-4o-mini · Whisper&lt;br&gt;(MindLyst triage)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1210" y="960" width="150" height="80" as="geometry" />
</mxCell>
<mxCell id="az_docker" value="&lt;b&gt;Docker Compose&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;Traefik · Loki · Grafana&lt;br&gt;All services&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1390" y="960" width="150" height="70" as="geometry" />
</mxCell>
<mxCell id="az_gh_actions" value="&lt;b&gt;GitHub Actions&lt;/b&gt;&lt;br&gt;&lt;font style='font-size:9px'&gt;CI pipelines&lt;br&gt;9 workflows (LysnrAI)&lt;br&gt;1 workflow (MindLyst)&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f7fa;strokeColor=#0097a7;fontSize=10;shadow=1;" vertex="1" parent="1">
<mxGeometry x="1570" y="960" width="160" height="80" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- DEPENDENCY ARROWS: Services → Common Platform -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- Platform service → errors -->
<mxCell id="arr_plat_err" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=2;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="svc_platform" target="pkg_errors">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Billing service → errors -->
<mxCell id="arr_bill_err" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=0;entryY=0.7;entryDx=0;entryDy=0;" edge="1" parent="1" source="svc_billing" target="pkg_errors">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Growth service → fastify-core -->
<mxCell id="arr_grow_fc" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.3;entryDx=0;entryDy=0;" edge="1" parent="1" source="svc_growth" target="pkg_fastify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Tracker service → auth -->
<mxCell id="arr_trk_auth" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="svc_tracker" target="pkg_auth">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Admin dashboard → cosmos -->
<mxCell id="arr_admin_cos" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=0;entryY=0.9;entryDx=0;entryDy=0;" edge="1" parent="1" source="dash_admin" target="pkg_cosmos">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- User dashboard → react-auth -->
<mxCell id="arr_user_ra" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="dash_user" target="pkg_react_auth">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Tracker dashboard → api-client -->
<mxCell id="arr_trkd_api" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="dash_tracker" target="pkg_api_client">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- DEPENDENCY ARROWS: MindLyst → Common Platform -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- MindLyst web → design-tokens -->
<mxCell id="arr_mlweb_tok" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=2;dashed=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="ml_web" target="pkg_tokens">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- MindLyst design-system → design-tokens -->
<mxCell id="arr_mlds_tok" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;exitX=0;exitY=0.7;exitDx=0;exitDy=0;entryX=1;entryY=0.8;entryDx=0;entryDy=0;" edge="1" parent="1" source="ml_design_sys" target="pkg_tokens">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- MindLyst future services → fastify-core -->
<mxCell id="arr_mlfut_fc" style="endArrow=classic;html=1;strokeColor=#34D399;strokeWidth=1.5;dashed=1;dashPattern=8 4;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.7;entryDx=0;entryDy=0;" edge="1" parent="1" source="ml_future" target="pkg_fastify">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- NETWORK ARROWS: Services → Azure -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- Services → Cosmos -->
<mxCell id="arr_svc_cosmos" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1.5;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="svc_group" target="az_cosmos">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Desktop → Azure Speech -->
<mxCell id="arr_desk_speech" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="desktop_app" target="az_speech">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Dashboards → Cosmos -->
<mxCell id="arr_dash_cosmos" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1.5;exitX=0.3;exitY=1;exitDx=0;exitDy=0;entryX=0.7;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="dash_group" target="az_cosmos">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Billing → Stripe -->
<mxCell id="arr_bill_stripe" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="svc_billing" target="az_stripe">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- KMP → OpenAI public -->
<mxCell id="arr_kmp_openai" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="kmp_shared" target="az_openai_public">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- CLIENT → SERVICE ARROWS -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- Desktop client → Desktop app -->
<mxCell id="arr_c_desk" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.3;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="client_desktop" target="desktop_app">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- Admin client → admin dashboard -->
<mxCell id="arr_c_admin" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="client_admin" target="dash_admin">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- User client → user dashboard -->
<mxCell id="arr_c_user" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="client_user" target="dash_user">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- MindLyst clients → KMP -->
<mxCell id="arr_c_ml_ios" style="endArrow=classic;html=1;strokeColor=#666666;strokeWidth=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.3;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="client_ml_ios" target="kmp_shared">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- INTRA-PACKAGE DEPENDENCY ARROWS (within common plat) -->
<!-- ═══════════════════════════════════════════════════════════ -->
<!-- fastify-core → errors -->
<mxCell id="arr_fc_err" style="endArrow=classic;html=1;strokeColor=#22c55e;strokeWidth=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="pkg_fastify" target="pkg_errors">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- fastify-core → config -->
<mxCell id="arr_fc_cfg" style="endArrow=classic;html=1;strokeColor=#22c55e;strokeWidth=1;exitX=0.3;exitY=0;exitDx=0;exitDy=0;entryX=0.3;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="pkg_fastify" target="pkg_config">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- auth → config -->
<mxCell id="arr_auth_cfg" style="endArrow=classic;html=1;strokeColor=#22c55e;strokeWidth=1;exitX=0.3;exitY=0;exitDx=0;exitDy=0;entryX=0.7;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="pkg_auth" target="pkg_config">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- api-client → auth -->
<mxCell id="arr_apic_auth" style="endArrow=classic;html=1;strokeColor=#22c55e;strokeWidth=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="pkg_api_client" target="pkg_auth">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<!-- react-auth → api-client -->
<mxCell id="arr_ra_apic" style="endArrow=classic;html=1;strokeColor=#22c55e;strokeWidth=1;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=0;entryY=0.7;entryDx=0;entryDy=0;" edge="1" parent="1" source="pkg_react_auth" target="pkg_api_client">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>