Date: 2025-22-09

Status: accepted

Current Architecture Analysis

The Vega web application is built with Remix (React Router v7) and follows a feature-based routing structure with the following characteristics:

Current Structure

services/remix/
├── app/
│   ├── components/           # Shared components
│   │   ├── ui/              # Design system components (shadcn/ui based)
│   │   ├── chat/            # Chat-related components
│   │   ├── forms/           # Form components
│   │   └── shared/          # Shared business components
│   ├── routes/              # Route-based organization
│   │   ├── alerts+/         # Alerts feature
│   │   ├── dashboards+/     # Dashboards feature
│   │   ├── detections+/     # Detections feature
│   │   ├── assessment+/       # Data assessment feature
│   │   ├── intel+/          # Intelligence feature
│   │   ├── notebooks+/      # Notebooks feature
│   │   ├── query+/          # Query feature
│   │   ├── settings+/       # Settings feature
│   │   └── _auth+/          # Authentication
│   ├── queries/             # GraphQL queries
│   ├── utils/               # Utility functions
│   └── transformations/     # Data transformations

Key Technologies

  • Framework: Remix (React Router v7) with Vite
  • UI Library: shadcn/ui components + Radix UI primitives
  • State Management: Zustand + React Query
  • Styling: Tailwind CSS
  • Data Fetching: GraphQL with custom hooks
  • Testing: Vitest + Playwright

Feature-Sliced Design (FSD) Migration

What is Feature-Sliced Design?

Feature-Sliced Design is a methodology for frontend project architecture that organizes code by features and slices, providing clear rules for structuring applications. It emphasizes:

  • Slices: Business logic units (features, entities, widgets)
  • Layers: Technical layers (app, pages, widgets, features, entities, shared)
  • Segments: Code organization within slices (ui, model, lib, api)

FSD Layer Structure

src/
├── app/                     # Application layer
│   ├── providers/           # Global providers
│   ├── router/             # Routing configuration
│   └── styles/             # Global styles
├── pages/                   # Pages layer
│   ├── dashboard/          # Dashboard page
│   ├── detections/         # Detections page
│   └── settings/           # Settings page
├── widgets/                 # Widgets layer
│   ├── navigation/          # Navigation widget
│   ├── dashboard-grid/     # Dashboard grid widget
│   └── detection-list/     # Detection list widget
├── features/                # Features layer
│   ├── auth/               # Authentication feature
│   ├── create-detection/   # Create detection feature
│   ├── dashboard-edit/     # Dashboard editing feature
│   └── query-execution/    # Query execution feature
├── entities/                # Entities layer
│   ├── user/               # User entity
│   ├── detection/           # Detection entity
│   ├── dashboard/          # Dashboard entity
│   └── connector/          # Connector entity
└── shared/                  # Shared layer
    ├── ui/                 # UI components
    ├── lib/                # Libraries and utilities
    ├── api/                # API layer
    └── config/             # Configuration

Migration Strategy

Phase 1: Setup FSD Infrastructure

Step 1: Create Base FSD Structure
// src/shared/lib/fsd/index.ts
export { createSlice } from './create-slice';
export { createFeature } from './create-feature';
export { createEntity } from './create-entity';
export { createWidget } from './create-widget';

// src/shared/lib/fsd/create-slice.ts
export interface SliceConfig {
  name: string;
  layer: 'shared' | 'entities' | 'features' | 'widgets' | 'pages' | 'app';
  segments: string[];
}

export function createSlice(config: SliceConfig) {
  return {
    ...config,
    path: `src/${config.layer}/${config.name}`,
  };
}
Step 2: Create Segment Templates
// src/shared/lib/fsd/segments/ui.tsx
export interface UISegmentProps {
  children: React.ReactNode;
  className?: string;
}

// src/shared/lib/fsd/segments/model.ts
export interface ModelSegment {
  store: any;
  hooks: Record<string, Function>;
  selectors: Record<string, Function>;
}

// src/shared/lib/fsd/segments/api.ts
export interface APISegment {
  endpoints: Record<string, Function>;
  types: Record<string, any>;
}

Phase 2: Extract Shared Layer

Step 1: Move UI Components
// src/shared/ui/button/index.ts
export { Button } from './ui/Button';
export type { ButtonProps } from './model/types';

// src/shared/ui/button/ui/Button.tsx
import { cn } from '@/shared/lib/utils';
import type { ButtonProps } from '../model/types';

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
        className
      )}
      {...props}
    />
  );
}

// src/shared/ui/button/model/types.ts
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
  size?: 'default' | 'sm' | 'lg' | 'icon';
}
Step 2: Create Shared Libraries
// src/shared/lib/utils/index.ts
export { cn } from './cn';
export { formatDate } from './date';
export { debounce } from './debounce';

// src/shared/lib/cn.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
Step 3: Setup API Layer
// src/shared/api/base.ts
import axios from 'axios';

export const api = axios.create({
  baseURL: process.env.API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// src/shared/api/graphql.ts
import { GraphQLClient } from 'graphql-request';

export const graphqlClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);

Phase 3: Extract Entities Layer

Step 1: User Entity
// src/entities/user/index.ts
export { UserCard } from './ui/UserCard';
export { useUser } from './model/useUser';
export { userStore } from './model/userStore';
export type { User } from './model/types';

// src/entities/user/model/types.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: string;
  permissions: string[];
}

// src/entities/user/model/userStore.ts
import { create } from 'zustand';
import type { User } from './types';

interface UserState {
  user: User | null;
  isLoading: boolean;
  setUser: (user: User | null) => void;
  setLoading: (loading: boolean) => void;
}

export const userStore = create<UserState>((set) => ({
  user: null,
  isLoading: false,
  setUser: (user) => set({ user }),
  setLoading: (isLoading) => set({ isLoading }),
}));

// src/entities/user/model/useUser.ts
import { useUserStore } from './userStore';
import { userApi } from '../api/userApi';

export function useUser() {
  const { user, isLoading, setUser, setLoading } = useUserStore();
  
  const fetchUser = async () => {
    setLoading(true);
    try {
      const userData = await userApi.getCurrentUser();
      setUser(userData);
    } finally {
      setLoading(false);
    }
  };
  
  return { user, isLoading, fetchUser };
}

// src/entities/user/api/userApi.ts
import { graphqlClient } from '@/shared/api/graphql';
import { GET_CURRENT_USER } from './queries';
import type { User } from '../model/types';

export const userApi = {
  getCurrentUser: async (): Promise<User> => {
    const data = await graphqlClient.request(GET_CURRENT_USER);
    return data.me;
  },
};
Step 2: Detection Entity
// src/entities/detection/index.ts
export { DetectionCard } from './ui/DetectionCard';
export { DetectionList } from './ui/DetectionList';
export { useDetection } from './model/useDetection';
export { detectionStore } from './model/detectionStore';
export type { Detection } from './model/types';

// src/entities/detection/model/types.ts
export interface Detection {
  id: string;
  name: string;
  description: string;
  query: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
  status: 'active' | 'inactive' | 'draft';
  createdAt: string;
  updatedAt: string;
  mitreTechniques: string[];
}

// src/entities/detection/model/detectionStore.ts
import { create } from 'zustand';
import type { Detection } from './types';

interface DetectionState {
  detections: Detection[];
  currentDetection: Detection | null;
  isLoading: boolean;
  setDetections: (detections: Detection[]) => void;
  setCurrentDetection: (detection: Detection | null) => void;
  setLoading: (loading: boolean) => void;
}

export const detectionStore = create<DetectionState>((set) => ({
  detections: [],
  currentDetection: null,
  isLoading: false,
  setDetections: (detections) => set({ detections }),
  setCurrentDetection: (currentDetection) => set({ currentDetection }),
  setLoading: (isLoading) => set({ isLoading }),
}));

Phase 4: Extract Features Layer

Step 1: Authentication Feature
// src/features/auth/index.ts
export { LoginForm } from './ui/LoginForm';
export { LogoutButton } from './ui/LogoutButton';
export { useAuth } from './model/useAuth';
export { authStore } from './model/authStore';

// src/features/auth/model/useAuth.ts
import { useAuthStore } from './authStore';
import { authApi } from '../api/authApi';
import { userStore } from '@/entities/user';

export function useAuth() {
  const { isAuthenticated, token, setAuth, clearAuth } = useAuthStore();
  const { setUser } = userStore();
  
  const login = async (credentials: LoginCredentials) => {
    try {
      const { token, user } = await authApi.login(credentials);
      setAuth(token);
      setUser(user);
    } catch (error) {
      throw error;
    }
  };
  
  const logout = async () => {
    await authApi.logout();
    clearAuth();
    setUser(null);
  };
  
  return { isAuthenticated, token, login, logout };
}

// src/features/auth/ui/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';
import { useAuth } from '../model/useAuth';

export function LoginForm() {
  const { login } = useAuth();
  const { register, handleSubmit } = useForm();
  
  const onSubmit = async (data: any) => {
    try {
      await login(data);
    } catch (error) {
      // Handle error
    }
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Input {...register('email')} placeholder="Email" />
      <Input {...register('password')} type="password" placeholder="Password" />
      <Button type="submit">Login</Button>
    </form>
  );
}
Step 2: Create Detection Feature
// src/features/create-detection/index.ts
export { CreateDetectionForm } from './ui/CreateDetectionForm';
export { DetectionWizard } from './ui/DetectionWizard';
export { useCreateDetection } from './model/useCreateDetection';

// src/features/create-detection/model/useCreateDetection.ts
import { detectionStore } from '@/entities/detection';
import { detectionApi } from '@/entities/detection/api';

export function useCreateDetection() {
  const { setDetections } = detectionStore();
  
  const createDetection = async (data: CreateDetectionData) => {
    try {
      const newDetection = await detectionApi.create(data);
      setDetections(prev => [...prev, newDetection]);
      return newDetection;
    } catch (error) {
      throw error;
    }
  };
  
  return { createDetection };
}

Phase 5: Extract Widgets Layer

Step 1: Navigation Widget
// src/widgets/navigation/index.ts
export { Navigation } from './ui/Navigation';
export { NavigationItem } from './ui/NavigationItem';
export { useNavigation } from './model/useNavigation';

// src/widgets/navigation/ui/Navigation.tsx
import { NavigationItem } from './NavigationItem';
import { useNavigation } from '../model/useNavigation';

export function Navigation() {
  const { navigationItems } = useNavigation();
  
  return (
    <nav className="flex flex-col space-y-2">
      {navigationItems.map((item) => (
        <NavigationItem key={item.path} item={item} />
      ))}
    </nav>
  );
}

// src/widgets/navigation/model/useNavigation.ts
import { useMemo } from 'react';
import { useUser } from '@/entities/user';
import { useFeatureFlags } from '@/shared/lib/feature-flags';

export function useNavigation() {
  const { user } = useUser();
  const { featureFlags } = useFeatureFlags();
  
  const navigationItems = useMemo(() => {
    return [
      { path: '/dashboard', label: 'Dashboard', visible: true },
      { path: '/detections', label: 'Detections', visible: true },
      { path: '/query', label: 'Query', visible: featureFlags.FEDERATED_SEARCH },
      { path: '/settings', label: 'Settings', visible: user?.role === 'admin' },
    ].filter(item => item.visible);
  }, [user, featureFlags]);
  
  return { navigationItems };
}
Step 2: Dashboard Grid Widget
// src/widgets/dashboard-grid/index.ts
export { DashboardGrid } from './ui/DashboardGrid';
export { DashboardWidget } from './ui/DashboardWidget';
export { useDashboardGrid } from './model/useDashboardGrid';

// src/widgets/dashboard-grid/ui/DashboardGrid.tsx
import { DashboardWidget } from './DashboardWidget';
import { useDashboardGrid } from '../model/useDashboardGrid';

export function DashboardGrid() {
  const { widgets, layout, updateLayout } = useDashboardGrid();
  
  return (
    <div className="grid grid-cols-12 gap-4">
      {widgets.map((widget) => (
        <DashboardWidget key={widget.id} widget={widget} />
      ))}
    </div>
  );
}

Phase 6: Extract Pages Layer

Step 1: Dashboard Page
// src/pages/dashboard/index.ts
export { DashboardPage } from './ui/DashboardPage';

// src/pages/dashboard/ui/DashboardPage.tsx
import { Page } from '@/shared/ui/page';
import { DashboardGrid } from '@/widgets/dashboard-grid';
import { Navigation } from '@/widgets/navigation';

export function DashboardPage() {
  return (
    <Page>
      <div className="flex h-full">
        <Navigation />
        <main className="flex-1">
          <DashboardGrid />
        </main>
      </div>
    </Page>
  );
}
Step 2: Detections Page
// src/pages/detections/index.ts
export { DetectionsPage } from './ui/DetectionsPage';

// src/pages/detections/ui/DetectionsPage.tsx
import { Page } from '@/shared/ui/page';
import { DetectionList } from '@/entities/detection';
import { CreateDetectionForm } from '@/features/create-detection';
import { Navigation } from '@/widgets/navigation';

export function DetectionsPage() {
  return (
    <Page>
      <div className="flex h-full">
        <Navigation />
        <main className="flex-1">
          <CreateDetectionForm />
          <DetectionList />
        </main>
      </div>
    </Page>
  );
}

Phase 7: Update App Layer

Step 1: App Providers
// src/app/providers/index.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ThemeProvider } from './ThemeProvider';
import { AuthProvider } from './AuthProvider';

const queryClient = new QueryClient();

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeProvider>
        <AuthProvider>
          {children}
          <ReactQueryDevtools />
        </AuthProvider>
      </ThemeProvider>
    </QueryClientProvider>
  );
}
Step 2: App Router
// src/app/router/index.tsx
import { createBrowserRouter } from 'react-router-dom';
import { DashboardPage } from '@/pages/dashboard';
import { DetectionsPage } from '@/pages/detections';
import { SettingsPage } from '@/pages/settings';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <DashboardPage />,
  },
  {
    path: '/detections',
    element: <DetectionsPage />,
  },
  {
    path: '/settings',
    element: <SettingsPage />,
  },
]);

FSD Import Rules

Public API Pattern

Each slice should have a clear public API:

// src/entities/user/index.ts - Public API
export { UserCard } from './ui/UserCard';
export { useUser } from './model/useUser';
export type { User } from './model/types';

// Usage in other slices
import { UserCard, useUser, type User } from '@/entities/user';

Import Restrictions

// ✅ Allowed imports
import { Button } from '@/shared/ui/button';           // From shared
import { User } from '@/entities/user';                // From entities
import { useAuth } from '@/features/auth';             // From features
import { Navigation } from '@/widgets/navigation';      // From widgets
import { DashboardPage } from '@/pages/dashboard';    // From pages

// ❌ Forbidden imports
import { UserCard } from '@/entities/user/ui/UserCard'; // Direct import
import { authStore } from '@/features/auth/model/store'; // Direct import

Benefits of FSD

  1. Clear Architecture: Well-defined layers and slices
  2. Scalability: Easy to add new features and entities
  3. Maintainability: Clear separation of concerns
  4. Team Collaboration: Clear boundaries for different teams
  5. Reusability: Shared components and entities
  6. Testing: Easy to test individual slices

Migration Timeline

Week 1-2: Setup FSD infrastructure and extract shared layer Week 3-4: Extract entities layer (user, detection, dashboard) Week 5-6: Extract features layer (auth, create-detection, query-execution) Week 7-8: Extract widgets layer (navigation, dashboard-grid, detection-list) Week 9-10: Extract pages layer and update app layer Week 11-12: Testing, optimization, and documentation

Implementation Considerations

  1. Import Rules: Strictly follow FSD import rules
  2. Public APIs: Each slice should have a clear public API
  3. Layer Dependencies: Only allow imports from lower layers
  4. Testing Strategy: Test each slice independently
  5. Documentation: Document each slice’s purpose and API

Potential Challenges

  1. Learning Curve: Team needs to understand FSD principles
  2. Initial Overhead: Setting up the structure requires significant work
  3. Import Restrictions: Strict import rules might feel limiting
  4. Refactoring: Existing code needs significant refactoring
  5. Tooling: Need tools to enforce FSD rules

Success Metrics

  • Code Organization: Percentage of code properly organized by FSD layers
  • Import Violations: Number of FSD import rule violations
  • Feature Development: Time to implement new features
  • Code Reusability: Percentage of shared code across features
  • Team Productivity: Features delivered per team per sprint