488 lines
14 KiB
Markdown
488 lines
14 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// @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
|
|
|
|
```typescript
|
|
// @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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
## Related Skills
|
|
|
|
- [Local Development Setup](./local-development.md) - Running the architecture
|
|
- [Production Readiness](./production-readiness.md) - Validating the architecture
|
|
- [Debug Service](./debug-service.md) - Fixing architectural issues
|