Skip to main content
Winnerr’s authentication system is built on Clerk with comprehensive multi-tenant organization support, providing secure access control for real estate brokerages of all sizes. The system ensures complete data isolation between organizations while maintaining seamless user experiences.

Authentication Architecture

Core Components

Clerk Integration

Modern authentication with social logins, MFA, and organization management

Multi-tenancy

Complete data isolation between real estate organizations

Role-based Access

Granular permissions for admins, agents, and viewers

API Security

JWT tokens with organization context validation

Authentication Flow

Organization Management

Organization Structure

interface Organization {
  id: string;
  name: string;
  slug: string;
  logo?: string;
  settings: OrganizationSettings;
  members: OrganizationMembership[];
  createdAt: Date;
  updatedAt: Date;
}

interface OrganizationMembership {
  id: string;
  userId: string;
  organizationId: string;
  role: 'ADMIN' | 'AGENT' | 'VIEWER';
  permissions: Permission[];
  joinedAt: Date;
}

Organization Creation Flow

// Organization creation process
export async function createOrganization(data: CreateOrgData) {
  // 1. Create Clerk organization
  const clerkOrg = await clerkClient.organizations.createOrganization({
    name: data.name,
    slug: data.slug,
    createdBy: data.userId
  });
  
  // 2. Create database record
  const organization = await database.organization.create({
    data: {
      id: clerkOrg.id,
      name: data.name,
      slug: data.slug,
      settings: {
        create: {
          timezone: data.timezone,
          businessHours: data.businessHours
        }
      }
    }
  });
  
  // 3. Set up initial permissions
  await setupDefaultPermissions(organization.id, data.userId);
  
  // 4. Initialize Twilio sub-account
  await createTwilioSubAccount(organization.id);
  
  return organization;
}
export async function inviteMember(
  orgId: string, 
  email: string, 
  role: OrganizationRole
) {
  // 1. Send Clerk invitation
  const invitation = await clerkClient.organizations.createOrganizationInvitation({
    organizationId: orgId,
    emailAddress: email,
    role: role.toLowerCase() as ClerkRole
  });
  
  // 2. Create pending membership
  await database.organizationMembership.create({
    data: {
      organizationId: orgId,
      invitationId: invitation.id,
      role,
      status: 'PENDING'
    }
  });
  
  // 3. Send welcome email
  await sendInvitationEmail(email, orgId, role);
}

Role-Based Access Control

Permission System

enum Permission {
  // Contact Management
  CONTACTS_VIEW = 'contacts:view',
  CONTACTS_CREATE = 'contacts:create',
  CONTACTS_EDIT = 'contacts:edit',
  CONTACTS_DELETE = 'contacts:delete',
  
  // Deal Management
  DEALS_VIEW = 'deals:view',
  DEALS_CREATE = 'deals:create',
  DEALS_EDIT = 'deals:edit',
  DEALS_DELETE = 'deals:delete',
  DEALS_VIEW_COMMISSIONS = 'deals:view_commissions',
  
  // Property Management
  PROPERTIES_VIEW = 'properties:view',
  PROPERTIES_CREATE = 'properties:create',
  PROPERTIES_EDIT = 'properties:edit',
  PROPERTIES_DELETE = 'properties:delete',
  
  // Communication
  COMMUNICATIONS_VIEW = 'communications:view',
  COMMUNICATIONS_SEND = 'communications:send',
  COMMUNICATIONS_EXPORT = 'communications:export',
  
  // Organization Management
  ORG_VIEW_MEMBERS = 'org:view_members',
  ORG_INVITE_MEMBERS = 'org:invite_members',
  ORG_REMOVE_MEMBERS = 'org:remove_members',
  ORG_EDIT_SETTINGS = 'org:edit_settings',
  ORG_VIEW_BILLING = 'org:view_billing',
  
  // Phone System
  PHONE_MAKE_CALLS = 'phone:make_calls',
  PHONE_VIEW_RECORDINGS = 'phone:view_recordings',
  PHONE_MANAGE_NUMBERS = 'phone:manage_numbers',
  
  // AI Features
  AI_USE_VOICE_SEARCH = 'ai:use_voice_search',
  AI_VIEW_SENTIMENT = 'ai:view_sentiment',
  AI_EXPORT_INSIGHTS = 'ai:export_insights'
}

Role Definitions

Full access to all organization features:
  • All contact, deal, and property management
  • Organization settings and member management
  • Billing and subscription management
  • Phone system configuration
  • AI features and analytics
  • Data export and reporting
const ADMIN_PERMISSIONS = [
  Permission.CONTACTS_VIEW,
  Permission.CONTACTS_CREATE,
  Permission.CONTACTS_EDIT,
  Permission.CONTACTS_DELETE,
  // ... all permissions
];
Access to CRM features with some restrictions:
  • Full contact and deal management
  • Property viewing and creation
  • Communication features
  • Phone system usage
  • AI features for productivity
  • Limited analytics access
const AGENT_PERMISSIONS = [
  Permission.CONTACTS_VIEW,
  Permission.CONTACTS_CREATE,
  Permission.CONTACTS_EDIT,
  Permission.DEALS_VIEW,
  Permission.DEALS_CREATE,
  Permission.DEALS_EDIT,
  Permission.COMMUNICATIONS_VIEW,
  Permission.COMMUNICATIONS_SEND,
  Permission.PHONE_MAKE_CALLS,
  Permission.AI_USE_VOICE_SEARCH,
  // ... agent-specific permissions
];
Read-only access for assistants and support staff:
  • View contacts and deals
  • View properties and communications
  • Limited phone system access
  • Basic AI features
const VIEWER_PERMISSIONS = [
  Permission.CONTACTS_VIEW,
  Permission.DEALS_VIEW,
  Permission.PROPERTIES_VIEW,
  Permission.COMMUNICATIONS_VIEW,
  Permission.AI_USE_VOICE_SEARCH
];

API Authentication

Middleware Implementation

// apps/api/middleware.ts
import { auth } from '@repo/auth/server';
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  // Skip auth for public routes
  if (isPublicRoute(request.nextUrl.pathname)) {
    return NextResponse.next();
  }
  
  try {
    // Verify authentication
    const { userId, orgId } = await auth();
    
    if (!userId) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }
    
    // Add context to request headers
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('x-user-id', userId);
    if (orgId) {
      requestHeaders.set('x-organization-id', orgId);
    }
    
    return NextResponse.next({
      request: {
        headers: requestHeaders
      }
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Authentication failed' },
      { status: 401 }
    );
  }
}

API Route Protection

// Standard authenticated API route
export async function POST(req: Request) {
  try {
    // 1. Verify authentication and get context
    const { userId, orgId } = await auth();
    
    if (!userId || !orgId) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }
    
    // 2. Parse and validate input
    const data = await req.json();
    const validatedData = createPersonSchema.parse(data);
    
    // 3. Check permissions
    await requirePermission(userId, orgId, Permission.CONTACTS_CREATE);
    
    // 4. Execute business logic with organization context
    const person = await database.person.create({
      data: {
        ...validatedData,
        organizationId: orgId, // Enforce organization isolation
        createdBy: userId
      }
    });
    
    // 5. Return response
    return NextResponse.json(person);
  } catch (error) {
    if (error instanceof PermissionError) {
      return NextResponse.json(
        { error: 'Insufficient permissions' },
        { status: 403 }
      );
    }
    
    log.error('API error', { error: parseError(error) });
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Multi-tenant Data Isolation

Database-Level Isolation

-- Row-level security policies ensure organization isolation
CREATE POLICY person_organization_isolation ON "Person"
  FOR ALL USING (
    "organizationId" = current_setting('app.current_organization')::text
  );

CREATE POLICY deal_organization_isolation ON "Deal"
  FOR ALL USING (
    "organizationId" = current_setting('app.current_organization')::text
  );

Application-Level Enforcement

// Database utility with automatic organization filtering
export class OrganizationDatabase {
  constructor(private orgId: string) {}
  
  get person() {
    return {
      findMany: (args?: Prisma.PersonFindManyArgs) => {
        return database.person.findMany({
          ...args,
          where: {
            ...args?.where,
            organizationId: this.orgId // Automatically enforce isolation
          }
        });
      },
      
      create: (args: Prisma.PersonCreateArgs) => {
        return database.person.create({
          ...args,
          data: {
            ...args.data,
            organizationId: this.orgId // Automatically set organization
          }
        });
      }
      // ... other methods
    };
  }
}

// Usage in API routes
export async function GET(req: Request) {
  const { userId, orgId } = await auth();
  const db = new OrganizationDatabase(orgId);
  
  const people = await db.person.findMany({
    include: { deals: true }
  });
  
  return NextResponse.json(people);
}

Client-Side Authentication

React Components

// Auth-aware component
import { useAuth } from '@repo/auth';

export function DashboardPage() {
  const { user, organization, isLoaded } = useAuth();
  
  if (!isLoaded) return <LoadingSpinner />;
  if (!user) return <SignInRedirect />;
  if (!organization) return <OrganizationSetup />;
  
  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>Organization: {organization.name}</p>
      <DashboardContent />
    </div>
  );
}

API Calls with Authentication

// Auth-aware fetch utility
import { useAuth } from '@repo/auth';

export function useAuthenticatedFetch() {
  const { getToken } = useAuth();
  
  return useCallback(async (url: string, options: RequestInit = {}) => {
    const token = await getToken();
    
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });
  }, [getToken]);
}

// Usage in components
export function ContactsList() {
  const authFetch = useAuthenticatedFetch();
  
  const { data: contacts } = useSWR('/api/people', authFetch);
  
  return (
    <div>
      {contacts?.map(contact => (
        <ContactCard key={contact.id} contact={contact} />
      ))}
    </div>
  );
}

Security Features

Session Management

Secure Sessions

JWT tokens with configurable expiration and automatic refresh

Multi-device Support

Secure sessions across web, mobile, and Chrome extension

Session Revocation

Immediate session termination for security incidents

Activity Monitoring

Login tracking and suspicious activity detection

Additional Security Measures

// Rate limiting for authentication endpoints
export const authRateLimit = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: 'Too many authentication attempts',
  standardHeaders: true,
  legacyHeaders: false
});

// IP allowlist for sensitive operations
export function requireIPAllowlist(allowedIPs: string[]) {
  return (req: Request) => {
    const clientIP = getClientIP(req);
    if (!allowedIPs.includes(clientIP)) {
      throw new SecurityError('IP not allowed');
    }
  };
}

// Device fingerprinting for fraud detection
export function trackDeviceFingerprint(req: Request) {
  const fingerprint = generateFingerprint({
    userAgent: req.headers.get('user-agent'),
    acceptLanguage: req.headers.get('accept-language'),
    // ... other headers
  });
  
  return fingerprint;
}

Organization Switching

Seamless Organization Context

// Organization context provider
export function OrganizationProvider({ children }: { children: React.ReactNode }) {
  const { user, organization } = useAuth();
  const [activeOrg, setActiveOrg] = useState(organization);
  
  const switchOrganization = useCallback(async (orgId: string) => {
    // Update Clerk session
    await setActiveOrganization({ organizationId: orgId });
    
    // Update local state
    setActiveOrg(orgId);
    
    // Refresh application data
    await revalidateOrganizationData();
  }, []);
  
  return (
    <OrganizationContext.Provider value={{
      activeOrganization: activeOrg,
      switchOrganization
    }}>
      {children}
    </OrganizationContext.Provider>
  );
}

Next Steps

Database Design

Explore the multi-tenant database schema

API Security

Learn about API security patterns and best practices

Real-time Features

Understand authenticated real-time communication

Security Note: Never store sensitive authentication data in client-side storage. Always use secure HTTP-only cookies or rely on Clerk’s session management.