learning_ai_common_plat/AI.dev/SKILLS/architecture-patterns.md

14 KiB

Architecture Patterns Skill

Description: Common architectural patterns and structures used across the projects.

When to Use

  • Designing new services or features
  • Onboarding to the codebase
  • Making architectural decisions
  • Understanding system design

Core Architectures

1. Microservices Architecture

┌─────────────────┐    ┌─────────────────┐
│   Frontend      │    │   Frontend      │
│   (Next.js)     │    │   (Next.js)     │
└─────────┬───────┘    └─────────┬───────┘
          │                      │
          └──────────┬───────────┘
                     │
          ┌─────────────────────┐
          │   API Gateway       │
          │   (Traefik)         │
          └─────────┬───────────┘
                    │
    ┌───────────────┼───────────────┐
    │               │               │
┌───▼───┐    ┌─────▼─────┐    ┌───▼───┐
│Billing│    │ Platform  │    │Growth │
│Service│    │ Service   │    │Service│
└───────┘    └───────────┘    └───────┘

Key Principles:

  • Single responsibility per service
  • Independent deployment
  • Shared data through Cosmos DB
  • JWT-based authentication via platform service
  • Product-agnostic design (productId field)

2. Monorepo Structure

learning_voice_ai_agent/          # Product repo
├── src/                          # Desktop app (Python)
├── backend/                      # FastAPI backend
├── admin-dashboard-web/          # Next.js admin UI
├── user-dashboard-web/           # Next.js user portal
├── tracker-dashboard-web/        # Next.js tracker UI
└── mobile_app/                   # Native mobile apps

learning_ai_common_plat/          # Shared platform
├── packages/                     # @bytelyst/* libraries
│   ├── errors/                   # Error types
│   ├── cosmos/                   # DB client
│   ├── auth/                     # JWT utils
│   ├── config/                   # Config loader
│   ├── api-client/               # Fetch wrapper
│   └── design-tokens/            # Design system
└── services/                     # @lysnrai/* microservices
    ├── billing-service/
    ├── growth-service/
    ├── platform-service/
    └── tracker-service/

3. Mobile Architecture (MindLyst)

┌─────────────────────────────────────┐
│           KMP Shared Module        │
│  (Business Logic, Repositories)    │
└──────────────┬──────────────────────┘
               │
    ┌──────────┴──────────┐
    │                     │
┌───▼────┐        ┌──────▼────┐
│ Android│        │    iOS    │
│(Compose)│       │ (SwiftUI) │
└────────┘        └───────────┘

Service Patterns

Fastify Service Template

// src/server.ts
import Fastify from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';

const server = Fastify({
  logger: true,
}).withTypeProvider<TypeBoxTypeProvider>();

// Register plugins
await server.register(import('@fastify/cors'));
await server.register(import('./lib/auth'));

// Register modules
await server.register(import('./modules/auth/routes'), { prefix: '/api/auth' });
await server.register(import('./modules/users/routes'), { prefix: '/api/users' });

// Health check
server.get('/health', async (request, reply) => {
  return {
    status: 'ok',
    service: 'billing-service',
    requestId: request.id,
  };
});

// Start server
try {
  await server.listen({ port: 4002, host: '0.0.0.0' });
} catch (err) {
  server.log.error(err);
  process.exit(1);
}

Module Pattern (types → repository → routes)

// modules/auth/types.ts
import { Type } from '@sinclair/typebox';

export const LoginSchema = Type.Object({
  email: Type.String({ format: 'email' }),
  password: Type.String({ minLength: 8 }),
});

export const AuthResponseSchema = Type.Object({
  token: Type.String(),
  user: Type.Object({
    id: Type.String(),
    email: Type.String(),
  }),
});

// modules/auth/repository.ts
export class AuthRepository {
  constructor(private client: CosmosClient) {}

  async validateUser(email: string, password: string): Promise<User | null> {
    // Database logic
  }

  async createToken(userId: string): Promise<string> {
    // JWT creation
  }
}

// modules/auth/routes.ts
import { AuthRepository } from './repository';
import { LoginSchema, AuthResponseSchema } from './types';

export default async function authRoutes(server: FastifyInstance) {
  const repo = new AuthRepository(server.cosmosClient);

  server.post(
    '/login',
    {
      schema: {
        body: LoginSchema,
        response: { 200: AuthResponseSchema },
      },
    },
    async (request, reply) => {
      const { email, password } = request.body;
      const user = await repo.validateUser(email, password);

      if (!user) {
        reply.code(401);
        return { error: 'Invalid credentials' };
      }

      const token = await repo.createToken(user.id);
      return { token, user };
    }
  );
}

Frontend Patterns

Next.js Dashboard Structure

src/app/
├── (dashboard)/           # Layout group
│   ├── layout.tsx         # Dashboard layout with sidebar
│   ├── page.tsx           # Dashboard home
│   ├── users/
│   │   ├── page.tsx       # Users list
│   │   └── [id]/
│   │       └── page.tsx   # User detail
│   └── settings/
│       └── page.tsx       # Settings page
├── api/                   # API routes
│   ├── auth/
│   │   └── route.ts       # /api/auth
│   └── users/
│       └── route.ts       # /api/users
├── lib/                   # Utilities
│   ├── cosmos.ts          # @bytelyst/cosmos wrapper
│   ├── auth-server.ts     # @bytelyst/auth wrapper
│   └── api-client.ts      # Fetch wrapper
└── components/            # Reusable components
    ├── ui/                # Base UI components
    └── forms/             # Form components

Service Client Pattern

// lib/billing-client.ts
import { createApiClient } from '@bytelyst/api-client';

const api = createApiClient({
  baseUrl: process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003',
  getToken: () => localStorage.getItem('token') || undefined,
});

export const billingClient = {
  // Subscriptions
  getSubscriptions: () => api.fetch<Subscription[]>('/api/subscriptions'),
  createSubscription: (data: CreateSubscriptionDto) =>
    api.fetch<Subscription>('/api/subscriptions', {
      method: 'POST',
      body: JSON.stringify(data),
    }),

  // Usage
  getUsage: (userId: string) => api.fetch<Usage>(`/api/usage/${userId}`),
};

Data Patterns

Cosmos DB Document Structure

// Base document interface
interface BaseDocument {
  id: string;
  productId: string; // REQUIRED for multi-tenancy
  createdAt: string;
  updatedAt: string;
  _etag?: string;
  _ts?: number;
}

// Example: User document
interface UserDocument extends BaseDocument {
  email: string;
  name: string;
  role: 'admin' | 'user' | 'viewer';
  permissions?: string[];
}

// Example: Tracker item document
interface TrackerItemDocument extends BaseDocument {
  title: string;
  description: string;
  type: 'feature' | 'bug' | 'task';
  status: 'planned' | 'in-progress' | 'completed';
  visibility: 'public' | 'internal';
  voteCount: number;
}

Repository Pattern

// repositories/base.ts
export abstract class BaseRepository<T extends BaseDocument> {
  constructor(
    protected client: CosmosClient,
    protected database: string,
    protected container: string
  ) {}

  protected get container() {
    return this.client.database(this.database).container(this.container);
  }

  async create(item: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> {
    const document: T = {
      ...item,
      id: crypto.randomUUID(),
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    const { resource } = await this.container.items.create(document);
    return resource!;
  }

  async getById(id: string, productId: string): Promise<T | null> {
    const { resource } = await this.container.item(id, productId).read<T>();

    return resource || null;
  }

  async update(id: string, productId: string, updates: Partial<T>): Promise<T> {
    const { resource } = await this.container.item(id, productId).replace<T>({
      ...updates,
      id,
      productId,
      updatedAt: new Date().toISOString(),
    } as T);

    return resource!;
  }
}

Authentication Patterns

JWT Flow

┌─────────┐  Login  ┌─────────────┐  Validate  ┌─────────────┐
│ Frontend │───────▶│ Platform    │────────────▶│ Other       │
│         │        │ Service     │            │ Services    │
└─────────┘◀───────└─────────────┘            └─────────────┘
     Token         JWT Issuer                   JWT Validation

Implementation

// Platform service - JWT issuance
export async function loginRoute(server: FastifyInstance) {
  server.post('/login', async (request, reply) => {
    const { email, password } = request.body;

    // Validate credentials
    const user = await validateUser(email, password);
    if (!user) return reply.code(401).send({ error: 'Invalid credentials' });

    // Issue JWT
    const token = await jwt.sign(
      { sub: user.id, email: user.email, role: user.role },
      process.env.JWT_SECRET!,
      { expiresIn: '24h' }
    );

    return { token, user };
  });
}

// Other services - JWT validation
export async function authMiddleware(server: FastifyInstance) {
  server.addHook('onRequest', async (request, reply) => {
    const token = request.headers.authorization?.replace('Bearer ', '');

    if (!token) {
      reply.code(401).send({ error: 'No token provided' });
      return;
    }

    try {
      const payload = await jwt.verify(token, process.env.JWT_SECRET!);
      request.user = payload;
    } catch {
      reply.code(401).send({ error: 'Invalid token' });
    }
  });
}

Configuration Patterns

Environment-based Configuration

// lib/config.ts
import { z } from 'zod';

const configSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(4002),
  COSMOS_ENDPOINT: z.string(),
  COSMOS_KEY: z.string(),
  COSMOS_DATABASE: z.string().default('lysnrai'),
  JWT_SECRET: z.string(),
  AZURE_BLOB_CONNECTION_STRING: z.string().optional(),
});

export const config = configSchema.parse(process.env);

Shared Package Configuration

// @bytelyst/config
export function loadProductIdentity() {
  const productId = process.env.PRODUCT_ID || 'lysnrai';
  const env = process.env.NODE_ENV || 'development';

  return {
    productId,
    env,
    isProduction: env === 'production',
    isDevelopment: env === 'development',
  };
}

Error Handling Patterns

Standardized Error Types

// @bytelyst/errors
export class BadRequestError extends Error {
  constructor(
    message: string,
    public details?: any
  ) {
    super(message);
    this.name = 'BadRequestError';
  }
}

export class UnauthorizedError extends Error {
  constructor(message = 'Unauthorized') {
    super(message);
    this.name = 'UnauthorizedError';
  }
}

export class NotFoundError extends Error {
  constructor(resource: string) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
  }
}

Global Error Handler

// Error handler for Fastify
server.setErrorHandler((error, request, reply) => {
  request.log.error(error);

  if (error instanceof BadRequestError) {
    reply.code(400);
    return { error: error.message, details: error.details };
  }

  if (error instanceof UnauthorizedError) {
    reply.code(401);
    return { error: error.message };
  }

  if (error instanceof NotFoundError) {
    reply.code(404);
    return { error: error.message };
  }

  // Default
  reply.code(500);
  return { error: 'Internal server error' };
});

Notes

  • Consistency is key - Follow established patterns
  • Product-agnostic design - Always include productId
  • Shared packages first - Don't duplicate code
  • Document decisions - Use ADRs for major changes
  • Evolve patterns - Improve but maintain consistency