Back to Blog
Development

Multi-Project Feedback Management: Isolating Data Across Products and Teams

Learn how agencies and multi-product companies securely manage user feedback across projects with proper multi-tenant data isolation, API key management, and team access controls.

UserVibesOS Team
9 min read
Multi-Project Feedback Management: Isolating Data Across Products and Teams

Summary

Managing feedback across multiple products requires more than separate folders. True multi-project feedback management demands cryptographic data isolation, project-scoped API keys, and secure team access controls. This guide covers the architecture and best practices for agencies and multi-product companies running feedback collection at scale.

The Multi-Project Challenge

When your organization manages multiple products, clients, or brands, feedback silos become critical. Consider these scenarios:

  • Agencies managing feedback widgets for 10+ client websites
  • SaaS companies with separate feedback streams for web, mobile, and API products
  • Enterprise teams requiring isolated feedback channels for different business units
  • White-label providers offering branded feedback collection to customers

Each scenario demands that feedback data never leak between projects. A client's feature requests must never appear in another client's dashboard. A mobile app's bug reports must stay separate from web app feedback.

Architecture Overview: True Multi-Tenant Isolation

Multi-tenant architecture isn't just about database tables with a projectId column. True isolation requires defense at every layer.

The Three-Layer Isolation Model

Layer 1: API Key Binding
   Public Keys (PK) are cryptographically bound to specific projects

Layer 2: JWT Signature Verification
   Each project has a unique secret key for signing tokens

Layer 3: Database Query Scoping
   All queries include projectId filtering at the database level

How Project Isolation Works

When a user submits feedback through your widget, here's what happens:

  1. Widget loads with project-specific Public Key (PK)
  2. Request arrives with projectId + apiKey headers
  3. Middleware validates the PK is bound to the claimed project
  4. JWT verification (if enabled) uses project's unique secret
  5. Data is stored with project scope in the database
  6. Queries filter by projectId on every read

This means an attacker cannot use one project's API key to access another project's data, even if they somehow obtain both keys.

API Key Architecture: Public vs Secret Keys

Understanding the two-key system is essential for secure multi-project management.

Public Keys (PK): Client-Facing Identifiers

Public Keys are designed for browser environments:

// Safe to embed in client-side code
const widgetConfig = {
  projectId: "proj_abc123",
  apiKey: "pk_live_a1b2c3d4e5f6..."
};

Public Key properties:

  • Prefix: pk_live_
  • Bound to a specific project at creation time
  • Requires origin whitelisting (1-5 allowed domains)
  • Safe to expose in JavaScript bundles
  • Cannot be used for server-side operations

Secret Keys (SK): Server-Only Credentials

Secret Keys never leave your server:

// NEVER expose in client code
// Used server-side only for JWT signing
const secretKey = process.env.USERVIBES_SECRET_KEY;
// "sk_live_x9y8z7..."

Secret Key properties:

  • Prefix: sk_live_
  • Used exclusively for JWT token generation
  • Must be stored in environment variables
  • Enables Tier 2 authenticated feedback
  • Cryptographically signs user identity claims

Project-Scoped Key Binding

The security model ensures keys cannot be reused across projects:

// When creating a Public Key, it's bound to the project
createPublicKey({
  name: "Production Widget Key",
  projectId: "proj_abc123",  // Cryptographic binding
  allowedOrigins: ["https://myapp.com", "https://www.myapp.com"]
});

If someone tries to use this key with a different project ID, the request is rejected:

Request: projectId=proj_xyz789, apiKey=pk_live_...
Result: "API key not authorized for this project"
Security Event: tenant_isolation_attack (logged)

Setting Up Multiple Projects

Step 1: Create Projects with Unique Identities

Each project represents a distinct feedback collection point:

// Project 1: Main Web App
{
  name: "Acme Web Dashboard",
  slug: "acme-web",
  branding: {
    primaryColor: "#6366f1",
    logoUrl: "https://cdn.acme.com/logo-web.png"
  }
}

// Project 2: Mobile App
{
  name: "Acme Mobile App",
  slug: "acme-mobile",
  branding: {
    primaryColor: "#10b981",
    logoUrl: "https://cdn.acme.com/logo-mobile.png"
  }
}

// Project 3: API Documentation
{
  name: "Acme Developer Portal",
  slug: "acme-devs",
  branding: {
    primaryColor: "#f59e0b",
    logoUrl: "https://cdn.acme.com/logo-dev.png"
  }
}

Step 2: Generate Project-Specific API Keys

For each project, create dedicated keys:

  1. Public Key for the widget embed
  2. Secret Key for JWT authentication (Tier 2)
  3. Configure origin restrictions per key
// For acme-web project
const webPublicKey = await createPublicKey({
  name: "Acme Web - Production",
  projectId: acmeWebProjectId,
  allowedOrigins: [
    "https://app.acme.com",
    "https://staging.acme.com"
  ]
});

// For acme-mobile project
const mobilePublicKey = await createPublicKey({
  name: "Acme Mobile - Production",
  projectId: acmeMobileProjectId,
  allowedOrigins: [
    "https://m.acme.com"  // Mobile web wrapper
  ]
});

Step 3: Configure Per-Project Settings

Each project can have unique configurations:

{
  settings: {
    allowAnonymous: false,     // Require user identification
    requireEmail: true,        // Collect email addresses
    moderationEnabled: true,   // Review before publishing
    votingEnabled: true,       // Allow community voting
    commentsEnabled: true      // Enable discussion threads
  },
  widgetTabs: {
    featureRequest: { enabled: true, order: 0 },
    bugReports: { enabled: true, order: 1 },
    roadmap: { enabled: true, order: 2 },
    changelog: { enabled: false, order: 3 }  // Hide for this project
  }
}

Origin Restrictions: Preventing Widget Hijacking

Origin restrictions ensure your widget only runs on authorized domains.

Why Origins Matter

Without origin restrictions, anyone could embed your widget on their site:

<!-- Malicious site embedding your widget -->
<iframe src="https://widget.uservibes.com/embed?projectId=YOUR_ID&apiKey=YOUR_KEY">

With origin restrictions, this fails:

Request Origin: https://malicious-site.com
Allowed Origins: ["https://app.acme.com"]
Result: 403 Forbidden - Origin not allowed

Configuring Allowed Origins

Best practices for origin configuration:

const allowedOrigins = [
  // Production domain
  "https://app.acme.com",

  // Staging for testing
  "https://staging.acme.com",

  // Local development (if needed)
  "http://localhost:3000",

  // Wildcard for subdomains (use carefully)
  "https://*.acme.com"
];

Important: Wildcard patterns (*.acme.com) should be used cautiously. They allow any subdomain, which could be exploited if subdomain takeover is possible.

Tier 2 Authentication: JWT-Based User Identity

For authenticated feedback, each project uses its own JWT secret.

How JWT Isolation Works

Project A: Secret Key = sk_live_aaa...
Project B: Secret Key = sk_live_bbb...

User submits to Project A with JWT signed by sk_live_bbb
→ Signature verification fails
→ Request rejected
→ Attack prevented

Implementing Server-Side Token Generation

Your backend generates tokens for authenticated users:

// Server-side only (Node.js, Python, etc.)
import jwt from 'jsonwebtoken';

function generateUserVibesToken(user, projectSecretKey) {
  return jwt.sign(
    {
      user: {
        id: user.id,
        email: user.email,
        name: user.displayName
      },
      jti: crypto.randomUUID(),  // Prevents replay attacks
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 300  // 5 min expiry
    },
    projectSecretKey,
    { algorithm: 'HS256' }
  );
}

// Generate token for Project A users
const tokenA = generateUserVibesToken(user, process.env.PROJECT_A_SECRET);

// Generate token for Project B users
const tokenB = generateUserVibesToken(user, process.env.PROJECT_B_SECRET);

Widget Configuration with JWT

// Client-side widget initialization
window.UserVibesWidget.init({
  projectId: "proj_abc123",
  apiKey: "pk_live_...",
  jwt: await fetchTokenFromYourBackend()  // Server-generated
});

Agency Use Case: Managing Client Feedback

For agencies managing multiple client projects, organization is key.

Agency Dashboard
├── Client: TechStartup Inc
│   ├── Project: Main Product
│   │   ├── PK: pk_live_tech_main...
│   │   └── SK: sk_live_tech_main...
│   └── Project: Mobile App
│       ├── PK: pk_live_tech_mobile...
│       └── SK: sk_live_tech_mobile...
│
├── Client: RetailCo
│   ├── Project: E-commerce Site
│   │   ├── PK: pk_live_retail_web...
│   │   └── SK: sk_live_retail_web...
│   └── Project: Admin Portal
│       ├── PK: pk_live_retail_admin...
│       └── SK: sk_live_retail_admin...
│
└── Client: HealthApp
    └── Project: Patient Portal
        ├── PK: pk_live_health_patient...
        └── SK: sk_live_health_patient...

Client Branding Per Project

Each project maintains independent branding:

// TechStartup project
{
  branding: {
    primaryColor: "#3b82f6",  // Blue theme
    logoUrl: "https://cdn.techstartup.com/logo.svg",
    fontFamily: "Inter"
  }
}

// RetailCo project
{
  branding: {
    primaryColor: "#dc2626",  // Red theme
    logoUrl: "https://cdn.retailco.com/logo.svg",
    fontFamily: "Roboto"
  }
}

Security Best Practices

1. Rotate Secret Keys Regularly

Schedule secret key rotation every 90 days:

// Create new secret key
const newSecretKey = await createSecretKey({
  name: "Production SK - Q2 2026"
});

// Update project to use new key
await updateJwtSettings({
  projectId: projectId,
  jwtSecretKeyId: newSecretKey._id
});

// Revoke old key after migration period
await revokeApiKey({ keyId: oldSecretKeyId });

2. Use Environment-Specific Keys

Never share keys between environments:

# .env.production
USERVIBES_PROJECT_ID=proj_prod_abc123
USERVIBES_PUBLIC_KEY=pk_live_prod...
USERVIBES_SECRET_KEY=sk_live_prod...

# .env.staging
USERVIBES_PROJECT_ID=proj_staging_xyz789
USERVIBES_PUBLIC_KEY=pk_live_staging...
USERVIBES_SECRET_KEY=sk_live_staging...

3. Monitor Security Events

Watch for these signals in your logs:

  • tenant_isolation_attack: Cross-project access attempt
  • invalid_api_key: Unknown or malformed keys
  • origin_mismatch: Widget loaded from unauthorized domain
  • jwt_validation_failed: Token signature verification failed

4. Implement Rate Limiting

Each project should have request limits:

{
  settings: {
    rateLimit: {
      feedbackPerMinute: 10,
      votesPerMinute: 30,
      commentsPerMinute: 20
    }
  }
}

Database Query Scoping

All data queries include project filtering at the database level:

// Feature requests are always project-scoped
const requests = await db
  .query("featureRequests")
  .withIndex("byProjectId", q => q.eq("projectId", projectId))
  .collect();

// Votes include project scope
const votes = await db
  .query("votes")
  .withIndex("byProjectAndEmail", q =>
    q.eq("projectId", projectId)
  )
  .collect();

// Comments filtered by project
const comments = await db
  .query("comments")
  .withIndex("byProject", q => q.eq("projectId", projectId))
  .collect();

This ensures that even if application code had a bug, the database index structure prevents cross-project data access.

Key Takeaways

  1. True isolation requires multiple layers: API key binding, JWT verification, and database scoping work together
  2. Public Keys are project-bound: They cannot access data from other projects
  3. Secret Keys never leave your server: Use them only for backend JWT generation
  4. Origin restrictions prevent widget hijacking: Always configure allowed domains
  5. Each project is independently configurable: Branding, settings, and keys are project-specific
  6. Monitor security events: Watch for cross-tenant access attempts
  7. Rotate credentials regularly: Schedule key rotation every 90 days

Implementation Checklist

  • Create separate projects for each product/client
  • Generate project-specific Public and Secret Keys
  • Configure origin restrictions for each Public Key
  • Set up environment variables per deployment environment
  • Enable Tier 2 JWT authentication for sensitive projects
  • Configure project-specific branding and settings
  • Set up security event monitoring
  • Document key rotation schedule
  • Test cross-project isolation (should fail)

Multi-project feedback management is essential for agencies and enterprise teams. With proper isolation, you can confidently manage feedback across unlimited products while maintaining strict data separation.

Share this article

Related Articles

Written by UserVibesOS Team

Published on January 9, 2026