Skip to main content

Overview

Your JOIP profile contains personal information, account settings, credit balance, and usage statistics. You can update most profile fields at any time through the API or web interface.

Profile Structure

User Profile Fields

interface UserProfile {
  // Identity
  id: string;                      // Unique user ID
  email: string;                   // Email address
  firstName: string | null;        // First name
  lastName: string | null;         // Last name
  profileImageUrl: string | null;  // Profile picture URL
  
  // Account Status
  role: 'user' | 'admin' | 'super_admin';
  isActive: boolean;               // Account enabled/disabled
  
  // Age Verification
  ageVerified: boolean;            // Completed age verification
  birthDate: string | null;        // YYYY-MM-DD format
  
  // Referral System
  referralCode: string;            // Your unique 8-char code
  referredBy: string | null;       // Who referred you
  
  // Onboarding
  onboardingCompleted: boolean;
  interests: UserInterests | null; // Preferences from onboarding
  
  // Credits
  credits: {
    balance: number;
    tier: string;
    isLowBalance: boolean;
    nextAllocationDate: string | null;
  };
  
  // Timestamps
  createdAt: string;
  updatedAt: string;
}

User Interests

Set during onboarding or updated later:
interface UserInterests {
  contentRole: 'creator' | 'consumer';
  preferredFeatures: string[];     // Max 2 feature slugs
  preferredTags: string[];         // Max 5 tag slugs
  onboardedAt: string;             // ISO timestamp
}

Updating Your Profile

Editable Fields

You can update these fields via PATCH /api/auth/user:
  • firstName - Your first name
  • lastName - Your last name
  • profileImageUrl - URL to your profile picture
Example Request:
const response = await fetch('/api/auth/user', {
  method: 'PATCH',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    firstName: 'John',
    lastName: 'Doe',
    profileImageUrl: 'https://example.com/avatar.jpg'
  })
});

const updatedUser = await response.json();

Protected Fields

These fields cannot be changed via standard profile update:
  • email - Set by authentication provider
  • role - Admin-only modification
  • isActive - Admin-only modification
  • referralCode - Auto-generated, permanent
  • referredBy - Set once during signup
  • credits - Managed by credit system

Input Validation

All profile updates are validated:
const userProfileUpdateSchema = z.object({
  firstName: z.string().min(1).max(100).optional(),
  lastName: z.string().min(1).max(100).optional(),
  profileImageUrl: z.string().url().optional()
});
Rules:
  • First and last names: 1-100 characters
  • Profile image URL must be valid URL format
  • All fields are optional (only update what you provide)

Age Verification

Why It’s Required

Age verification is required for:
  • Compliance with adult content regulations
  • Access to certain NSFW features
  • Legal protection for the platform

Verification Process

Endpoint: POST /api/auth/verify-age Request:
{
  "birthDate": "1990-01-15",
  "ageVerified": true
}
Validation:
  • Birth date must be in YYYY-MM-DD format
  • Must be a valid calendar date
  • Cannot be in the future
  • Cannot be less than 18 years ago
Response:
{
  "message": "Age verification updated successfully",
  "user": {
    "ageVerified": true,
    "birthDate": "1990-01-15"
  }
}

Privacy & Compliance

Age verification is logged for compliance:
await storage.logUserActivity({
  userId,
  action: 'age_verification_updated',
  feature: 'user_profile',
  details: {
    birthDate,
    ageVerified,
    timestamp: new Date().toISOString()
  },
  ipAddress: req.ip,
  userAgent: req.get('User-Agent'),
  sessionId: req.sessionID
});
Privacy Notes:
  • Birth date stored encrypted in database
  • Only used for age verification, not shared publicly
  • Logged activity includes timestamp and IP for legal compliance

Onboarding Preferences

First-Time Setup

New users complete onboarding to personalize their experience: Endpoint: POST /api/auth/onboarding Request:
{
  "contentRole": "consumer",
  "preferredFeatures": ["smart-captions", "sessions"],
  "preferredTags": ["joi", "sissy", "femdom", "edging", "cei"]
}
Validation:
  • contentRole: Must be “creator” or “consumer”
  • preferredFeatures: Maximum 2 feature slugs
  • preferredTags: Maximum 5 tag slugs
Response:
{
  "onboardingCompleted": true,
  "interests": {
    "contentRole": "consumer",
    "preferredFeatures": ["smart-captions", "sessions"],
    "preferredTags": ["joi", "sissy", "femdom", "edging", "cei"],
    "onboardedAt": "2026-03-02T12:00:00.000Z"
  }
}

Resetting Onboarding

Onboarding can only be completed once unless reset by admin or in dev environment. Endpoint: POST /api/auth/onboarding/reset (Admin/Dev only) Authorization:
  • Admin users (role: admin or super_admin)
  • Development environment (NODE_ENV !== 'production')
Use Case: Testing onboarding flow during development.

Credit Balance

Your profile includes current credit information:

Credit Info Structure

interface UserCreditsSnapshot {
  balance: number;              // Current credit balance
  tier: string;                 // 'free' | 'joi_curious' | 'full_access' | 'vip'
  isLowBalance: boolean;        // Below threshold (20 credits)
  nextAllocationDate: string | null; // Next monthly allocation
}

Viewing Credit Balance

Client-Side:
import { useAuth } from '@/lib/AuthContext';

function ProfileComponent() {
  const { user } = useAuth();
  
  if (user?.credits) {
    return (
      <div>
        <h3>Credit Balance: {user.credits.balance}</h3>
        <p>Tier: {user.credits.tier}</p>
        {user.credits.isLowBalance && (
          <Alert>Running low on credits! Consider upgrading.</Alert>
        )}
        {user.credits.nextAllocationDate && (
          <p>Next allocation: {new Date(user.credits.nextAllocationDate).toLocaleDateString()}</p>
        )}
      </div>
    );
  }
}

Credit History

View detailed transaction history:
const response = await fetch('/api/credits/transactions?page=1&limit=20');
const { transactions, hasMore, total } = await response.json();

transactions.forEach(tx => {
  console.log(`${tx.type}: ${tx.amount} credits (${tx.description})`);
});
See Credits System for more details.

Referral Information

Your profile includes referral statistics:

Referral Code

Every user has a unique 8-character referral code:
const response = await fetch('/api/referrals/link');
const { code, referralUrl } = await response.json();

console.log('Your code:', code);        // e.g., "K7M2P9X4"
console.log('Share this:', referralUrl); // e.g., "https://joip.app?r=..."

Referral Stats

const response = await fetch('/api/referrals/stats');
const stats = await response.json();

console.log({
  totalReferrals: stats.totalReferrals,      // Number of successful referrals
  creditsEarned: stats.creditsEarned,        // Total credits from referrals
  referralCode: stats.referralCode,          // Your code
  isProgramActive: stats.isProgramActive     // Program status
});

Who Referred You

If you were referred:
if (user.referredBy) {
  console.log('You were referred by user:', user.referredBy);
  // Note: Referrer's identity (email, name) is not exposed for privacy
}
See Referral Program for more details.

Usage Statistics

Your account tracks comprehensive usage metrics:

Tracked Metrics

Sessions:
  • Sessions created
  • Sessions viewed
  • Sessions edited/deleted
  • Sessions shared/favorited
Smart Captions:
  • Total captions generated
  • Custom vs themed captions
  • Caption themes used (JOI, Forced-Bi, Beta, etc.)
Babecock Studio:
  • Images created
  • Remixes created
  • Layouts used (side-by-side vs top-bottom)
HypnoCocks:
  • Total animations created
Media Vault:
  • Media uploaded/deleted
  • Media shared/downloaded
  • Bulk downloads
General:
  • Login count
  • Pages viewed
  • API calls total
  • Time spent (minutes)

Viewing Your Stats

Usage statistics are tracked in the user_usage_stats table:
SELECT 
  sessions_created,
  smart_captions_generated,
  babecock_images_created,
  media_uploaded,
  login_count,
  last_activity_at
FROM user_usage_stats
WHERE user_id = 'your-user-id';
API Endpoint (if implemented):
const response = await fetch('/api/user/stats');
const stats = await response.json();

Activity Logs

Detailed activity logs are also maintained:
interface UserActivityLog {
  id: number;
  userId: string;
  action: string;          // e.g., 'session_created', 'caption_generated'
  feature: string;         // e.g., 'sessions', 'smart_captions'
  details: object;         // Action-specific metadata
  ipAddress: string;
  userAgent: string;
  sessionId: string;       // Browser session ID
  createdAt: timestamp;
}
Example Queries:
-- Recent activity
SELECT action, feature, created_at
FROM user_activity_logs
WHERE user_id = 'your-user-id'
ORDER BY created_at DESC
LIMIT 50;

-- Activity by feature
SELECT feature, COUNT(*) as count
FROM user_activity_logs
WHERE user_id = 'your-user-id'
GROUP BY feature
ORDER BY count DESC;

Account Roles

User Roles

RolePermissions
userStandard access to all features
adminUser management, analytics, settings
super_adminFull system access, admin management

Role-Based Access

Certain features require specific roles:
// Admin-only endpoints
app.get('/api/admin/users', isAdmin, handler);
app.get('/api/admin/analytics', isAdmin, handler);

// Admin check middleware
function isAdmin(req, res, next) {
  const userId = getUserId(req);
  const user = await storage.getUser(userId);
  
  if (!user || (user.role !== 'admin' && user.role !== 'super_admin')) {
    return res.status(403).json({ error: 'Admin access required' });
  }
  
  next();
}

Requesting Admin Access

Admin access is granted manually:
  1. Contact platform administrators
  2. Provide justification for admin access
  3. Admin creates temporary or permanent admin grant
  4. Access logged in admin audit logs

Profile Privacy

What’s Public

  • First name (if you share content publicly)
  • Profile image (if you share content publicly)
  • Public sessions and media you create

What’s Private

  • Email address
  • Last name
  • Birth date
  • Credit balance and transactions
  • Referral earnings
  • Usage statistics
  • Activity logs
  • IP addresses

Data Sanitization

Sensitive data is removed before sending to client:
function sanitizeUserForClient(user: User) {
  const { password, ...safeUser } = user;
  return safeUser;
}
Never exposed:
  • Password hashes (local auth only)
  • Session tokens
  • Internal database IDs (in some contexts)

Account Deletion

Request Deletion

To delete your account:
  1. Contact support with deletion request
  2. Verify your identity
  3. Admin processes deletion

What Gets Deleted

Immediately:
  • User profile record
  • Session cookies
  • Active sessions
Cascading Deletes:
  • All sessions you created
  • All media in your vault
  • All activity logs
  • Credit balance and transaction history
  • Referral records (as referred user)
Preserved (anonymized):
  • Community content you shared (anonymized)
  • Referral records (as referrer - anonymized)
  • Aggregate analytics (no personal identifiers)

Cascade Behavior

Database foreign keys with ON DELETE CASCADE:
-- Sessions deleted when user deleted
ALTER TABLE content_sessions
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE;

-- Media deleted when user deleted
ALTER TABLE user_media
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE;

API Reference

Get Current User

Get your complete profile including credits.Response:
{
  "id": "user123",
  "email": "user@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "profileImageUrl": "https://example.com/avatar.jpg",
  "role": "user",
  "isActive": true,
  "ageVerified": true,
  "birthDate": "1990-01-15",
  "referralCode": "K7M2P9X4",
  "referredBy": null,
  "onboardingCompleted": true,
  "interests": {
    "contentRole": "consumer",
    "preferredFeatures": ["smart-captions"],
    "preferredTags": ["joi", "edging"],
    "onboardedAt": "2026-03-01T10:00:00.000Z"
  },
  "credits": {
    "balance": 150,
    "tier": "free",
    "isLowBalance": false,
    "nextAllocationDate": "2026-04-01T00:00:00.000Z"
  },
  "createdAt": "2026-03-01T10:00:00.000Z",
  "updatedAt": "2026-03-02T14:30:00.000Z"
}

Update Profile

Update your profile information.Request Body:
{
  "firstName": "Jane",
  "lastName": "Smith",
  "profileImageUrl": "https://example.com/new-avatar.jpg"
}
Response: Returns updated user object (sanitized)Rate Limiting: Subject to public API rate limitsActivity Logging: Update is logged with fields changed

Verify Age

Update age verification status.Request Body:
{
  "birthDate": "1990-01-15",
  "ageVerified": true
}
Response:
{
  "message": "Age verification updated successfully",
  "user": { /* updated user */ }
}

Complete Onboarding

Save onboarding preferences.Request Body:
{
  "contentRole": "consumer",
  "preferredFeatures": ["smart-captions", "sessions"],
  "preferredTags": ["joi", "sissy", "femdom", "edging", "cei"]
}
Can only be completed once unless reset by admin/dev

Best Practices

Profile Security

  1. Use Strong Profile Images
    • Host on trusted CDN (Imgur, Cloudinary, etc.)
    • Use HTTPS URLs only
    • Avoid exposing personal information in image
  2. Be Mindful of Public Info
    • First name is visible on shared content
    • Profile image appears on community posts
    • Consider pseudonym for privacy
  3. Complete Age Verification Early
    • Required for many features
    • One-time process
    • Logged for compliance

Profile Optimization

  1. Complete Onboarding
    • Personalizes your experience
    • Helps with content recommendations
    • Quick 2-minute process
  2. Monitor Credit Balance
    • Set up low balance alerts
    • Track usage patterns
    • Plan tier upgrades if needed
  3. Use Referral Code
    • Share to earn free credits
    • Help grow the community
    • Track your referral success

Client-Side Integration

import { useAuth } from '@/lib/AuthContext';

function UserProfile() {
  const { user, refreshUser } = useAuth();
  const [isEditing, setIsEditing] = useState(false);
  
  const handleUpdate = async (updates) => {
    const response = await fetch('/api/auth/user', {
      method: 'PATCH',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates)
    });
    
    if (response.ok) {
      await refreshUser(); // Refresh auth context
      setIsEditing(false);
    }
  };
  
  return (
    <div>
      <h1>{user.firstName} {user.lastName}</h1>
      <img src={user.profileImageUrl} alt="Profile" />
      <p>Email: {user.email}</p>
      <p>Credits: {user.credits.balance}</p>
      <p>Tier: {user.credits.tier}</p>
      {isEditing ? (
        <EditForm onSave={handleUpdate} onCancel={() => setIsEditing(false)} />
      ) : (
        <button onClick={() => setIsEditing(true)}>Edit Profile</button>
      )}
    </div>
  );
}

Build docs developers (and LLMs) love