Skip to content

Data Protection

Flowstate treats user data with the highest level of care. Every piece of data is validated, isolated, and protected through multiple layers of security controls.

All database access goes through Prisma ORM, which provides type-safe, parameterized queries. This architecture prevents SQL injection attacks and ensures data integrity.

Traditional database access using raw SQL queries is vulnerable to injection attacks:

// DANGEROUS: User input directly in query
const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);

If email contains ' OR '1'='1, the query becomes:

SELECT * FROM users WHERE email = '' OR '1'='1'

This returns all users, bypassing authentication.

Prisma eliminates this risk by using parameterized queries:

// SAFE: Prisma handles escaping and parameterization
const user = await prisma.user.findUnique({
where: { email: email },
});

Prisma generates safe queries that treat user input as data, never as executable code. Even if email contains SQL syntax, it’s treated as a literal string.

The codebase has a strict policy: no raw database queries. All data access uses Prisma’s query builder. This ensures:

  • Type safety: TypeScript catches errors at compile time
  • Injection prevention: User input is always parameterized
  • Consistent patterns: All queries follow the same structure
  • Automatic escaping: Prisma handles special characters correctly

The only exception is database migrations, which are reviewed and tested before deployment.

Every API endpoint validates input using Zod schemas before processing. Validation happens at the API boundary, before data reaches business logic or the database.

Each endpoint has three validation layers:

  1. Schema validation: Zod checks that input matches the expected structure
  2. Business logic validation: Application code checks business rules (e.g., “user must own this resource”)
  3. Database constraints: MongoDB enforces uniqueness, required fields, and data types

This defense-in-depth approach catches errors early and prevents invalid data from entering the system.

Creating a room requires validation at all three layers:

// Layer 1: Schema validation (Zod)
const createRoomSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
isPublic: z.boolean().default(false),
});
// Layer 2: Business logic validation
if (user.roomCount >= user.maxRooms) {
throw new Error('Room limit reached');
}
// Layer 3: Database constraints (Prisma schema)
model Room {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String // Required field
ownerId String @db.ObjectId
owner User @relation(fields: [ownerId], references: [id])
@@unique([ownerId, name]) // Unique constraint
}

If validation fails at any layer, the request is rejected with a 400 Bad Request response. The error message explains what failed validation, helping developers debug issues without exposing internal system details.

Zod validation provides:

  • Type safety: Schemas are TypeScript types, so the compiler catches mismatches
  • Runtime checks: Validation happens at runtime, catching errors that TypeScript can’t detect
  • Automatic parsing: Zod converts strings to numbers, dates, and other types
  • Custom validation: Business rules can be encoded in schemas (e.g., “email must be from allowed domain”)
  • Error messages: Zod generates detailed error messages for failed validation

All validation schemas are defined in the same file as the route handler, making it easy to see what input is expected.

File uploads are restricted to specific types and sizes to prevent abuse:

  • Allowed types: JPEG, PNG, GIF, WebP only
  • Maximum size: 5MB per file
  • Path traversal prevention: Filenames are sanitized and validated
  • Storage isolation: Files are stored in S3-compatible object storage with access controls

When a file is uploaded:

  1. Size check: File size is checked before reading the entire file (prevents memory exhaustion)
  2. Type check: MIME type is validated against the allowed list
  3. Header validation: File headers are checked to ensure the file is actually the claimed type
  4. Filename sanitization: Filename is stripped of special characters and path separators
  5. Virus scanning: (Planned) Files will be scanned for malware before storage
  6. Storage: File is uploaded to S3-compatible storage with a unique key

If any validation step fails, the upload is rejected and the file is discarded.

Filenames are sanitized to prevent path traversal attacks:

// DANGEROUS: User-provided filename could be "../../etc/passwd"
const filePath = `/uploads/${filename}`;
// SAFE: Sanitize filename to remove path separators
const safeFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
const filePath = `/uploads/${safeFilename}`;

Even if an attacker uploads a file named ../../etc/passwd, it’s stored as _____etc_passwd in the uploads directory.

Uploaded files are stored in S3-compatible object storage (MinIO in development, S3 or Cloudflare R2 in production). Access controls ensure:

  • Private by default: Files are not publicly accessible
  • Signed URLs: Access is granted via time-limited signed URLs
  • User isolation: Users can only access their own files
  • Expiry: Signed URLs expire after a short period (typically 1 hour)

When a user requests a file, the backend generates a signed URL that grants temporary access. The URL expires after the specified time, preventing long-term sharing of private files.

User data is strictly isolated. Each user has their own:

  • Canvas positions: Block positions are scoped to the user
  • Sessions: Login sessions are tied to the user
  • Uploaded files: Files are stored with user-scoped keys
  • Rooms and offices: Access is controlled by membership

Database queries always filter by user ID to ensure users can only access their own data:

// CORRECT: Filter by authenticated user
const rooms = await prisma.room.findMany({
where: { ownerId: req.user.id },
});
// WRONG: Returns all rooms (data leak)
const rooms = await prisma.room.findMany();

All queries that return user-specific data include a where clause that filters by the authenticated user’s ID. This prevents accidental data leaks.

Rooms and offices are multi-tenant resources. Multiple users can access the same room, but access is controlled by membership:

// Check if user is a member of the room
const membership = await prisma.roomMember.findUnique({
where: {
roomId_userId: {
roomId: roomId,
userId: req.user.id,
},
},
});
if (!membership) {
throw new Error('Access denied');
}

Users can only access rooms they’re members of. Room owners can invite other users, but invited users can’t invite additional users (unless the owner grants them that permission).

Users can report content that violates community guidelines. The reporting system supports five specific reasons:

ReasonDescription
SpamMisleading, repetitive, or commercial content
HarassmentBullying, threats, or targeted abuse
InappropriateOffensive, explicit, or disturbing material
CopyrightIntellectual property violations
OtherViolations not covered by other categories
  1. User clicks “Report” on a piece of content (message, room, profile, etc.)
  2. User selects a reason and optionally provides additional context
  3. Report is submitted and stored in the database
  4. Content moderators review the report in the admin dashboard
  5. Moderator takes action: approve (no violation), reject (false report), or escalate (needs admin review)
  6. User who reported the content is notified of the outcome

All reports are tracked in the database with:

  • Reporter ID: Who submitted the report
  • Target ID: What was reported (message, room, user, etc.)
  • Reason: Why it was reported
  • Context: Additional details from the reporter
  • Status: Pending, approved, rejected, or escalated
  • Moderator ID: Who reviewed the report
  • Resolution: What action was taken

If content is moderated (removed or hidden), the content owner can appeal the decision:

  1. User receives a notification that their content was moderated
  2. User clicks “Appeal” and provides their reasoning
  3. Appeal is submitted and stored in the database
  4. A different moderator reviews the appeal (not the original moderator)
  5. Moderator upholds or overturns the original decision
  6. User is notified of the appeal outcome

Appeals ensure fairness and accountability. If a moderator makes a mistake, the appeal process provides a path to correction.

All moderation actions are logged in the admin audit trail:

  • Action type: Report reviewed, content removed, appeal processed, etc.
  • Target: What was moderated
  • Moderator: Who took the action
  • Timestamp: When the action occurred
  • Reason: Why the action was taken

Audit logs are immutable and queryable. If a moderator abuses their power, the logs provide evidence for investigation.

Every privileged action taken by an admin is recorded in an immutable audit log. This provides accountability and enables investigation of suspicious activity.

Audit logs capture:

  • User management: Account creation, deletion, suspension, role changes
  • Content moderation: Reports reviewed, content removed, appeals processed
  • System configuration: Feature flags changed, settings updated
  • Data access: Sensitive data viewed or exported
  • Admin actions: Role promotions, permission grants, audit log queries

Each log entry includes:

  • Action type: What happened (e.g., user_ban, content_delete, role_change)
  • Target: What was affected (user ID, content ID, resource ID)
  • Timestamp: When the action occurred (UTC)
  • IP address: Where the request originated
  • User agent: Browser or client used
  • Admin identity: Who performed the action (admin user ID and email)
  • Details: Additional context (e.g., reason for ban, old and new role)

Admins can query audit logs by:

  • Date range: Show actions between two dates
  • Action type: Filter by specific action types
  • Admin: Show actions by a specific admin
  • Target: Show actions affecting a specific user or resource

Query results are paginated and can be exported as CSV for external analysis.

Audit logs are immutable. Once written, they cannot be modified or deleted. This ensures the integrity of the audit trail.

Even super admins cannot delete audit logs. The only way to remove logs is to delete the entire database, which would be immediately obvious and require physical access to the database server.

Users have control over their data. They can:

  • Export their data: Download a JSON file with all their data (canvas positions, rooms, messages, etc.)
  • Delete their account: Permanently remove their account and all associated data
  • Revoke sessions: Log out of specific devices or all devices

Account deletion is permanent and irreversible. The process:

  1. User requests account deletion
  2. System sends a confirmation email with a single-use token
  3. User clicks the confirmation link within 30 minutes
  4. System validates the token and deletes the account
  5. All user data is permanently removed:
    • User profile and authentication records
    • Canvas positions and sessions
    • Uploaded files (from S3 storage)
    • Room memberships (user is removed from all rooms)
    • Messages and content (marked as deleted, not removed)

Messages and content are marked as deleted rather than removed to preserve conversation context. The message text is replaced with “[deleted]” and the author is shown as “[deleted user]”.

Users can export their data as a JSON file. The export includes:

  • User profile (name, email, avatar URL)
  • Canvas positions (all blocks and their positions)
  • Rooms (owned and joined rooms)
  • Messages (sent messages with timestamps)
  • Sessions (active login sessions)
  • Uploaded files (list of files with download URLs)

The export is generated on-demand and sent via email as a downloadable link. The link expires after 24 hours.

Application logs are sanitized to prevent accidental exposure of personally identifiable information (PII).

Logs never contain:

  • Email addresses: Replaced with user IDs
  • IP addresses: Replaced with hashed values
  • Session tokens: Never logged
  • Passwords: Never logged (and never stored)
  • File contents: Only filenames and sizes are logged

Error messages are also sanitized. Stack traces are logged, but sensitive data is redacted:

// WRONG: Logs user email
logger.error(`Failed to send email to ${user.email}`);
// CORRECT: Logs user ID
logger.error(`Failed to send email to user ${user.id}`);

Logs are retained for 90 days and then automatically deleted. This limits the window of exposure if logs are compromised.

Audit logs (admin actions) are retained indefinitely for accountability, but they don’t contain PII beyond user IDs.

Flowstate’s data protection measures include:

  • Prisma ORM (prevents SQL injection attacks)
  • No raw queries (all database access is type-safe)
  • Zod validation (input is validated before processing)
  • File upload restrictions (type, size, and path validation)
  • S3 storage (files are isolated and access-controlled)
  • User data isolation (queries filter by user ID)
  • Content moderation (reporting and appeals process)
  • Admin audit logging (all privileged actions are logged)
  • Account deletion (users can permanently remove their data)
  • Data export (users can download their data)
  • No PII in logs (logs are sanitized to prevent exposure)
  • Log retention (logs are automatically deleted after 90 days)

Planned improvements:

  • Encryption at rest: Encrypt sensitive fields in the database
  • Field-level access control: Restrict access to specific fields based on role
  • Data retention policies: Automatically delete old data based on configurable policies
  • GDPR compliance tools: Automated data subject access requests and right-to-be-forgotten workflows
  • Virus scanning: Scan uploaded files for malware before storage
  • Data loss prevention: Detect and prevent accidental exposure of sensitive data
  • Backup encryption: Encrypt database backups before storage

Data protection is an ongoing commitment. The platform evolves to meet new regulatory requirements and incorporate industry best practices.