Unclassified

Work Router - Migration & Enhancement Guide

This guide explains how to evolve the Work Router system from development (InMemory) to production (Drizzle + Neon), and how to add real data/features.

Phase 1: Auth Integration (Required First)

Current State

  • Demo user hardcoded in pages
  • No real Descope integration

Migration Steps

Step 1: Create Auth Helper

File: lib/auth.ts


export async function getAuthenticatedUser() {
  try {
    const user = await getSessionUser();
    if (!user) return null;

    return {
      userId: user.loginIds?.[0] || user.userId,
      email: user.email,
      tenantId: user.customAttributes?.tenantId || "default-tenant",
      name: user.name || "User",
    };
  } catch (error) {
    console.error("[v0] Auth error:", error);
    return null;
  }
}

Step 2: Update Onboarding Page

File: app/onboarding/page.tsx


  const user = await getAuthenticatedUser();

  if (!user) {
    redirect('/auth/login');
  }

  return (
    <OnboardingContainer
      userId={user.userId}
      tenantId={user.tenantId}
      initialName={user.name}
      initialEmail={user.email}
    />
  );
}

Step 3: Update Work Page

File: app/work/page.tsx


  const user = await getAuthenticatedUser();

  if (!user) {
    redirect('/auth/login');
  }

  // Convert to server component
  return <WorkViewContainer userId={user.userId} tenantId={user.tenantId} />;
}

Test: Visit /onboarding → Descope login → Complete flow → Redirects to /work


Phase 2: Real Sections (Medium Priority)

Current State

  • 50+ placeholder section components
  • All sections show "coming soon" text

Migration Strategy

Step 1: Identify Section Templates

Find existing dashboard/section implementations in the codebase to use as reference:

  • Sales dashboard template
  • Customer success section
  • Financial dashboard
  • Operations queue

Step 2: Create Section Components

File structure:

components/work/sections/
├── company-overview.tsx
├── revenue.tsx
├── pipeline.tsx
├── accounts.tsx
├── opportunities.tsx
├── campaigns.tsx
├── operations/
│   ├── tasks.tsx
│   ├── queues.tsx
│   └── approvals.tsx
└── admin/
    ├── users.tsx
    ├── roles.tsx
    └── audit.tsx

Step 3: Update Section Map

File: app/work/page.tsx

// ... import all sections

const SECTION_COMPONENTS: Record<string, React.ReactNode> = {
  'company-overview': <CompanyOverviewSection />,
  'revenue': <RevenueSection />,
  'pipeline': <PipelineSection />,
  // ... all sections
};

Example Section Component

// components/work/sections/revenue.tsx

  const [data, setData] = useState(null);

  useEffect(() => {
    async function loadData() {
      // Call your API
      const response = await fetch('/api/metrics/revenue');
      setData(await response.json());
    }
    loadData();
  }, []);

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-2 gap-4">
        <div className="p-3 bg-secondary rounded">
          <div className="text-xs text-muted-foreground">MRR</div>
          <div className="text-lg font-semibold">${data?.mrr || '—'}</div>
        </div>
        <div className="p-3 bg-secondary rounded">
          <div className="text-xs text-muted-foreground">ARR</div>
          <div className="text-lg font-semibold">${data?.arr || '—'}</div>
        </div>
      </div>
      
    </div>
  );
}

Phase 3: Database Migration (Production)

Current State

  • InMemoryDataProvider (ephemeral)
  • No persistence across sessions

Goal

  • DrizzleDataProvider with Neon Postgres
  • Persistent user profiles and audit logs

Migration Steps

Step 1: Setup Neon

Already configured in project (env vars present)

Step 2: Create Drizzle Schema

File: lib/data/schema.ts


  id: uuid("id").primaryKey().defaultRandom(),
  tenantId: uuid("tenant_id").notNull(),
  descopeUserId: text("descope_user_id").notNull(),
  name: text("name"),
  email: text("email"),
  avatarUrl: text("avatar_url"),
  onboardedAt: timestamp("onboarded_at"),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").notNull(),
  tenantId: uuid("tenant_id").notNull(),
  name: text("name").notNull(),
  email: text("email").notNull(),
  role: varchar("role", { length: 50 }).notNull(),
  department: varchar("department", { length: 50 }).notNull(),
  industry: varchar("industry", { length: 50 }).notNull(),
  onboardedAt: timestamp("onboarded_at").notNull(),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

  id: uuid("id").primaryKey().defaultRandom(),
  tenantId: uuid("tenant_id").notNull(),
  actorId: uuid("actor_id").notNull(),
  action: text("action").notNull(),
  entity: text("entity").notNull(),
  entityId: text("entity_id"),
  metadata: jsonb("metadata"),
  createdAt: timestamp("created_at").defaultNow(),
});

// ... other tables

Step 3: Create DrizzleDataProvider

File: lib/data/drizzle-provider.ts

import { db } from "@/lib/db"; // Your Drizzle instance

export class DrizzleDataProvider implements DataProvider {
  profiles = {
    async get(userId: string): Promise<UserProfile | null> {
      const result = await db
        .select()
        .from(userProfiles)
        .where(eq(userProfiles.userId, userId))
        .limit(1);

      return result[0] || null;
    },

    async create(profile: Omit<UserProfile, "id" | "createdAt" | "updatedAt">) {
      const result = await db.insert(userProfiles).values(profile).returning();

      return result[0];
    },

    // ... other methods
  };

  // ... other repos
}

Step 4: Update Provider Factory

File: lib/data/index.ts


let provider: DataProvider | null = null;

  if (!provider) {
    const isProd = getEnv("NODE_ENV") === "production";
    provider = isProd ? new DrizzleDataProvider() : new InMemoryDataProvider();
  }
  return provider;
}

Step 5: Create Migrations

File: migrations/0001_initial.sql

-- Create tenants table
CREATE TABLE IF NOT EXISTS tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(255) UNIQUE NOT NULL,
  industry VARCHAR(50),
  environment VARCHAR(50),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Create users table
CREATE TABLE IF NOT EXISTS users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  descope_user_id TEXT UNIQUE NOT NULL,
  name TEXT,
  email TEXT,
  avatar_url TEXT,
  onboarded_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Create user_profiles table
CREATE TABLE IF NOT EXISTS user_profiles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  role VARCHAR(50) NOT NULL,
  department VARCHAR(50) NOT NULL,
  industry VARCHAR(50) NOT NULL,
  onboarded_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Create indexes
CREATE INDEX idx_user_profiles_user_id ON user_profiles(user_id);
CREATE INDEX idx_user_profiles_tenant_id ON user_profiles(tenant_id);
CREATE INDEX idx_users_tenant_id ON users(tenant_id);

Step 6: Run Migrations

# Using Drizzle CLI
pnpm drizzle-kit push

# Or manually via Neon console

Step 7: Test

  • Switch to production mode
  • Test onboarding flow
  • Verify data persists in Neon
  • Verify profile loads on subsequent visits

Phase 4: Permissions Engine

Current State

  • Basic section gating by permissions
  • No fine-grained action control

Enhancement

Step 1: Expand Permission Model

File: lib/data/types.ts

export interface Permission {
  id: string;
  key: string;
  resource: string; // 'section', 'action', 'feature'
  scope: "global" | "tenant" | "user";
  description: string;
  createdAt: string;
}

export interface RolePermissionGrant {
  id: string;
  roleId: string;
  permissionId: string;
  deniedAt?: string; // Soft revoke
  createdAt: string;
}

Step 2: Update Router

File: lib/work/router.ts

  permissions: Permission[],
  resource: string,
  action: string
): boolean {
  const key = `${resource}:${action}`;
  return permissions.some((p) => p.key === key);
}

// In resolveWorkView:
export async function resolveWorkView(context: WorkContext): Promise<ResolvedWorkView> {
  // ... existing code ...

  // For each section, check if user can access
  let filteredSections = roleDefinition.sections.filter((section) => {
    // Existing permission checks...

    // New fine-grained checks
    if (!canUserAction(permissions, section.id, "view")) {
      return false;
    }

    return true;
  });

  return {
    /* ... */
  };
}

Step 3: Gate Actions in Components

// components/work/sections/revenue.tsx
  const canExport = permissions.some(p => p.key === 'revenue:export');
  const canEdit = permissions.some(p => p.key === 'revenue:edit');

  return (
    <div>
      
      {canExport && <button>Export</button>}
      {canEdit && <button>Edit Targets</button>}
    </div>
  );
}

Phase 5: Settings & Preferences

Add Settings Page

File: app/settings/page.tsx

  const user = await getAuthenticatedUser();
  const provider = getDataProvider();

  const profile = await provider.profiles.get(user.userId);

  return (
    <div>
      <h1>Workspace Settings</h1>

      
      <UserProfileForm profile={profile} />

      
      <WorkspacePreferencesForm profile={profile} />

      
      <ActivityLog userId={user.userId} />
    </div>
  );
}

Implementation Roadmap

PhasePriorityEffortDependenciesBlocker
Auth IntegrationCRITICAL2-4hDescope SDKYES
Real SectionsHIGH3-5 daysSection templatesNO
DatabaseHIGH2-3 daysNeon setupNO
PermissionsMEDIUM1-2 daysDB schemaNO
SettingsMEDIUM1 dayPermissionsNO
Audit LoggingLOW1 dayDB schemaNO

Testing Checklist for Each Phase

Phase 1: Auth

  • Descope login redirects to onboarding
  • Auth token passed to backend
  • TenantId correctly set
  • Logout works and redirects to login

Phase 2: Sections

  • Each section renders without errors
  • Section data loads from API
  • Section state updates correctly
  • Error states display gracefully
  • All 50+ sections work

Phase 3: Database

  • Profile persists across sessions
  • Multiple users have separate profiles
  • Audit logs record all actions
  • Data is tenant-scoped
  • No data leaks between tenants

Phase 4: Permissions

  • Sections hidden when lacking permissions
  • Actions hidden when lacking permissions
  • Permission inheritance from role works
  • Permission revocation works
  • Upselling shows for restricted features

Phase 5: Settings

  • User can update profile
  • User can change preferences
  • Activity feed shows history
  • Changes audit logged
  • Permissions respected

Troubleshooting

"Profile not found" error

  • Check Descope user is in tenant
  • Verify onboarding was completed
  • Check DataProvider is initialized
  • In Drizzle phase: verify DB migrations ran

"Sections not rendering"

  • Check section component imports
  • Verify SECTION_COMPONENTS map is complete
  • Check for TypeScript errors
  • Verify data API is responding

"Permission denied" errors

  • Check role-permission mappings in seed data
  • Verify permission keys match
  • In Drizzle phase: check DB seeding

Database connection errors

  • Verify Neon credentials in env
  • Check network connectivity
  • Verify migrations ran successfully
  • Check SQL syntax in schema

Performance Optimization Tips

  1. Caching - Cache resolved views in Redis
  2. Lazy Loading - Load sections on-demand not all at once
  3. Image Optimization - Use Next.js Image for section icons
  4. Data Fetching - Use SWR for section data
  5. Query Optimization - Add indexes for frequently queried fields

Deployment Checklist

  • All sections implemented
  • Auth integrated and tested
  • Database migrations deployed
  • Permissions configured
  • Audit logging enabled
  • Error monitoring set up
  • Performance tested
  • Security review passed
  • User documentation complete
  • Staged rollout plan created

For questions or issues, refer to:

  • WORK_ROUTER_IMPLEMENTATION.md - Technical details
  • WORK_ROUTER_QUICKSTART.md - Quick reference
  • WORK_ROUTER_DELIVERY.md - What's delivered