Skip to content

This document describes the technical architecture, system design patterns, and infrastructure of TendSocial.

Table of Contents


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

  1. Separation of Concerns: Clear boundaries between frontend, backend, and external services
  2. API-First Design: All functionality exposed via RESTful endpoints
  3. Stateless Services: Backend is stateless for horizontal scalability
  4. Eventual Consistency: Async jobs for non-critical operations
  5. 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 configuration

Frontend 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.json

API 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 blog

Request/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.user

Middleware 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) CompanyVideoScript

AI 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)     │
└──────────────┘


Last Updated: November 24, 2025
Architect: TendSocial AI Engineering Team

TendSocial Documentation