This document describes the technical architecture, system design patterns, and infrastructure of TendSocial.
Table of Contents
- Overview
- High-Level Architecture
- Frontend Architecture
- Backend Architecture
- Data Models
- AI Integration
- Security
- Scalability
- Infrastructure Diagrams
Overview
TendSocial is a full-stack application following modern web architecture patterns:
- Frontend: Single-Page Application (SPA) built with React
- Backend: RESTful API built with Fastify
- Database: PostgreSQL with Prisma ORM
- AI Services: Google Generative AI (Gemini, Imagen)
- Storage: S3-compatible object storage
- Deployment: Containerized microservices
Architecture Principles
- Separation of Concerns: Clear boundaries between frontend, backend, and external services
- API-First Design: All functionality exposed via RESTful endpoints
- Stateless Services: Backend is stateless for horizontal scalability
- Eventual Consistency: Async jobs for non-critical operations
- Security by Default: Authentication, authorization, and encryption at every layer
High-Level Architecture
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT LAYER │
│ ┌────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ React │ │ TypeScript │ │ Tailwind │ │
│ │ App │ │ Types │ │ CSS │ │
│ └────────────┘ └───────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ HTTPS (REST API)
▼
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Fastify API Server │ │
│ │ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────┐ │ │
│ │ │ Auth │ │ Routes │ │ Business │ │ Jobs │ │ │
│ │ │Middleware│ │ │ │ Logic │ │ Queue │ │ │
│ │ └──────────┘ └──────────┘ └───────────┘ └────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ DATA LAYER │ │ AI SERVICES │ │ EXTERNAL APIs │
│ │ │ │ │ │
│ ┌────────────┐ │ │ ┌──────────┐ │ │ ┌──────────────┐ │
│ │ PostgreSQL │ │ │ │ Gemini │ │ │ │ LinkedIn API │ │
│ │ (Prisma) │ │ │ │ 2.5 │ │ │ │ │ │
│ └────────────┘ │ │ └──────────┘ │ │ └──────────────┘ │
│ │ │ │ │ │
│ ┌────────────┐ │ │ ┌──────────┐ │ │ ┌──────────────┐ │
│ │ S3 │ │ │ │ Imagen │ │ │ │ X (Twitter)│ │
│ │ Storage │ │ │ │ 4.0 │ │ │ │ API │ │
│ └────────────┘ │ │ └──────────┘ │ │ └──────────────┘ │
│ │ │ │ │ │
│ ┌────────────┐ │ │ │ │ ┌──────────────┐ │
│ │ Redis │ │ │ │ │ │ Instagram │ │
│ │ (Cache + │ │ │ │ │ │ Graph API │ │
│ │ Queue) │ │ │ │ │ └──────────────┘ │
│ └────────────┘ │ │ │ │ │
└──────────────────┘ └──────────────┘ └──────────────────┘Frontend Architecture
Technology Stack
- Framework: React 19 with hooks
- Language: TypeScript (strict mode)
- Bundler: Vite 6 (HMR, fast builds)
- Styling: Tailwind CSS 4 (utility-first)
- State Management: React Context + local state
- Routing: Simulated routing via component state (can migrate to React Router)
Directory Structure
/
├── apps/
│ ├── web/ # Frontend (React + Vite)
│ │ ├── src/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ └── ...
│ │ ├── vite.config.ts
│ │ └── Dockerfile
│ │
│ └── api/ # Backend (Fastify + Node.js)
│ ├── src/
│ │ ├── routes/
│ │ ├── services/
│ │ └── ...
│ ├── prisma/
│ └── Dockerfile
│
├── packages/ # Shared packages
│ ├── types/ # Shared TypeScript types
│ └── ...
│
├── infra/ # Infrastructure as Code
│ ├── terraform/
│ └── ...
│
├── docs/ # Documentation
│ ├── architecture/
│ ├── guides/
│ └── ...
│
├── package.json # Root (Turbo)
└── turbo.json # Turbo configurationFrontend Architecture (apps/frontend)
Technology Stack:
- Framework: React 19 with hooks
- Language: TypeScript (strict mode)
- Bundler: Vite 6 (HMR, fast builds)
- Styling: Tailwind CSS 4 (utility-first)
- State Management: React Context + local state
Component Architecture:
- Container/Presenter Pattern: Separating logic from UI.
- Shared UI: Reusable components in
components/ui(shadcn/ui style).
Backend Architecture (apps/backend)
Technology Stack:
- Framework: Fastify (high performance)
- Language: TypeScript (strict mode)
- ORM: Prisma 7 (type-safe database access)
- Database: PostgreSQL
- Validation: Zod schemas
- Authentication: JWT (via @fastify/jwt)
Directory Structure (apps/backend):
apps/backend/
├── src/
│ ├── index.ts # Server entry point
│ ├── routes/ # API endpoints
│ ├── services/ # Business logic
│ ├── middleware/ # Request/response middleware
│ ├── schemas/ # Zod validation schemas
│ └── utils/ # Helpers
├── prisma/
│ ├── schema.prisma # Database schema
│ └── migrations/ # Migration history
├── Dockerfile # Production container
└── package.jsonAPI Design
RESTful Conventions:
GET /api/blogs # List blogs
GET /api/blogs/:id # Get specific blog
POST /api/blogs # Create blog
PUT /api/blogs/:id # Update blog
DELETE /api/blogs/:id # Delete blogRequest/Response Format:
typescript
// Request
POST /api/blogs
{
"title": "My Blog Post",
"content": "...",
"campaignId": "uuid"
}
// Success Response (200/201)
{
"success": true,
"data": {
"id": "uuid",
"title": "My Blog Post",
"createdAt": "2025-01-24T12:00:00Z"
}
}
// Error Response (400/401/500)
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Title is required",
"details": { ... }
}
}Authentication Flow
typescript
// 1. User login
POST /auth/login
{
"email": "user@example.com",
"password": "hashed_password"
}
// 2. Server validates credentials, returns JWT
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": "...", "email": "..." }
}
// 3. Client includes token in subsequent requests
GET /api/blogs
Headers: {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs..."
}
// 4. Server validates JWT via middleware
// 5. If valid, attach user to request object
// 6. Route handler can access req.userMiddleware Stack
typescript
import fastify from 'fastify';
import jwt from '@fastify/jwt';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
const app = fastify();
// 1. CORS (allow frontend origin)
app.register(cors, { origin: process.env.FRONTEND_URL });
// 2. JWT authentication
app.register(jwt, { secret: process.env.JWT_SECRET });
// 3. Rate limiting (prevent abuse)
app.register(rateLimit, {
max: 100,
timeWindow: '15 minutes'
});
// 4. Custom middleware (authentication)
app.decorate('authenticate', async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
reply.status(401).send({ error: 'Unauthorized' });
}
});
// 5. Routes (with middleware applied)
app.get('/api/blogs', {
preHandler: [app.authenticate]
}, async (request, reply) => {
// request.user is available here
});Data Models
Core Entities
User
prisma
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships Membership[]
@@index([email])
}Company
prisma
model Company {
id String @id @default(uuid())
name String
subscriptionStatus String? // 'active', 'cancelled', etc.
subscriptionPlanName String?
createdAt DateTime @default(now())
users User[]
socialAccounts SocialAccount[]
brandProfile BrandProfile?
campaigns CompanyCampaign[]
}User (Company Member)
prisma
model User {
id String @id @default(uuid())
email String @unique
name String
role String @default("member") // owner, admin, member
companyId String
createdAt DateTime @default(now())
company Company @relation(fields: [companyId], references: [id])
@@index([companyId])
}BrandProfile
prisma
model BrandProfile {
id String @id @default(uuid())
companyId String @unique
name String?
websiteUrl String?
// Visual Identity
primaryColor String?
typography String?
logoUrl String?
// Messaging
voice String? // JSON: { tone, style }
mission String?
values String[] // Array of strings
// Target Audience
targetAudience String? // JSON: { demographics, psychographics }
company Company @relation(fields: [companyId], references: [id])
@@index([companyId])
}CompanyCampaign
prisma
model CompanyCampaign {
id String @id @default(uuid())
companyId String
name String
brief String? @db.Text
goal String?
status String @default("DRAFT") // DRAFT, ACTIVE, COMPLETED
startsAt DateTime?
endsAt DateTime?
company Company @relation(fields: [companyId], references: [id])
posts CompanyPost[]
blogPosts CompanyBlogPost[]
videoScripts CompanyVideoScript[]
@@index([companyId])
}Blog
prisma
model Blog {
id String @id @default(uuid())
campaignId String?
title String
content String @db.Text // Markdown
frontmatter String? @db.Text // JSON or YAML string
coverImageUrl String?
status String @default("draft") // draft, scheduled, published
scheduledAt DateTime?
publishedAt DateTime?
campaign Campaign? @relation(fields: [campaignId], references: [id])
@@index([campaignId, status])
}SocialPost
prisma
model SocialPost {
id String @id @default(uuid())
campaignId String?
blogId String? // If generated from a blog
platform String // linkedin, twitter, instagram, facebook
content String @db.Text
imageUrls String[] // Array of image URLs
status String @default("draft")
scheduledAt DateTime?
publishedAt DateTime?
campaign Campaign? @relation(fields: [campaignId], references: [id])
@@index([campaignId, platform, status])
}Database Relationships
Company (1) ──────── (Many) User
│
├──── (1) BrandProfile
│
└──── (Many) CompanyCampaign
├──── (Many) CompanyPost
├──── (Many) CompanyBlogPost
└──── (Many) CompanyVideoScriptAI Integration
Google Generative AI Architecture
typescript
import { GoogleGenerativeAI } from '@google/genai';
const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);
// Text Generation
const textModel = genAI.getGenerativeModel({
model: 'gemini-2.5-flash'
});
// Image Generation
const imageModel = genAI.getGenerativeModel({
model: 'imagen-4.0-generate-001'
});AI Service Patterns
1. Structured Output with JSON
typescript
async function generateBlogPost(topic: string, brandProfile: Brand) {
const prompt = `
Generate a blog post about "${topic}".
Brand Context:
- Voice: ${brandProfile.voice}
- Mission: ${brandProfile.mission}
- Target Audience: ${brandProfile.icp}
Return JSON with this structure:
{
"title": "...",
"content": "... (markdown)",
"tags": ["tag1", "tag2"],
"seoDescription": "..."
}
`;
const result = await textModel.generateContent({
contents: [{ role: 'user', parts: [{ text: prompt }] }],
generationConfig: {
responseMimeType: 'application/json'
}
});
return JSON.parse(result.response.text());
}2. Streaming Responses (for chat)
typescript
async function* chatStream(messages: Message[]) {
const chat = textModel.startChat({ history: messages });
const result = await chat.sendMessageStream(userMessage);
for await (const chunk of result.stream) {
yield chunk.text();
}
}3. Image Generation
typescript
async function generateImage(prompt: string, brand: Brand) {
const enhancedPrompt = `
${prompt}.
Color palette: ${brand.primaryColor}.
Style: modern, professional, on-brand.
No text in image.
`;
const result = await imageModel.generateImages({
prompt: enhancedPrompt,
numberOfImages: 1,
aspectRatio: '16:9'
});
// Upload to S3
const imageUrl = await uploadToS3(result.images[0].imageData);
return imageUrl;
}Rate Limiting & Cost Optimization
typescript
// 1. Client-side caching
const cache = new Map<string, CachedResponse>();
async function cachedAICall(cacheKey: string, fn: () => Promise<any>) {
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = await fn();
cache.set(cacheKey, result);
return result;
}
// 2. Request debouncing (prevent duplicate requests)
import debounce from 'lodash.debounce';
const debouncedGenerate = debounce(generateBlogPost, 1000);
// 3. User-level rate limiting
const userRequestCounts = new Map<string, number>();
function checkRateLimit(userId: string) {
const count = userRequestCounts.get(userId) || 0;
if (count > MAX_REQUESTS_PER_HOUR) {
throw new Error('Rate limit exceeded');
}
userRequestCounts.set(userId, count + 1);
}Security
Authentication & Authorization
JWT Structure
json
{
"sub": "user-uuid",
"email": "user@example.com",
"orgId": "org-uuid",
"role": "admin",
"iat": 1706097600,
"exp": 1706184000
}Role-Based Access Control (RBAC)
typescript
const permissions = {
viewer: ['read:blogs', 'read:campaigns'],
editor: ['read:blogs', 'write:blogs', 'read:campaigns', 'write:campaigns'],
admin: ['*'], // All permissions
owner: ['*', 'delete:company', 'manage:billing']
};
function authorize(requiredPermission: string) {
return async (request: FastifyRequest, reply: FastifyReply) => {
const userRole = request.user.role;
const userPermissions = permissions[userRole];
if (!userPermissions.includes(requiredPermission) && !userPermissions.includes('*')) {
reply.status(403).send({ error: 'Forbidden' });
}
};
}
// Usage
app.delete('/api/blogs/:id', {
preHandler: [authenticate, authorize('write:blogs')]
}, async (request, reply) => {
// Handler logic
});Data Protection
1. Encryption at Rest
- Database: PostgreSQL with encryption enabled
- File Storage: S3 server-side encryption (SSE-S3 or SSE-KMS)
- Secrets: Google Secret Manager or AWS Secrets Manager
2. Encryption in Transit
- All API calls over HTTPS/TLS
- Certificate management via Let's Encrypt or cloud provider
3. Sensitive Data Handling
typescript
// Never log sensitive data
logger.info('User login attempt', {
userId: user.id,
// DO NOT log: password, API keys, tokens
});
// Sanitize error responses
app.setErrorHandler((error, request, reply) => {
logger.error(error); // Full error in logs
reply.status(500).send({
error: 'Internal Server Error',
// DO NOT send: stack trace, internal details to client
});
});Input Validation
All inputs validated with Zod before processing:
typescript
import { z } from 'zod';
const createBlogSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
campaignId: z.string().uuid().optional(),
tags: z.array(z.string()).max(10).optional(),
});
app.post('/api/blogs', async (request, reply) => {
const validated = createBlogSchema.parse(request.body); // Throws if invalid
// Proceed with validated data
});API Security Checklist
- [x] HTTPS only (HTTP redirects to HTTPS)
- [x] CORS configured (specific origins, not wildcard)
- [x] Rate limiting (per IP and per user)
- [x] JWT expiration (15 minutes access, 7 days refresh)
- [x] Input validation on all endpoints
- [x] SQL injection prevention (Prisma ORM)
- [x] XSS prevention (sanitize user-generated content)
- [x] CSRF tokens (if using cookies)
- [ ] API versioning (future: /v1/blogs, /v2/blogs)
- [ ] Audit logging (track sensitive operations)
Scalability
Horizontal Scaling
Stateless Backend:
- No session state stored in backend (JWT in client)
- Multiple backend instances behind load balancer
- Sticky sessions NOT required
Database Connection Pooling:
typescript
// Prisma connection pool
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
// Pool settings
// Max connections per instance: 10
// Connection timeout: 10s
});Load Balancing:
┌──────────────┐
│ Load Balancer│
│ (NGINX / │
│ Cloud LB) │
└──────┬───────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Backend 1│ │Backend 2│ │Backend 3│
└─────────┘ └─────────┘ └─────────┘
│ │ │
└────────────────┼────────────────┘
│
┌──────▼───────┐
│ PostgreSQL │
│ (Read Replicas)
└──────────────┘Caching Strategy
1. Application-Level Cache (Redis)
typescript
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getCachedBrandProfile(brandId: string) {
const cached = await redis.get(`brand:${brandId}`);
if (cached) return JSON.parse(cached);
const brand = await prisma.brand.findUnique({ where: { id: brandId } });
await redis.set(`brand:${brandId}`, JSON.stringify(brand), 'EX', 3600); // 1 hour TTL
return brand;
}Cache Invalidation:
typescript
async function updateBrand(brandId: string, data: BrandUpdate) {
const updated = await prisma.brand.update({
where: { id: brandId },
data
});
// Invalidate cache
await redis.del(`brand:${brandId}`);
return updated;
}2. CDN for Static Assets
- Frontend bundle served via Vercel/Cloudflare CDN
- S3 images behind CloudFront or Cloudflare
Background Job Processing
Job Queue (BullMQ):
typescript
import { Queue, Worker } from 'bullmq';
const postQueue = new Queue('scheduled-posts', {
connection: { host: 'localhost', port: 6379 }
});
// Producer: Add job to queue
async function schedulePost(post: SocialPost, publishAt: Date) {
await postQueue.add('publish-post', {
postId: post.id,
platform: post.platform
}, {
delay: publishAt.getTime() - Date.now()
});
}
// Consumer: Worker processes jobs
const worker = new Worker('scheduled-posts', async (job) => {
const { postId, platform } = job.data;
// Publish to platform API
await publishToLinkedIn(postId);
// Update database
await prisma.socialPost.update({
where: { id: postId },
data: { status: 'published', publishedAt: new Date() }
});
}, {
connection: { host: 'localhost', port: 6379 }
});Database Optimization
Indexes
prisma
model Blog {
// ...
@@index([campaignId]) // For filtering by campaign
@@index([status, scheduledAt]) // For scheduled post queries
@@index([publishedAt]) // For recent posts
}Read Replicas (Production)
typescript
// Prisma with read replicas
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL, // Primary (writes)
},
},
});
const replicaPrisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_REPLICA_URL, // Replica (reads)
},
},
});
// Use replica for read-heavy operations
async function listBlogs() {
return replicaPrisma.blog.findMany();
}Infrastructure Diagrams
Production Deployment (Google Cloud)
┌─────────────────────────────────────────────────────────┐
│ Cloudflare CDN │
│ (Frontend + Static Assets) │
└────────────────────┬────────────────────────────────────┘
│
│ HTTPS
▼
┌───────────────────────┐
│ Cloud Load Balancer │
└───────────┬───────────┘
│
┌────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Cloud │ │Cloud │ │Cloud │
│Run │ │Run │ │Run │
│Instance│ │Instance│ │Instance│
│ 1 │ │ 2 │ │ 3 │
└────┬───┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────────┘
│
┌────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌─────────┐ ┌──────────────┐
│Cloud SQL │ │ Cloud │ │ Google │
│(Postgres)│ │ Storage │ │ Generative │
│ │ │ (Images)│ │ AI API │
└──────────┘ └─────────┘ └──────────────┘
│
│ Backup
▼
┌──────────────┐
│ Automated │
│ Backups │
│ (Daily) │
└──────────────┘Related Documentation
- Tech Stack
- Deployment Guide
- Backup & Restore - Data protection strategies
Last Updated: November 24, 2025
Architect: TendSocial AI Engineering Team