Browse documentation

Specifications

RankFlow AI — Security & Compliance Documentation

// src/lib/crypto.ts

docs/specs/security-compliance.md
On this page

Version: 1.0.0
Compliance: DPDPA 2023 (India), GDPR-ready
Encryption: AES-256-GCM
Auth: Better Auth with RBAC


1. Token Encryption#

Encryption Implementation#

// src/lib/crypto.ts

import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";

const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;

function getKey(): Buffer {
  return scryptSync(process.env.ENCRYPTION_KEY!, "rankflow-salt", 32);
}

export function encrypt(value: string): string {
  const key = getKey();
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  
  const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
  const authTag = cipher.getAuthTag();
  
  return `${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted.toString("base64")}`;
}

export function decrypt(encryptedValue: string): string {
  const key = getKey();
  const [ivB64, authTagB64, encryptedB64] = encryptedValue.split(":");
  
  const iv = Buffer.from(ivB64, "base64");
  const authTag = Buffer.from(authTagB64, "base64");
  const encrypted = Buffer.from(encryptedB64, "base64");
  
  const decipher = createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);
  
  return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
}

Encrypted Fields#

Field Model Encryption
accessToken GbpAccount At rest
refreshToken GbpAccount At rest
accessToken SocialAccount At rest
refreshToken SocialAccount At rest
passwordEncrypted Citation At rest (directory credentials)

Environment Variable#

# 32-byte key, base64 encoded
ENCRYPTION_KEY=xxx...xxx

2. Authentication & Authorization#

Role-Based Access Control#

enum UserRole {
  ADMIN      // Full system access
  CLIENT     // Practice owner
  EDITOR     // Can edit content, approve posts
  VIEWER     // Read-only dashboard
}

Middleware Stack#

Request → Rate Limit → Request ID → Auth → Practice Resolve → Role Check → Handler

Procedure Types#

Procedure Auth Practice Role Use Case
publicProcedure Any Health, webhooks
protectedProcedure Any User profile
practiceProcedure CLIENT+ Practice data
adminProcedure ADMIN System-wide

Session Management#

// Better Auth configuration
export const auth = betterAuth({
  database: prismaAdapter(db),
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // Refresh every 24h
  },
  rateLimit: {
    window: 60, // 1 minute
    max: 100, // 100 requests per minute
  },
});

3. DPDPA Compliance#

Data Processing Principles#

Principle Implementation
Consent Explicit opt-in during onboarding
Purpose limitation Data used only for SEO services
Data minimization Only collect necessary business data
Accuracy NAP monitoring ensures accuracy
Storage limitation Auto-delete after 1 year post-cancellation
Security Encryption + access controls
Accountability Full audit log of all data access
1. Client signs up → sees data usage terms
2. Must check "I consent to data processing"
3. Consent recorded with timestamp + IP
4. Can withdraw consent from settings
5. Withdrawal triggers data deletion workflow

Sensitive Data Handling#

Data Type Classification Handling
Business NAP Personal data (business) Encrypted, shared with directories only
Patient reviews Sensitive personal data Never stored long-term, only processed for replies
Reviewer names Personal data Anonymized in analytics
Doctor photos Personal data Stored with consent, deleted on cancellation
Payment info Financial data Tokenized (Stripe/Razorpay), never stored

Data Processing Agreement#

// src/server/lib/compliance/dpdpa.ts

export async function recordConsent(
  userId: string,
  practiceId: string,
  consentType: string,
  ipAddress: string,
  userAgent: string
) {
  await db.consentLog.create({
    data: {
      userId,
      practiceId,
      consentType,
      granted: true,
      ipAddress,
      userAgent,
      grantedAt: new Date(),
    },
  });
}

export async function withdrawConsent(userId: string, practiceId: string) {
  await db.consentLog.create({
    data: {
      userId,
      practiceId,
      consentType: "data_processing",
      granted: false,
      withdrawnAt: new Date(),
    },
  });
  
  // Trigger data deletion workflow
  await inngest.send({
    name: "compliance/data-deletion",
    data: { practice_id: practiceId },
  });
}

4. Audit Logging#

Audit Log Schema#

model AuditLog {
  id              String   @id @default(cuid())
  practiceId      String?
  userId          String?
  action          String   // CREATE, UPDATE, DELETE, LOGIN, SKILL_EXECUTE, etc.
  entityType      String   // table name or resource type
  entityId        String?
  oldValue        Json?
  newValue        Json?
  metadata        Json     @default("{}")
  ipAddress       String?
  userAgent       String?
  createdAt       DateTime @default(now())
  
  practice        Practice? @relation(fields: [practiceId], references: [id], onDelete: SetNull)
  user            User?     @relation(fields: [userId], references: [id], onDelete: SetNull)
  
  @@index([practiceId, createdAt])
  @@index([userId, createdAt])
  @@index([action, createdAt])
  @@map("audit_logs")
}

Logged Events#

Event Action Data Captured
User login LOGIN IP, user agent, timestamp
Practice created CREATE Full practice data
Practice updated UPDATE Old + new values
GBP connected CREATE Account email (not tokens)
Post published SKILL_EXECUTE Skill ID, cost, duration
Content approved UPDATE Content ID, approver
Billing event UPDATE Invoice status, amount
Data export EXPORT What was exported
Data deletion DELETE What was deleted

Audit Logger#

// src/server/harness/audit.ts

export async function logAudit(event: {
  practiceId?: string;
  userId?: string;
  action: string;
  entityType: string;
  entityId?: string;
  oldValue?: any;
  newValue?: any;
  metadata?: Record<string, any>;
  ipAddress?: string;
  userAgent?: string;
}) {
  await db.auditLog.create({
    data: {
      practiceId: event.practiceId,
      userId: event.userId,
      action: event.action,
      entityType: event.entityType,
      entityId: event.entityId,
      oldValue: event.oldValue,
      newValue: event.newValue,
      metadata: event.metadata || {},
      ipAddress: event.ipAddress,
      userAgent: event.userAgent,
    },
  });
  
  logger.info({
    event: "audit",
    practiceId: event.practiceId,
    userId: event.userId,
    action: event.action,
    entityType: event.entityType,
    entityId: event.entityId,
  }, "Audit event");
}

5. Data Export & Deletion#

Data Export (DPDPA Right to Access)#

export async function exportPracticeData(practiceId: string) {
  const [
    practice,
    locations,
    members,
    gbpAccounts,
    socialAccounts,
    citations,
    contentPieces,
    reviews,
    jobs,
    reports,
    auditLogs,
  ] = await Promise.all([
    db.practice.findUnique({ where: { id: practiceId } }),
    db.location.findMany({ where: { practiceId } }),
    db.practiceMember.findMany({ where: { practiceId }, include: { user: true } }),
    db.gbpAccount.findMany({ where: { practiceId } }),
    db.socialAccount.findMany({ where: { practiceId } }),
    db.citation.findMany({ where: { practiceId } }),
    db.contentPiece.findMany({ where: { practiceId } }),
    db.review.findMany({ where: { location: { practiceId } } }),
    db.job.findMany({ where: { practiceId } }),
    db.report.findMany({ where: { practiceId } }),
    db.auditLog.findMany({ where: { practiceId } }),
  ]);
  
  return {
    personalData: {
      practice,
      locations,
      members: members.map(m => ({
        role: m.role,
        joinedAt: m.acceptedAt,
        user: { name: m.user.name, email: m.user.email },
      })),
      gbpAccounts: gbpAccounts.map(a => ({
        accountEmail: a.accountEmail,
        connectedAt: a.createdAt,
      })),
      socialAccounts: socialAccounts.map(a => ({
        platform: a.platform,
        accountName: a.accountName,
      })),
      citations,
      contentPieces,
      reviews: reviews.map(r => ({
        rating: r.rating,
        comment: r.comment,
        reviewDate: r.reviewDate,
      })),
      jobs: jobs.map(j => ({ type: j.skillId, status: j.status, createdAt: j.createdAt })),
      reports,
      auditLogs,
    },
    generatedAt: new Date().toISOString(),
    format: "JSON",
  };
}

Data Deletion (DPDPA Right to Erasure)#

export async function deletePracticeData(practiceId: string) {
  await db.$transaction([
    // Soft delete practice
    db.practice.update({
      where: { id: practiceId },
      data: {
        name: `[DELETED] ${Date.now()}`,
        status: "CANCELLED",
        deletedAt: new Date(),
        customDomain: null,
        subdomain: `deleted-${Date.now()}`,
        stripeCustomerId: null,
        razorpayCustomerId: null,
      },
    }),
    
    // Wipe tokens
    db.gbpAccount.updateMany({
      where: { practiceId },
      data: { 
        accessToken: "[DELETED]", 
        refreshToken: "[DELETED]", 
        isActive: false 
      },
    }),
    
    db.socialAccount.updateMany({
      where: { practiceId },
      data: { 
        accessToken: "[DELETED]", 
        refreshToken: "[DELETED]", 
        isActive: false 
      },
    }),
    
    // Delete citations from directories
    db.citation.updateMany({
      where: { practiceId },
      data: { status: "REMOVED", directoryUrl: null },
    }),
    
    // Anonymize reviews
    db.review.updateMany({
      where: { location: { practiceId } },
      data: { 
        reviewerName: "[REDACTED]",
        comment: "[REDACTED]",
      },
    }),
  ]);
  
  // Trigger external deletions
  await inngest.send({
    name: "compliance/external-deletion",
    data: { practice_id: practiceId },
  });
}

6. API Security#

Rate Limiting#

Route Pattern Limit Window
skill.execute 10 1 minute
content.generate 5 1 minute
gbp.createPost 30 1 minute
social.createPost 30 1 minute
All other 100 1 minute

Request ID Propagation#

// Every request gets x-request-id
// Propagated to: audit logs, job records, external APIs

const requestId = crypto.randomUUID();
response.headers.set("x-request-id", requestId);

// External API calls include:
headers: {
  "X-RankFlow-Request-ID": requestId,
}

Input Validation#

// All inputs validated with Zod
const createPracticeSchema = z.object({
  name: z.string().min(2).max(100),
  slug: z.string().regex(/^[a-z0-9-]+$/).optional(),
  type: z.enum(["CLINIC", "HOSPITAL", /* ... */]),
});

// Sanitization
import DOMPurify from "isomorphic-dompurify";
const cleanContent = DOMPurify.sanitize(rawContent);

7. Infrastructure Security#

Docker Security#

# Dockerfile
FROM node:20-alpine
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs
# ... rest of Dockerfile

Environment Variables#

# Required secrets
ENCRYPTION_KEY=                    # 32-byte AES key
BETTER_AUTH_SECRET=                # Auth signing secret
ANTHROPIC_API_KEY=                 # Claude API
OPENAI_API_KEY=                    # OpenAI API
GOOGLE_CLIENT_SECRET=              # Google OAuth
STRIPE_SECRET_KEY=                   # Stripe
RAZORPAY_KEY_SECRET=               # Razorpay
AWS_SECRET_ACCESS_KEY=             # S3
CLOUDFLARE_API_TOKEN=              # DNS
HYPERBROWSER_API_KEY=              # Browser automation
FIRECRAWL_API_KEY=                 # Scraping
COMPOSIO_API_KEY=                  # Social auth
ZERNIO_API_KEY=                    # Social scheduling
RESEND_API_KEY=                    # Email
DATAFORSEO_PASSWORD=               # SEO data
SERPAPI_KEY=                       # Rank tracking

Network Security#

Layer Config
VPC AWS VPC with private subnets
Security Groups Only 80/443 inbound, restricted outbound
SSL Cloudflare wildcard cert + force HTTPS
CORS Strict origin whitelist
CSP Content Security Policy headers

Health Check#

// src/app/api/health/route.ts

export async function GET() {
  const checks = {
    database: await checkDatabase(),
    redis: await checkRedis(),
    inngest: await checkInngest(),
  };
  
  const overall = Object.values(checks).every(c => c.status === "pass")
    ? "healthy"
    : Object.values(checks).some(c => c.status === "fail")
    ? "unhealthy"
    : "degraded";
  
  return NextResponse.json({ status: overall, checks });
}

End of Security & Compliance Documentation