# HRIStudio Technical Implementation Guide

## Overview

This guide provides step-by-step technical instructions for implementing HRIStudio using the T3 stack with Next.js, tRPC, Drizzle ORM, Better Auth, and supporting infrastructure.

## Table of Contents

1. [Project Setup](#project-setup)
2. [Development Environment](#development-environment)
3. [Database Implementation](#database-implementation)
4. [Authentication System](#authentication-system)
5. [tRPC API Implementation](#trpc-api-implementation)
6. [Frontend Architecture](#frontend-architecture)
7. [Real-time Features](#real-time-features)
8. [File Storage System](#file-storage-system)
9. [Plugin System](#plugin-system)
10. [Testing Strategy](#testing-strategy)
11. [Deployment](#deployment)
12. [Performance Optimization](#performance-optimization)
13. [Security Implementation](#security-implementation)

## Project Setup

### 1. Initialize Project

```bash
# Clone repository (includes robot-plugins as submodule)
git clone https://github.com/soconnor0919/hristudio.git
cd hristudio

# Initialize submodules
git submodule update --init --recursive

# Or create from scratch with T3 stack:
bunx create-t3-app@latest hristudio \
  --nextjs \
  --tailwind \
  --trpc \
  --drizzle \
  --app-router \
  --package-manager bun \
  --typescript

# Update to Next.js 15
cd hristudio
bun add next@15 react@rc react-dom@rc

cd hristudio
```

### 2. Install Additional Dependencies

```bash
# Core dependencies
bun add @auth/drizzle-adapter next-auth@beta
bun add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-label @radix-ui/react-select @radix-ui/react-slot @radix-ui/react-tabs @radix-ui/react-toast
bun add class-variance-authority clsx tailwind-merge
bun add nuqs zod react-hook-form @hookform/resolvers
bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
bun add ws @types/ws
bun add date-fns
bun add recharts
bun add roslib @types/roslib  # For ROS2 integration via rosbridge

# Development dependencies
bun add -d @types/node
bun add -d drizzle-kit
bun add -d @faker-js/faker
bun add -d vitest @vitest/ui
bun add -d @testing-library/react @testing-library/jest-dom
bun add -d playwright @playwright/test
```

### 3. Project Structure

```
src/
├── app/
│   ├── (auth)/
│   │   ├── login/
│   │   ├── register/
│   │   └── verify/
│   ├── (dashboard)/
│   │   ├── studies/
│   │   ├── experiments/
│   │   ├── trials/
│   │   └── admin/
│   ├── api/
│   │   ├── auth/[...nextauth]/
│   │   ├── trpc/[trpc]/
│   │   └── ws/
│   └── layout.tsx
├── components/
│   ├── ui/                    # Shadcn components
│   ├── experiment/
│   │   ├── designer/
│   │   ├── step-editor/
│   │   └── action-library/
│   ├── trial/
│   │   ├── wizard-interface/
│   │   ├── execution-panel/
│   │   └── quick-actions/
│   └── layout/
│       ├── navigation/
│       ├── sidebar/
│       └── breadcrumbs/
├── features/
│   ├── auth/
│   ├── studies/
│   ├── experiments/
│   ├── trials/
│   ├── participants/
│   ├── plugins/
│   └── analysis/
├── hooks/
├── lib/
│   ├── auth/
│   ├── db/
│   ├── trpc/
│   ├── storage/
│   └── websocket/
├── server/
│   ├── api/
│   └── db/
└── types/
```

## Development Environment

### 1. Environment Variables

Create `.env.local`:

```env
# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/hristudio?sslmode=disable"

# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32"

# OAuth Providers (optional)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
GITHUB_ID=""
GITHUB_SECRET=""

# S3/MinIO
S3_ENDPOINT="http://localhost:9000"
S3_ACCESS_KEY_ID="minioadmin"
S3_SECRET_ACCESS_KEY="minioadmin"
S3_BUCKET_NAME="hristudio"
S3_REGION="us-east-1"

# WebSocket
WS_URL="wss://your-app.vercel.app/api/ws"  # For production on Vercel

# ROS2 Bridge
ROSBRIDGE_URL="ws://localhost:9090"  # Local development
# ROSBRIDGE_URL="wss://your-ros-bridge.com:9090"  # Production

# Email (for production)
SMTP_HOST=""
SMTP_PORT=""
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM=""
```

### 2. Docker Services

Ensure Docker services are running:

```bash
docker-compose up -d
```

### 3. Initialize Database

```bash
# Generate database migrations
bun db:generate

# Run migrations
bun db:migrate

# Seed database (development)
bun db:seed
```

## Database Implementation

### 1. Drizzle Configuration

Create `src/lib/db/index.ts`:

```typescript
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const connectionString = process.env.DATABASE_URL!;
const sql = postgres(connectionString, { max: 1 });

export const db = drizzle(sql, { schema });
export type Database = typeof db;
```

### 2. Schema Definition

Create modular schema files:

`src/lib/db/schema/users.ts`:

```typescript
import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  emailVerified: timestamp('email_verified'),
  name: varchar('name', { length: 255 }),
  image: varchar('image'),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
  deletedAt: timestamp('deleted_at'),
});

export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
```

### 3. Relationships

`src/lib/db/schema/relations.ts`:

```typescript
import { relations } from 'drizzle-orm';
import { users, studies, studyMembers } from './index';

export const usersRelations = relations(users, ({ many }) => ({
  ownedStudies: many(studies),
  studyMemberships: many(studyMembers),
}));

export const studiesRelations = relations(studies, ({ one, many }) => ({
  creator: one(users, {
    fields: [studies.createdBy],
    references: [users.id],
  }),
  members: many(studyMembers),
  experiments: many(experiments),
}));
```

### 4. Database Utilities

`src/lib/db/utils.ts`:

```typescript
import { db } from './index';
import { sql } from 'drizzle-orm';

export async function withTransaction<T>(
  callback: (tx: typeof db) => Promise<T>
): Promise<T> {
  return await db.transaction(callback);
}

export async function checkUserPermission(
  userId: string,
  studyId: string,
  requiredPermission: string
): Promise<boolean> {
  const result = await db.execute(sql`
    SELECT check_user_permission(${userId}, ${studyId}, ${requiredPermission})
  `);
  return result.rows[0]?.check_user_permission ?? false;
}
```

## Authentication System

### 1. NextAuth Configuration

`src/lib/auth/config.ts`:

```typescript
import { NextAuthConfig } from 'next-auth';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import { db } from '@/lib/db';
import { users, accounts, sessions } from '@/lib/db/schema';
import { z } from 'zod';
import bcrypt from 'bcryptjs';

export const authConfig: NextAuthConfig = {
  adapter: DrizzleAdapter(db, {
    usersTable: users,
    accountsTable: accounts,
    sessionsTable: sessions,
  }),
  session: {
    strategy: 'database',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  pages: {
    signIn: '/login',
    signUp: '/register',
    error: '/auth/error',
    verifyRequest: '/auth/verify',
  },
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const parsed = z
          .object({
            email: z.string().email(),
            password: z.string().min(8),
          })
          .safeParse(credentials);

        if (!parsed.success) return null;

        const user = await db.query.users.findFirst({
          where: eq(users.email, parsed.data.email),
        });

        if (!user || !user.password) return null;

        const isValid = await bcrypt.compare(
          parsed.data.password,
          user.password
        );

        if (!isValid) return null;

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          image: user.image,
        };
      },
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async session({ session, user }) {
      // Add user roles to session
      const userWithRoles = await db.query.users.findFirst({
        where: eq(users.id, user.id),
        with: {
          systemRoles: true,
        },
      });

      session.user.id = user.id;
      session.user.roles = userWithRoles?.systemRoles || [];
      
      return session;
    },
  },
};
```

### 2. Auth Utilities

`src/lib/auth/utils.ts`:

```typescript
import { auth } from './index';
import { redirect } from 'next/navigation';

export async function requireAuth() {
  const session = await auth();
  if (!session) {
    redirect('/login');
  }
  return session;
}

export async function requireRole(role: SystemRole) {
  const session = await requireAuth();
  const hasRole = session.user.roles.some(r => r.role === role);
  
  if (!hasRole) {
    throw new Error('Insufficient permissions');
  }
  
  return session;
}
```

## tRPC API Implementation

### 1. tRPC Setup

`src/lib/trpc/trpc.ts`:

```typescript
import { initTRPC, TRPCError } from '@trpc/server';
import { type Session } from 'next-auth';
import superjson from 'superjson';
import { ZodError } from 'zod';
import { db } from '@/lib/db';

export const createTRPCContext = async (opts: {
  headers: Headers;
  session: Session | null;
}) => {
  return {
    db,
    session: opts.session,
    ...opts,
  };
};

// In-memory stores for real-time state (instead of Redis)
export const trialStateStore = new Map<string, TrialState>();
export const activeConnections = new Map<string, Set<WebSocket>>();

const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;

const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
```

### 2. Router Implementation

`src/server/api/routers/studies.ts`:

```typescript
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '@/lib/trpc/trpc';
import { studies, studyMembers } from '@/lib/db/schema';
import { TRPCError } from '@trpc/server';
import { eq, and } from 'drizzle-orm';

export const studiesRouter = createTRPCRouter({
  list: protectedProcedure
    .input(z.object({
      page: z.number().min(1).default(1),
      limit: z.number().min(1).max(100).default(20),
      status: z.enum(['draft', 'active', 'completed', 'archived']).optional(),
      myStudiesOnly: z.boolean().default(false),
    }))
    .query(async ({ ctx, input }) => {
      const offset = (input.page - 1) * input.limit;
      
      const whereConditions = [];
      if (input.status) {
        whereConditions.push(eq(studies.status, input.status));
      }
      if (input.myStudiesOnly) {
        const memberStudyIds = await ctx.db
          .select({ studyId: studyMembers.studyId })
          .from(studyMembers)
          .where(eq(studyMembers.userId, ctx.session.user.id));
        
        whereConditions.push(
          inArray(studies.id, memberStudyIds.map(m => m.studyId))
        );
      }

      const [items, totalCount] = await Promise.all([
        ctx.db.query.studies.findMany({
          where: and(...whereConditions),
          limit: input.limit,
          offset,
          with: {
            creator: true,
            members: {
              with: {
                user: true,
              },
            },
          },
          orderBy: (studies, { desc }) => [desc(studies.createdAt)],
        }),
        ctx.db.select({ count: count() }).from(studies).where(and(...whereConditions)),
      ]);

      return {
        items,
        totalCount: totalCount[0].count,
        totalPages: Math.ceil(totalCount[0].count / input.limit),
        currentPage: input.page,
      };
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(255),
      description: z.string().optional(),
      institution: z.string().optional(),
      irbProtocol: z.string().optional(),
      metadata: z.record(z.any()).default({}),
    }))
    .mutation(async ({ ctx, input }) => {
      return await ctx.db.transaction(async (tx) => {
        // Create study
        const [study] = await tx.insert(studies).values({
          ...input,
          createdBy: ctx.session.user.id,
        }).returning();

        // Add creator as owner
        await tx.insert(studyMembers).values({
          studyId: study.id,
          userId: ctx.session.user.id,
          role: 'owner',
        });

        // Log activity
        await tx.insert(activityLogs).values({
          studyId: study.id,
          userId: ctx.session.user.id,
          action: 'study.created',
          description: `Created study "${study.name}"`,
        });

        return study;
      });
    }),

  // ... more procedures
});
```

### 3. Root Router

`src/server/api/root.ts`:

```typescript
import { createTRPCRouter } from '@/lib/trpc/trpc';
import { authRouter } from './routers/auth';
import { studiesRouter } from './routers/studies';
import { experimentsRouter } from './routers/experiments';
import { trialsRouter } from './routers/trials';
import { participantsRouter } from './routers/participants';
import { robotsRouter } from './routers/robots';
import { mediaRouter } from './routers/media';
import { analysisRouter } from './routers/analysis';
import { adminRouter } from './routers/admin';

export const appRouter = createTRPCRouter({
  auth: authRouter,
  studies: studiesRouter,
  experiments: experimentsRouter,
  trials: trialsRouter,
  participants: participantsRouter,
  robots: robotsRouter,
  media: mediaRouter,
  analysis: analysisRouter,
  admin: adminRouter,
});

export type AppRouter = typeof appRouter;
```

## Frontend Architecture

### 1. Component Organization

`src/components/experiment/designer/ExperimentDesigner.tsx`:

```typescript
'use client';

import { useState, useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Canvas } from './Canvas';
import { Toolbar } from './Toolbar';
import { PropertiesPanel } from './PropertiesPanel';
import { useExperiment } from '@/hooks/useExperiment';

interface ExperimentDesignerProps {
  experimentId: string;
}

export function ExperimentDesigner({ experimentId }: ExperimentDesignerProps) {
  const { experiment, updateExperiment } = useExperiment(experimentId);
  const [selectedStepId, setSelectedStepId] = useState<string | null>(null);

  const handleStepSelect = useCallback((stepId: string) => {
    setSelectedStepId(stepId);
  }, []);

  const handleStepUpdate = useCallback((stepId: string, updates: Partial<Step>) => {
    updateExperiment({
      steps: experiment.steps.map(step =>
        step.id === stepId ? { ...step, ...updates } : step
      ),
    });
  }, [experiment, updateExperiment]);

  return (
    <DndProvider backend={HTML5Backend}>
      <div className="flex h-full">
        <Toolbar className="w-64 border-r" />
        <div className="flex-1 flex flex-col">
          <Canvas
            steps={experiment.steps}
            onStepSelect={handleStepSelect}
            onStepUpdate={handleStepUpdate}
            selectedStepId={selectedStepId}
          />
        </div>
        {selectedStepId && (
          <PropertiesPanel
            step={experiment.steps.find(s => s.id === selectedStepId)!}
            onUpdate={(updates) => handleStepUpdate(selectedStepId, updates)}
            className="w-80 border-l"
          />
        )}
      </div>
    </DndProvider>
  );
}
```

### 2. Custom Hooks

`src/hooks/useExperiment.ts`:

```typescript
import { api } from '@/lib/trpc/react';
import { useRouter } from 'next/navigation';
import { toast } from '@/components/ui/use-toast';

export function useExperiment(experimentId: string) {
  const router = useRouter();
  const utils = api.useUtils();

  const { data: experiment, isLoading } = api.experiments.get.useQuery({
    id: experimentId,
  });

  const updateMutation = api.experiments.update.useMutation({
    onSuccess: () => {
      utils.experiments.get.invalidate({ id: experimentId });
      toast({
        title: 'Experiment updated',
        description: 'Your changes have been saved.',
      });
    },
    onError: (error) => {
      toast({
        title: 'Update failed',
        description: error.message,
        variant: 'destructive',
      });
    },
  });

  const updateExperiment = (updates: Partial<Experiment>) => {
    updateMutation.mutate({
      id: experimentId,
      ...updates,
    });
  };

  return {
    experiment,
    isLoading,
    updateExperiment,
    isUpdating: updateMutation.isLoading,
  };
}
```

### 3. Server Components

`src/app/(dashboard)/studies/[studyId]/page.tsx`:

```typescript
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { requireAuth } from '@/lib/auth/utils';
import { api } from '@/lib/trpc/server';
import { StudyDashboard } from '@/components/study/StudyDashboard';
import { StudyDashboardSkeleton } from '@/components/study/StudyDashboardSkeleton';

interface StudyPageProps {
  params: {
    studyId: string;
  };
}

export default async function StudyPage({ params }: StudyPageProps) {
  await requireAuth();

  const study = await api.studies.get({
    id: params.studyId,
  });

  if (!study) {
    notFound();
  }

  return (
    <Suspense fallback={<StudyDashboardSkeleton />}>
      <StudyDashboard study={study} />
    </Suspense>
  );
}
```

## Real-time Features

### 1. WebSocket Server

`src/server/websocket/server.ts`:

```typescript
// For Vercel deployment, we use Edge Runtime WebSocket API
// src/app/api/ws/route.ts
import { NextRequest } from 'next/server';
import { verifySession } from '@/lib/auth/session';
import { TrialExecutionHandler } from '@/lib/websocket/handlers/trial-execution';
import { ObserverHandler } from '@/lib/websocket/handlers/observer';

export const runtime = 'edge';

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const token = searchParams.get('token');
  
  if (req.headers.get('upgrade') !== 'websocket') {
    return new Response('Expected websocket', { status: 400 });
  }

const handlers = {
  trial: new TrialExecutionHandler(),
  observer: new ObserverHandler(),
};

wss.on('connection', async (ws, req) => {
  const { query } = parse(req.url!, true);
  const token = query.token as string;
  
  try {
    const session = await verifySession(token);
    if (!session) {
      ws.close(1008, 'Invalid session');
      return;
    }

    const handler = handlers[query.type as keyof typeof handlers];
    if (!handler) {
      ws.close(1008, 'Invalid connection type');
      return;
    }

    await handler.handleConnection(ws, session, query);
  } catch (error) {
    console.error('WebSocket connection error:', error);
    ws.close(1011, 'Internal error');
  }
});

  const { socket, response } = Deno.upgradeWebSocket(req);
  
  socket.onopen = () => handleConnection(socket, session);
  socket.onmessage = (event) => handleMessage(socket, event.data);
  socket.onclose = () => handleDisconnect(socket);
  
  return response;
}
```

### 2. Client WebSocket Hook

`src/hooks/useWebSocket.ts`:

```typescript
import { useEffect, useRef, useState, useCallback } from 'react';
import { useSession } from 'next-auth/react';

interface UseWebSocketOptions {
  url: string;
  onMessage?: (data: any) => void;
  onConnect?: () => void;
  onDisconnect?: () => void;
  reconnectAttempts?: number;
}

export function useWebSocket({
  url,
  onMessage,
  onConnect,
  onDisconnect,
  reconnectAttempts = 5,
}: UseWebSocketOptions) {
  const { data: session } = useSession();
  const ws = useRef<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [lastMessage, setLastMessage] = useState<any>(null);
  const reconnectCount = useRef(0);

  const connect = useCallback(() => {
    if (!session?.user) return;

    const wsUrl = new URL(url);
    wsUrl.searchParams.set('token', session.user.sessionToken);

    ws.current = new WebSocket(wsUrl.toString());

    ws.current.onopen = () => {
      setIsConnected(true);
      reconnectCount.current = 0;
      onConnect?.();
    };

    ws.current.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setLastMessage(data);
      onMessage?.(data);
    };

    ws.current.onclose = () => {
      setIsConnected(false);
      onDisconnect?.();

      if (reconnectCount.current < reconnectAttempts) {
        reconnectCount.current++;
        setTimeout(connect, 1000 * Math.pow(2, reconnectCount.current));
      }
    };

    ws.current.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
  }, [session, url, onConnect, onDisconnect, onMessage, reconnectAttempts]);

  useEffect(() => {
    connect();

    return () => {
      ws.current?.close();
    };
  }, [connect]);

  const sendMessage = useCallback((data: any) => {
    if (ws.current?.readyState === WebSocket.OPEN) {
      ws.current.send(JSON.stringify(data));
    }
  }, []);

  return {
    isConnected,
    sendMessage,
    lastMessage,
  };
}
```

## File Storage System

### 1. S3/MinIO Client

`src/lib/storage/s3.ts`:

```typescript
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3Client = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: process.env.S3_REGION || 'us-east-1',
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID!,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
  },
  forcePathStyle: true, // Required for MinIO
});

export async function uploadFile(
  key: string,
  body: Buffer | Uint8Array | string,
  contentType?: string
) {
  const command = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET_NAME,
    Key: key,
    Body: body,
    ContentType: contentType,
  });

  return await s3Client.send(command);
}

export async function getPresignedUrl(
  key: string,
  expiresIn: number = 3600
): Promise<string> {
  const command = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET_NAME,
    Key: key,
  });

  return await getSignedUrl(s3Client, command, { expiresIn });
}

export async function deleteFile(key: string) {
  const command = new DeleteObjectCommand({
    Bucket: process.env.S3_BUCKET_NAME,
    Key: key,
  });

  return await s3Client.send(command);
}
```

### 2. File Upload Handler

`src/features/media/upload.ts`:

```typescript
import { z } from 'zod';
import { uploadFile } from '@/lib/storage/s3';
import { db } from '@/lib/db';
import { mediaCaptures } from '@/lib/db/schema';

const uploadSchema = z.object({
  trialId: z.string().uuid(),
  file: z.instanceof(File),
  mediaType: z.enum(['video', 'audio', 'image']),
  startTimestamp: z.date(),
  endTimestamp: z.date().optional(),
});

export async function handleMediaUpload(input: z.infer<typeof uploadSchema>) {
  const { trialId, file, mediaType, startTimestamp, endTimestamp } = input;

  // Generate unique key
  const key = `trials/${trialId}/${mediaType}/${Date.now()}-${file.name}`;

  // Upload to S3
  const buffer = await file.arrayBuffer();
  await uploadFile(key, Buffer.from(buffer), file.type);

  // Save metadata to database
  const [mediaRecord] = await db.insert(mediaCaptures).values({
    trialId,
    mediaType,
    storagePath: key,
    fileSize: file.size,
    format: file.type,
    startTimestamp,
    endTimestamp,
    metadata: {
      originalName: file.name,
      uploadedAt: new Date(),
    },
  }).returning();

  return mediaRecord;
}
```

## Plugin System

### 1. Plugin Interface

`src/lib/plugins/types.ts`:

```typescript
import { z } from 'zod';

export interface RobotPlugin {
  id: string;
  name: string;
  version: string;
  robotId: string;
  
  // Configuration
  configSchema: z.ZodSchema;
  defaultConfig: Record<string, any>;
  
  // Capabilities
  actions: ActionDefinition[];
  
  // Lifecycle
  initialize(config: any): Promise<void>;
  connect(): Promise<boolean>;
  disconnect(): Promise<void>;
  
  // Execution
  executeAction(action: Action, params: any): Promise<ActionResult>;
  
  // State
  getState(): Promise<RobotState>;
}

export interface ActionDefinition {
  id: string;
  name: string;
  description: string;
  category: string;
  icon?: string;
  parameterSchema: z.ZodSchema;
  timeout?: number;
  retryable?: boolean;
}

export interface ActionResult {
  success: boolean;
  data?: any;
  error?: string;
  duration: number;
}

export interface RobotState {
  connected: boolean;
  battery?: number;
  position?: { x: number; y: number; z: number };
  sensors?: Record<string, any>;
}
```

### 2. Plugin Manager

`src/lib/plugins/manager.ts`:

```typescript
import { RobotPlugin } from './types';
import { db } from '@/lib/db';
import { plugins, studyPlugins } from '@/lib/db/schema';

export class PluginManager {
  private plugins = new Map<string, RobotPlugin>();
  private instances = new Map<string, RobotPlugin>();

  async loadPlugin(pluginId: string): Promise<RobotPlugin> {
    if (this.plugins.has(pluginId)) {
      return this.plugins.get(plug