SAAS Platform Development Guide¶
This guide provides comprehensive information for developers working on the SAAS Platform, including architecture, development practices, and deployment procedures.
Development Environment Setup¶
Prerequisites¶
- Node.js (18.0 or higher)
- npm or yarn package manager
- PostgreSQL (14.0 or higher)
- Redis (6.0 or higher)
- Docker and Docker Compose
- Git for version control
Local Development Setup¶
- Clone Repository
- Install Dependencies
# Backend dependencies
cd backend
npm install
# Frontend dependencies
cd ../frontend
npm install
# Return to root
cd ..
- Environment Configuration
# Copy environment templates
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
# Configure environment variables
# Edit .env files with your local settings
- Database Setup
# Start PostgreSQL and Redis with Docker
docker-compose up -d postgres redis
# Run database migrations
cd backend
npm run migrate
# Seed with development data
npm run seed
- Start Development Servers
# Terminal 1: Backend API
cd backend
npm run dev
# Terminal 2: Frontend
cd frontend
npm run dev
# Terminal 3: Background jobs (optional)
cd backend
npm run worker
Project Structure¶
saas-platform/
├── backend/ # Node.js API server
│ ├── src/
│ │ ├── controllers/ # Request handlers
│ │ ├── services/ # Business logic
│ │ ├── models/ # Database models
│ │ ├── middleware/ # Express middleware
│ │ ├── routes/ # API routes
│ │ ├── utils/ # Utility functions
│ │ └── types/ # TypeScript types
│ ├── migrations/ # Database migrations
│ ├── seeds/ # Development data
│ └── tests/ # Backend tests
├── frontend/ # React application
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── pages/ # Page components
│ │ ├── hooks/ # Custom hooks
│ │ ├── services/ # API client
│ │ ├── store/ # Redux store
│ │ ├── utils/ # Frontend utilities
│ │ └── types/ # TypeScript types
│ ├── public/ # Static assets
│ └── tests/ # Frontend tests
├── shared/ # Shared code and types
├── infrastructure/ # Infrastructure as code
├── docs/ # Technical documentation
└── scripts/ # Build and deployment scripts
Architecture Overview¶
Multi-Tenant Architecture¶
// Tenant isolation strategy
interface TenantContext {
tenantId: string;
schema: string;
features: Feature[];
limits: ResourceLimits;
}
// Middleware for tenant resolution
export const resolveTenant = async (
req: Request,
res: Response,
next: NextFunction
) => {
const tenantId = extractTenantId(req);
const tenant = await getTenantById(tenantId);
if (!tenant) {
return res.status(404).json({ error: "Tenant not found" });
}
req.tenant = tenant;
next();
};
Database Schema Design¶
-- Multi-tenant schema design
CREATE SCHEMA tenant_core;
CREATE SCHEMA tenant_data;
-- Core tables (shared across tenants)
CREATE TABLE tenant_core.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
domain VARCHAR(255) UNIQUE,
schema_name VARCHAR(63) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
settings JSONB DEFAULT '{}'
);
-- Tenant-specific tables (per tenant schema)
CREATE TABLE tenant_data.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenant_core.tenants(id),
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT NOW()
);
API Design Patterns¶
RESTful API Structure¶
// User controller example
export class UserController {
async getUsers(req: AuthenticatedRequest, res: Response) {
try {
const { tenant } = req;
const { page = 1, limit = 20, search } = req.query;
const users = await this.userService.getUsers(tenant.id, {
page: Number(page),
limit: Number(limit),
search: search as string,
});
res.json({
data: users.data,
pagination: users.pagination,
});
} catch (error) {
this.handleError(error, res);
}
}
async createUser(req: AuthenticatedRequest, res: Response) {
try {
const { tenant } = req;
const userData = this.validateUserData(req.body);
const user = await this.userService.createUser(tenant.id, userData);
res.status(201).json({ data: user });
} catch (error) {
this.handleError(error, res);
}
}
}
GraphQL Implementation¶
// GraphQL resolver example
export const userResolvers = {
Query: {
users: async (
_: any,
args: { filter?: UserFilter; pagination?: Pagination },
context: AuthContext
) => {
await requirePermission(context, "read:users");
return userService.getUsers(context.tenant.id, args);
},
},
Mutation: {
createUser: async (
_: any,
args: { input: CreateUserInput },
context: AuthContext
) => {
await requirePermission(context, "create:users");
return userService.createUser(context.tenant.id, args.input);
},
},
};
Frontend Development¶
Component Architecture¶
// Feature-based component structure
interface UserListProps {
tenantId: string;
filters?: UserFilters;
onUserSelect?: (user: User) => void;
}
export const UserList: React.FC<UserListProps> = ({
tenantId,
filters,
onUserSelect,
}) => {
const { data: users, isLoading, error } = useUsers(tenantId, filters);
const { mutate: deleteUser } = useDeleteUser();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return (
<div className="user-list">
{users?.map((user) => (
<UserCard
key={user.id}
user={user}
onSelect={onUserSelect}
onDelete={() => deleteUser(user.id)}
/>
))}
</div>
);
};
State Management¶
// Redux store configuration
export interface AppState {
auth: AuthState;
tenant: TenantState;
users: UsersState;
ui: UIState;
}
// Tenant slice
export const tenantSlice = createSlice({
name: "tenant",
initialState: {
current: null as Tenant | null,
settings: {},
features: [],
loading: false,
},
reducers: {
setCurrentTenant: (state, action) => {
state.current = action.payload;
},
updateTenantSettings: (state, action) => {
if (state.current) {
state.current.settings = {
...state.current.settings,
...action.payload,
};
}
},
},
});
Custom Hooks¶
// Custom hook for API data fetching
export const useUsers = (tenantId: string, filters?: UserFilters) => {
return useQuery(
["users", tenantId, filters],
() => userApi.getUsers(tenantId, filters),
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
}
);
};
// Custom hook for mutations
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation((data: CreateUserData) => userApi.createUser(data), {
onSuccess: (newUser) => {
queryClient.invalidateQueries(["users"]);
toast.success("User created successfully");
},
onError: (error) => {
toast.error("Failed to create user");
console.error("User creation error:", error);
},
});
};
Authentication & Authorization¶
JWT Implementation¶
// JWT service
export class AuthService {
private readonly jwtSecret = process.env.JWT_SECRET!;
generateToken(user: User, tenant: Tenant): string {
const payload = {
userId: user.id,
tenantId: tenant.id,
email: user.email,
role: user.role,
permissions: this.getUserPermissions(user, tenant),
};
return jwt.sign(payload, this.jwtSecret, {
expiresIn: "24h",
issuer: "saas-platform",
audience: tenant.domain,
});
}
verifyToken(token: string): TokenPayload {
try {
return jwt.verify(token, this.jwtSecret) as TokenPayload;
} catch (error) {
throw new AuthenticationError("Invalid token");
}
}
}
Permission System¶
// Permission definitions
export enum Permission {
READ_USERS = "read:users",
CREATE_USERS = "create:users",
UPDATE_USERS = "update:users",
DELETE_USERS = "delete:users",
MANAGE_TENANT = "manage:tenant",
VIEW_ANALYTICS = "view:analytics",
}
// Role-based permissions
export const ROLE_PERMISSIONS: Record<string, Permission[]> = {
admin: Object.values(Permission),
manager: [
Permission.READ_USERS,
Permission.CREATE_USERS,
Permission.UPDATE_USERS,
Permission.VIEW_ANALYTICS,
],
user: [Permission.READ_USERS],
};
// Permission middleware
export const requirePermission = (permission: Permission) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
};
Testing Strategy¶
Backend Testing¶
// Unit test example
describe("UserService", () => {
let userService: UserService;
let mockRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepository = createMockUserRepository();
userService = new UserService(mockRepository);
});
describe("createUser", () => {
it("should create a user with valid data", async () => {
const userData = {
email: "test@example.com",
name: "Test User",
role: "user",
};
const expectedUser = { id: "123", ...userData };
mockRepository.create.mockResolvedValue(expectedUser);
const result = await userService.createUser("tenant-1", userData);
expect(result).toEqual(expectedUser);
expect(mockRepository.create).toHaveBeenCalledWith("tenant-1", userData);
});
});
});
// Integration test example
describe("User API", () => {
let app: Application;
let tenantId: string;
let authToken: string;
beforeAll(async () => {
app = await createTestApp();
const { tenant, token } = await setupTestTenant();
tenantId = tenant.id;
authToken = token;
});
describe("POST /api/users", () => {
it("should create a new user", async () => {
const userData = {
email: "newuser@example.com",
name: "New User",
role: "user",
};
const response = await request(app)
.post("/api/users")
.set("Authorization", `Bearer ${authToken}`)
.send(userData)
.expect(201);
expect(response.body.data).toMatchObject(userData);
});
});
});
Frontend Testing¶
// Component test example
import { render, screen, fireEvent } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "react-query";
import { UserList } from "../UserList";
describe("UserList", () => {
const mockUsers = [
{ id: "1", name: "John Doe", email: "john@example.com" },
{ id: "2", name: "Jane Smith", email: "jane@example.com" },
];
const renderWithProviders = (component: React.ReactElement) => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
{component}
</QueryClientProvider>
);
};
it("should render user list", async () => {
jest.spyOn(userApi, "getUsers").mockResolvedValue(mockUsers);
renderWithProviders(<UserList tenantId="tenant-1" />);
expect(await screen.findByText("John Doe")).toBeInTheDocument();
expect(await screen.findByText("Jane Smith")).toBeInTheDocument();
});
});
Performance Optimization¶
Database Optimization¶
// Query optimization
export class UserRepository {
async getUsersWithPagination(
tenantId: string,
options: PaginationOptions
): Promise<PaginatedResult<User>> {
const { page, limit, search } = options;
const offset = (page - 1) * limit;
// Optimized query with proper indexing
const query = this.db
.select("*")
.from("users")
.where("tenant_id", tenantId)
.andWhere((builder) => {
if (search) {
builder
.where("name", "ilike", `%${search}%`)
.orWhere("email", "ilike", `%${search}%`);
}
})
.orderBy("created_at", "desc")
.limit(limit)
.offset(offset);
const [users, totalCount] = await Promise.all([
query,
this.getUserCount(tenantId, search),
]);
return {
data: users,
pagination: {
page,
limit,
total: totalCount,
pages: Math.ceil(totalCount / limit),
},
};
}
}
Caching Strategy¶
// Redis caching implementation
export class CacheService {
private redis: Redis;
constructor() {
this.redis = new Redis(process.env.REDIS_URL);
}
async get<T>(key: string): Promise<T | null> {
const cached = await this.redis.get(key);
return cached ? JSON.parse(cached) : null;
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
await this.redis.setex(key, ttl, JSON.stringify(value));
}
async invalidate(pattern: string): Promise<void> {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}
// Cache decorator
export const cached = (ttl: number = 3600) => {
return (
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) => {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cacheKey = `${
target.constructor.name
}:${propertyName}:${JSON.stringify(args)}`;
let result = await this.cacheService.get(cacheKey);
if (!result) {
result = await method.apply(this, args);
await this.cacheService.set(cacheKey, result, ttl);
}
return result;
};
};
};
Deployment¶
Docker Configuration¶
# Backend Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS runtime
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
ENV NODE_ENV production
CMD ["npm", "start"]
Kubernetes Deployment¶
# Kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: saas-backend
spec:
replicas: 3
selector:
matchLabels:
app: saas-backend
template:
metadata:
labels:
app: saas-backend
spec:
containers:
- name: backend
image: optim/saas-backend:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: redis-secret
key: url
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Monitoring and Observability¶
Logging¶
Pour la journalisation et la gestion des erreurs, nous utilisons Sentry pour capturer et tracker les erreurs côté API (FastAPI) et frontend (React + TypeScript).
📖 Consultez le guide complet de journalisation et Sentry
Ce guide détaillé couvre :
- Configuration FastAPI avec Sentry SDK
- Intégration React + TypeScript avec Sentry
- Upload des sourcemaps et configuration des releases
- Exemples pratiques et bonnes pratiques
- Gestion des variables d'environnement
- Checklist de déploiement
Metrics¶
// Prometheus metrics
import client from "prom-client";
export const metrics = {
httpRequestDuration: new client.Histogram({
name: "http_request_duration_seconds",
help: "HTTP request duration in seconds",
labelNames: ["method", "route", "status_code", "tenant_id"],
}),
activeConnections: new client.Gauge({
name: "active_connections_total",
help: "Total number of active connections",
}),
databaseConnections: new client.Gauge({
name: "database_connections_active",
help: "Number of active database connections",
}),
};
// Metrics middleware
export const metricsMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
const start = Date.now();
res.on("finish", () => {
const duration = (Date.now() - start) / 1000;
metrics.httpRequestDuration.observe(
{
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode.toString(),
tenant_id: req.tenant?.id || "unknown",
},
duration
);
});
next();
};
Security Best Practices¶
Input Validation¶
// Request validation with Joi
import Joi from "joi";
export const userSchema = Joi.object({
email: Joi.string().email().required(),
name: Joi.string().min(2).max(100).required(),
role: Joi.string().valid("admin", "manager", "user").default("user"),
permissions: Joi.array().items(Joi.string()).optional(),
});
export const validateRequest = (schema: Joi.ObjectSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: "Validation failed",
details: error.details.map((d) => d.message),
});
}
req.body = value;
next();
};
};
Rate Limiting¶
// Rate limiting implementation
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
export const createRateLimiter = (
windowMs: number,
max: number,
keyGenerator?: (req: Request) => string
) => {
return rateLimit({
store: new RedisStore({
client: redis,
prefix: "rl:",
}),
windowMs,
max,
keyGenerator: keyGenerator || ((req) => req.ip),
message: "Too many requests from this IP, please try again later.",
standardHeaders: true,
legacyHeaders: false,
});
};
// Apply different limits based on endpoint
export const apiLimiter = createRateLimiter(15 * 60 * 1000, 100); // 100 requests per 15 minutes
export const authLimiter = createRateLimiter(15 * 60 * 1000, 5); // 5 login attempts per 15 minutes
Contributing Guidelines¶
Code Standards¶
- TypeScript: Use strict type checking
- ESLint: Follow configured linting rules
- Prettier: Consistent code formatting
- Tests: Maintain >80% code coverage
- Documentation: Update docs for new features
Git Workflow¶
# Feature development workflow
git checkout -b feature/user-management-improvements
git commit -m "feat(users): add bulk user import functionality"
git push origin feature/user-management-improvements
# Create pull request with proper template
# Wait for code review and CI checks
# Merge after approval
Pull Request Template¶
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Manual testing completed
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] Breaking changes documented
For development questions or support, contact the SAAS Development Team at saas-dev@optim.com