Disciplines · Migration

Yemaya Migration Guide

1.

15sections3 minread

On this page

This guide helps developers migrate existing Yemaya (creative production studio) codebases to the unified Oshun monorepo.

Table of Contents#

  1. Overview
  2. Breaking Changes
  3. Pre-Migration Checklist
  4. Architecture Changes
  5. Step-by-Step Migration
  6. Asset Pipeline Changes
  7. Collaboration Features
  8. AI Integration
  9. Plugin System
  10. SDK Migration
  11. Testing Updates
  12. Common Issues

Overview#

The Yemaya platform is the creative production studio for AAA game and movie production. Migrating to Oshun consolidates AI generation (Isis), research capabilities (Sophia), worldbuilding (Hathor), and engine integration (Bellona).

Benefits of Migration#

Capability Before After
AI Generation Direct API calls Unified Isis with tracking, cost management
Research Manual document search Sophia RAG with citations
Worldbuilding Separate tools Integrated Hathor
Engine Export Custom pipelines Bellona adapters for all engines
Real-time Collab Custom implementation Shared @oshun/websocket
Authentication Yemaya-specific Unified team/organization auth

What Changes#

Aspect Before After
Repository Standalone yemaya-studio repo oshun/apps/yemaya/, oshun/libs/yemaya/
Asset generation Direct Replicate/RunPod @isis/client with provenance
Real-time sync Custom Yjs server @oshun/websocket + Yjs
File storage Direct S3 @oshun/storage
Background jobs BullMQ direct @oshun/queue wrapper

Breaking Changes#

Yemaya's migration changes several integration contracts. Treat these as required migrations, not optional cleanup:

Area Required Change Compatibility Strategy
Asset generation Submit generation through @isis/client rather than provider SDK calls Preserve provider-specific options only through typed Isis parameters
Engine export Use Bellona export jobs for Godot, Unreal, Unity, and Blender packages Validate exported package manifests before disabling legacy exporters
Collaboration Move realtime sync onto the shared websocket/Yjs service Run dual-write awareness events only during a controlled migration window
Storage Store files through @oshun/storage with provenance metadata Backfill asset metadata and reject uploads missing project ownership
Authentication Replace Yemaya-local team auth with unified organization/session auth Migrate permissions before enabling shared gateway enforcement

These changes centralize provenance, cost tracking, permissions, and release gates, which means old direct-provider or direct-storage paths must be removed before production traffic is switched.


Pre-Migration Checklist#

Before starting:

  • Export all project data and assets
  • Document all custom workflows
  • List all third-party integrations
  • Backup user permissions and team configurations
  • Document asset pipeline configurations
  • Review custom plugin code

Local Development Environment Setup#

Before migrating, set up the local development environment:

bash
# Clone the Oshun monorepo
git clone git@github.com:GreyChimp/oshun.git
cd oshun

# Start core infrastructure (PostgreSQL, Redis, MinIO, Mailpit)
docker compose -f docker/docker-compose.dev.yml up -d

# Verify all services are running
docker compose -f docker/docker-compose.dev.yml ps

# Install dependencies
pnpm install

# Create your local .env from template (already done if using docker setup)
cp docker/.env.example docker/.env

The development databases are automatically created:

  • yemaya - Yemaya domain database (PostgreSQL with pgvector)
  • oshun_dev - Shared development database

Connection URL: postgresql://oshun:oshun_dev@localhost:5432/yemaya

For detailed setup instructions, see CLAUDE.md.


Architecture Changes#

Old Architecture#

text
yemaya-studio/
├── apps/
│   ├── api/                    # Main API (Express)
│   ├── web/                    # React web app
│   ├── desktop/                # Electron app
│   ├── cli/                    # CLI tool
│   └── workers/
│       ├── generation/         # AI generation worker
│       ├── export/             # Asset export worker
│       └── collaboration/      # Real-time sync worker
├── packages/
│   ├── core/                   # Core business logic
│   ├── database/               # Prisma client
│   ├── auth/                   # Authentication
│   ├── storage/                # S3 integration
│   ├── ai-providers/           # AI service integrations
│   ├── collaboration/          # Yjs/CRDT
│   ├── export/                 # Engine exporters
│   └── sdk/                    # Public SDK
└── plugins/
    ├── blender/                # Blender plugin
    ├── unreal/                 # Unreal plugin
    └── godot/                  # Godot plugin

New Architecture#

text
oshun/
├── apps/yemaya/
│   ├── api/                    # API Gateway (Hono)
│   ├── studio-web/             # React web app
│   ├── studio-desktop/         # Electron desktop app
│   ├── workers/                # Background processors
│   └── cli/                    # CLI interface
│
├── libs/yemaya/
│   ├── sdk/                    # TypeScript SDK
│   ├── sdk-cpp/                # C++ SDK
│   ├── sdk-python/             # Python SDK
│   ├── database/               # Prisma schema
│   ├── projects/               # Project management
│   ├── orchestration/          # Workflow orchestration
│   ├── collaboration/          # Real-time (Yjs)
│   ├── autonomous-pipelines/   # AI workflows
│   ├── agents/                 # AI agent framework
│   ├── asset-library/          # Asset management
│   ├── ui/                     # React components
│   └── event-handlers/         # Event subscriptions
│
├── libs/isis/                  # ← Replaces packages/ai-providers
│   ├── client/
│   └── workflows/
│
├── libs/bellona/               # ← Replaces packages/export
│   ├── godot/
│   ├── unreal/
│   ├── unity/
│   └── blender/
│
└── libs/shared/                # ← Replaces duplicated utilities
    ├── storage/
    ├── queue/
    ├── websocket/
    └── ...

Step-by-Step Migration#

Step 1: Project Structure Migration#

bash
# Create Yemaya directory structure in Oshun
mkdir -p apps/yemaya/{api,studio-web,studio-desktop,workers,cli}
mkdir -p libs/yemaya/{sdk,database,projects,collaboration,ui}

# Copy main app code
cp -r yemaya-studio/apps/api/* oshun/apps/yemaya/api/
cp -r yemaya-studio/apps/web/* oshun/apps/yemaya/studio-web/
cp -r yemaya-studio/apps/desktop/* oshun/apps/yemaya/studio-desktop/

# Copy library code
cp -r yemaya-studio/packages/core/* oshun/libs/yemaya/projects/
cp -r yemaya-studio/packages/collaboration/* oshun/libs/yemaya/collaboration/
cp -r yemaya-studio/packages/sdk/* oshun/libs/yemaya/sdk/

Step 2: Update project.json#

Create Nx project configuration:

json
// apps/yemaya/api/project.json
{
  "name": "yemaya-api",
  "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "apps/yemaya/api/src",
  "projectType": "application",
  "tags": ["scope:yemaya", "type:app", "platform:node"],
  "targets": {
    "build": {
      "executor": "@nx/js:tsc",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/yemaya/api",
        "main": "apps/yemaya/api/src/main.ts",
        "tsConfig": "apps/yemaya/api/tsconfig.app.json"
      }
    },
    "dev": {
      "executor": "nx:run-commands",
      "options": {
        "command": "tsx watch apps/yemaya/api/src/main.ts"
      }
    },
    "test": {
      "executor": "@nx/vite:test",
      "options": {
        "config": "apps/yemaya/api/vitest.config.ts"
      }
    }
  }
}

Step 3: Migrate API Framework#

typescript
// BEFORE: Express
import express from 'express';
import cors from 'cors';

const app = express();
app.use(cors());
app.use(express.json());
app.use(authMiddleware);

app.get('/api/projects', async (req, res) => {
  const projects = await getProjects(req.user.id);
  res.json(projects);
});

app.listen(4000);

// AFTER: Hono with OpenAPI
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { OpenAPIHono } from '@hono/zod-openapi';
import { authMiddleware, rateLimitMiddleware } from '@oshun/traefik-config';
import { tracingMiddleware } from '@oshun/tracing';
import { metricsMiddleware } from '@oshun/metrics';

const app = new OpenAPIHono();

// Middleware stack
app.use('*', tracingMiddleware());
app.use('*', metricsMiddleware());
app.use('*', cors({ origin: ['https://studio.oshun.io'] }));
app.use('/api/*', rateLimitMiddleware({ windowMs: 60000, max: 100 }));
app.use('/api/*', authMiddleware({ audience: 'yemaya' }));

// Routes
app.route('/api/projects', projectsRouter);
app.route('/api/assets', assetsRouter);
app.route('/api/generation', generationRouter);

// OpenAPI documentation
app.doc('/openapi.json', {
  openapi: '3.1.0',
  info: { title: 'Yemaya API', version: '1.0.0' },
});

export default app;

Step 4: Migrate Storage Layer#

typescript
// BEFORE: Direct S3
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
} from '@aws-sdk/client-s3';

const s3 = new S3Client({ region: 'us-east-1' });

async function uploadAsset(file: Buffer, key: string) {
  await s3.send(
    new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
      Body: file,
    })
  );
  return `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`;
}

// AFTER: Oshun storage abstraction
import { createStorageClient, StorageProvider } from '@oshun/storage';

const storage = createStorageClient({
  provider: StorageProvider.S3,
  bucket: process.env.ASSET_BUCKET,
  region: process.env.AWS_REGION,
  // Automatic CDN URL generation
  cdnBaseUrl: process.env.ASSET_CDN_URL,
});

async function uploadAsset(
  file: Buffer,
  key: string,
  metadata?: AssetMetadata
) {
  const result = await storage.upload({
    key: `projects/${projectId}/assets/${key}`,
    body: file,
    contentType: metadata?.mimeType,
    metadata: {
      projectId,
      uploadedBy: userId,
      ...metadata,
    },
  });

  return {
    url: result.cdnUrl, // CDN URL
    storageUrl: result.url, // Direct S3 URL
    key: result.key,
    size: result.size,
  };
}

// Presigned URLs for large uploads
const uploadUrl = await storage.createPresignedUpload({
  key: `projects/${projectId}/assets/${filename}`,
  expiresIn: 3600,
  maxSize: 500 * 1024 * 1024, // 500MB
});

Step 5: Migrate to Isis for AI Generation#

typescript
// BEFORE: Direct Replicate/RunPod
import Replicate from 'replicate';

const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });

async function generateImage(prompt: string) {
  const output = await replicate.run(
    'stability-ai/stable-diffusion:27b93a2413e...',
    { input: { prompt, width: 1024, height: 1024 } }
  );
  return output[0];
}

async function generateVideo(prompt: string) {
  const output = await replicate.run(
    'anotherjesse/zeroscope-v2-xl:9f747673...',
    { input: { prompt, num_frames: 24 } }
  );
  return output;
}

// AFTER: Isis client with unified interface
import { createIsisClient } from '@isis/client';

const isis = createIsisClient({ baseUrl: process.env.ISIS_API_URL });

async function generateImage(prompt: string, options: ImageOptions = {}) {
  const job = await isis.generation.create({
    type: 'image',
    prompt,
    model: options.model || 'stable-diffusion-xl',
    parameters: {
      width: options.width || 1024,
      height: options.height || 1024,
      steps: options.steps || 50,
      guidance: options.guidance || 7.5,
      negativePrompt: options.negativePrompt,
    },
    // Project context for tracking
    metadata: {
      projectId: options.projectId,
      userId: options.userId,
      source: 'yemaya',
    },
  });

  // Wait for completion with progress
  return await isis.generation.waitForCompletion(job.id, {
    onProgress: options.onProgress,
  });
}

async function generateVideo(prompt: string, options: VideoOptions = {}) {
  const job = await isis.generation.create({
    type: 'video',
    prompt,
    model: options.model || 'zeroscope-v2',
    parameters: {
      frames: options.frames || 24,
      fps: options.fps || 8,
      duration: options.duration,
    },
    metadata: {
      projectId: options.projectId,
      userId: options.userId,
      source: 'yemaya',
    },
  });

  return await isis.generation.waitForCompletion(job.id);
}

// 3D generation through Isis
async function generate3DModel(prompt: string) {
  const job = await isis.generation.create({
    type: '3d',
    prompt,
    model: 'point-e',
    parameters: {
      outputFormat: 'glb',
      quality: 'high',
    },
  });

  return await isis.generation.waitForCompletion(job.id);
}

Step 6: Migrate to Bellona for Exports#

typescript
// BEFORE: Custom export pipelines
async function exportToUnreal(projectId: string) {
  const project = await getProject(projectId);
  const assets = await getProjectAssets(projectId);

  // Custom FBX conversion
  const fbxAssets = await Promise.all(assets.map((a) => convertToFbx(a)));

  // Package for Unreal
  const package = await createUnrealPackage(project, fbxAssets);
  return uploadPackage(package);
}

// AFTER: Bellona handles all exports
import { createBellonaClient } from '@bellona/client';

const bellona = createBellonaClient({ baseUrl: process.env.BELLONA_API_URL });

async function exportToEngine(
  projectId: string,
  engine: 'godot' | 'unreal' | 'unity' | 'blender'
) {
  // Create export job
  const job = await bellona.exports.create({
    source: 'yemaya',
    sourceId: projectId,
    engine,
    version: getEngineVersion(engine),
    options: {
      // Asset settings
      includeTextures: true,
      textureFormat: 'png',
      textureSize: 2048,

      // Mesh settings
      meshFormat: engine === 'godot' ? 'gltf' : 'fbx',
      lodLevels: [1.0, 0.5, 0.25],

      // Animation settings
      includeAnimations: true,
      animationFormat: 'native',

      // Project settings
      generateProjectFile: true,
      includeScripts: true,
    },
  });

  // Stream progress to client
  const progressStream = bellona.exports.streamProgress(job.id);
  for await (const progress of progressStream) {
    await notifyClient(projectId, {
      type: 'export-progress',
      stage: progress.stage,
      percent: progress.percent,
    });
  }

  // Get artifacts
  const result = await bellona.exports.waitForCompletion(job.id);
  return {
    artifacts: result.artifacts,
    downloadUrl: result.downloadUrl,
    expiresAt: result.expiresAt,
  };
}

Asset Pipeline Changes#

Asset References#

typescript
// BEFORE: Local asset references
interface Asset {
  id: string;
  url: string; // Direct S3 URL
  type: string;
  projectId: string;
}

// AFTER: Cross-domain asset references
interface Asset {
  id: string;
  yemayaId: string; // Yemaya's internal ID
  isisAssetId?: string; // Reference to generated asset in Isis
  url: string;
  cdnUrl: string;
  type: AssetType;
  projectId: string;
  metadata: {
    width?: number;
    height?: number;
    duration?: number;
    fileSize: number;
    mimeType: string;
    generationJob?: {
      jobId: string;
      prompt: string;
      model: string;
    };
  };
  provenance: {
    source: 'upload' | 'generated' | 'import';
    createdAt: string;
    createdBy: string;
  };
}

Asset Event Handlers#

typescript
// libs/yemaya/event-handlers/src/isis-handlers.ts
import { EventBus } from '@oshun/event-bus';
import { createYemayaService } from '@yemaya/service';

export function registerIsisHandlers(eventBus: EventBus) {
  // Handle asset generation completion
  eventBus.subscribe('isis.asset.generated', async (event) => {
    const { assetId, metadata } = event.payload;
    const projectId = metadata.projectId as string;

    // Only process Yemaya-originated generations
    if (metadata.source !== 'yemaya' || !projectId) {
      return;
    }

    const yemaya = createYemayaService();

    // Create asset reference in Yemaya
    await yemaya.assets.createFromGeneration({
      projectId,
      isisAssetId: assetId,
      url: event.payload.url,
      type: event.payload.type,
      metadata: event.payload.metadata,
      generationJob: {
        jobId: event.payload.jobId,
        prompt: metadata.prompt as string,
        model: metadata.model as string,
      },
    });

    // Notify project collaborators
    await eventBus.publish('yemaya.asset.generated', {
      projectId,
      assetId,
      type: event.payload.type,
    });
  });

  // Handle generation failures
  eventBus.subscribe('isis.job.failed', async (event) => {
    const { jobId, error, metadata } = event.payload;
    const projectId = metadata?.projectId as string;

    if (metadata?.source !== 'yemaya' || !projectId) {
      return;
    }

    await eventBus.publish('yemaya.generation.failed', {
      projectId,
      jobId,
      error,
    });
  });
}

Collaboration Features#

Real-time Sync Migration#

typescript
// BEFORE: Custom Yjs server
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const doc = new Y.Doc();
const provider = new WebsocketProvider(
  'wss://collab.yemaya.io',
  `project-${projectId}`,
  doc
);

// AFTER: Oshun WebSocket with Yjs
import * as Y from 'yjs';
import { createCollaborationClient } from '@yemaya/collaboration';

const collaboration = createCollaborationClient({
  wsUrl: process.env.WEBSOCKET_URL,
  projectId,
  userId,
});

// Project document sync
const projectDoc = await collaboration.getDocument('project');
const scenes = projectDoc.getArray('scenes');
const assets = projectDoc.getMap('assets');

// Awareness (cursors, selections)
collaboration.awareness.setLocalState({
  user: { id: userId, name: userName, color: userColor },
  cursor: null,
  selection: null,
});

// Listen for updates
collaboration.on('update', (update) => {
  console.log('Document updated', update);
});

collaboration.on('awareness', (changes) => {
  console.log('Collaborators changed', changes);
});

Server-Side Collaboration Handler#

typescript
// apps/yemaya/api/src/collaboration/handler.ts
import { WebSocketServer } from '@oshun/websocket';
import { createCollaborationHandler } from '@yemaya/collaboration';

export function setupCollaboration(wss: WebSocketServer) {
  const handler = createCollaborationHandler({
    storage: {
      // Persist Yjs documents
      load: async (docId) => {
        const doc = await redis.get(`yjs:${docId}`);
        return doc ? Buffer.from(doc, 'base64') : null;
      },
      save: async (docId, update) => {
        await redis.set(`yjs:${docId}`, update.toString('base64'));
      },
    },
    auth: {
      // Verify access permissions
      canAccess: async (userId, projectId) => {
        return await hasProjectAccess(userId, projectId);
      },
      canEdit: async (userId, projectId) => {
        return await hasEditAccess(userId, projectId);
      },
    },
  });

  wss.on('connection', async (ws, req) => {
    const projectId = req.query.projectId;
    const userId = req.user.id;

    if (!(await handler.auth.canAccess(userId, projectId))) {
      ws.close(4003, 'Access denied');
      return;
    }

    handler.handleConnection(ws, projectId, userId);
  });
}

AI Integration#

Autonomous Pipeline Migration#

typescript
// BEFORE: Custom AI workflow
async function runCreativeWorkflow(prompt: string) {
  // Manual orchestration of AI calls
  const conceptArt = await replicate.run(/* ... */);
  const variations = await Promise.all([
    replicate.run(/* variation 1 */),
    replicate.run(/* variation 2 */),
  ]);
  const upscaled = await replicate.run(/* upscale */);
  return { conceptArt, variations, upscaled };
}

// AFTER: Isis workflows
import { createIsisClient } from '@isis/client';

const isis = createIsisClient({ baseUrl: process.env.ISIS_API_URL });

async function runCreativeWorkflow(request: CreativeWorkflowRequest) {
  // Use pre-built workflow in Isis
  const workflow = await isis.workflows.run('creative-concept', {
    prompt: request.prompt,
    style: request.style,
    variations: 4,
    upscale: true,
    metadata: {
      projectId: request.projectId,
      source: 'yemaya',
    },
  });

  // Workflow handles all steps, we just wait for results
  return await isis.workflows.waitForCompletion(workflow.id, {
    onStepComplete: (step) => {
      console.log(`Step ${step.name}: ${step.status}`);
    },
  });
}

// Create custom workflows
async function createCharacterWorkflow() {
  await isis.workflows.create({
    name: 'character-generation',
    steps: [
      {
        name: 'concept',
        type: 'image',
        model: 'stable-diffusion-xl',
        inputs: { prompt: '{{input.prompt}} character concept art' },
      },
      {
        name: 'turnaround',
        type: 'image',
        model: 'stable-diffusion-xl',
        inputs: { prompt: '{{input.prompt}} character turnaround sheet' },
        dependsOn: ['concept'],
      },
      {
        name: '3d-model',
        type: '3d',
        model: 'point-e',
        inputs: { image: '{{steps.concept.output}}' },
        dependsOn: ['concept'],
      },
    ],
  });
}

Research Integration with Sophia#

typescript
// NEW: Sophia integration for creative research
import { createSophiaClient } from '@sophia/client';

const sophia = createSophiaClient({ baseUrl: process.env.SOPHIA_API_URL });

// Research for creative projects
async function researchForProject(projectId: string, topic: string) {
  // Search project-specific documents
  const projectDocs = await sophia.search({
    query: topic,
    collections: [`project-${projectId}`],
    limit: 10,
  });

  // Search global creative resources
  const globalDocs = await sophia.search({
    query: topic,
    collections: ['creative-reference', 'art-history', 'film-techniques'],
    limit: 20,
  });

  return {
    projectReferences: projectDocs.hits,
    globalReferences: globalDocs.hits,
  };
}

// Ingest project documents for RAG
async function ingestProjectDocument(projectId: string, document: Document) {
  await sophia.documents.ingest({
    source: document.url,
    collection: `project-${projectId}`,
    metadata: {
      projectId,
      documentType: document.type,
      uploadedBy: document.uploadedBy,
    },
  });
}

Plugin System#

Engine Plugin Updates#

typescript
// BEFORE: Plugin makes direct API calls
// blender-plugin/yemaya_addon.py
import requests

class YemayaOperator:
    def execute(self, context):
        response = requests.post(
            f'{YEMAYA_API}/assets/upload',
            files={'file': open(filepath, 'rb')},
            headers={'Authorization': f'Bearer {token}'}
        )
        return response.json()

// AFTER: Use Yemaya SDK
// libs/yemaya/sdk-python/src/yemaya/client.py
from yemaya import YemayaClient

class YemayaClient:
    def __init__(self, api_url: str, api_key: str):
        self.api_url = api_url
        self.api_key = api_key

    def upload_asset(self, filepath: str, project_id: str) -> Asset:
        """Upload asset to Yemaya project."""
        with open(filepath, 'rb') as f:
            response = self._request(
                'POST',
                f'/projects/{project_id}/assets',
                files={'file': f}
            )
        return Asset(**response)

    def export_to_blender(self, project_id: str, options: ExportOptions) -> ExportJob:
        """Export project for Blender."""
        response = self._request(
            'POST',
            f'/projects/{project_id}/export',
            json={
                'engine': 'blender',
                'options': options.dict()
            }
        )
        return ExportJob(**response)

# blender-plugin/yemaya_addon.py (updated)
from yemaya import YemayaClient

client = YemayaClient(
    api_url=get_preference('yemaya_api_url'),
    api_key=get_preference('yemaya_api_key')
)

class YEMAYA_OT_upload(bpy.types.Operator):
    def execute(self, context):
        asset = client.upload_asset(
            filepath=bpy.data.filepath,
            project_id=context.scene.yemaya_project_id
        )
        self.report({'INFO'}, f'Uploaded: {asset.id}')
        return {'FINISHED'}

SDK Migration#

TypeScript SDK#

typescript
// BEFORE: packages/sdk/src/index.ts
export class YemayaClient {
  constructor(options: { apiKey: string; baseUrl: string }) {
    this.apiKey = options.apiKey;
    this.baseUrl = options.baseUrl;
  }

  async getProject(id: string): Promise<Project> {
    const response = await fetch(`${this.baseUrl}/projects/${id}`, {
      headers: { 'X-API-Key': this.apiKey },
    });
    return response.json();
  }
}

// AFTER: libs/yemaya/sdk/src/client.ts
import { createHttpClient, HttpClient } from '@oshun/http-client';

export interface YemayaClientOptions {
  baseUrl?: string;
  auth?: {
    type: 'api-key' | 'jwt' | 'service-account';
    credentials: string | ServiceAccountCredentials;
  };
  timeout?: number;
  retries?: number;
}

export function createYemayaClient(options: YemayaClientOptions = {}) {
  const http = createHttpClient({
    baseUrl: options.baseUrl || 'https://api.oshun.io/yemaya',
    timeout: options.timeout || 30000,
    retries: options.retries || 3,
    auth: options.auth,
  });

  return {
    projects: createProjectsAPI(http),
    assets: createAssetsAPI(http),
    teams: createTeamsAPI(http),
    generation: createGenerationAPI(http),
    collaboration: createCollaborationAPI(http),
    exports: createExportsAPI(http),
  };
}

// Resource APIs
function createProjectsAPI(http: HttpClient) {
  return {
    list: (params?: ListParams) =>
      http.get<PaginatedResponse<Project>>('/v1/projects', { params }),

    get: (id: string) => http.get<Project>(`/v1/projects/${id}`),

    create: (data: CreateProjectRequest) =>
      http.post<Project>('/v1/projects', data),

    update: (id: string, data: UpdateProjectRequest) =>
      http.patch<Project>(`/v1/projects/${id}`, data),

    delete: (id: string) => http.delete(`/v1/projects/${id}`),

    // Generation through Isis
    generateAsset: async (id: string, request: GenerateAssetRequest) => {
      return http.post<GenerationJob>(`/v1/projects/${id}/generate`, request);
    },

    // Export through Bellona
    export: async (id: string, request: ExportRequest) => {
      return http.post<ExportJob>(`/v1/projects/${id}/export`, request);
    },
  };
}

// Usage
const yemaya = createYemayaClient({
  auth: { type: 'api-key', credentials: process.env.YEMAYA_API_KEY },
});

const project = await yemaya.projects.create({
  name: 'My Film Project',
  type: 'film',
});

const generationJob = await yemaya.projects.generateAsset(project.id, {
  type: 'image',
  prompt: 'Concept art for sci-fi cityscape',
});

Testing Updates#

Test Configuration#

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.{test,spec}.ts'],
    setupFiles: ['./test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['**/*.d.ts', '**/test/**'],
    },
  },
});

Mock External Domains#

typescript
// test/mocks/isis.ts
import { vi } from 'vitest';

export const mockIsisClient = {
  generation: {
    create: vi.fn().mockResolvedValue({ id: 'job-123', status: 'pending' }),
    get: vi.fn().mockResolvedValue({ id: 'job-123', status: 'completed' }),
    waitForCompletion: vi.fn().mockResolvedValue({
      id: 'job-123',
      status: 'completed',
      assetId: 'asset-456',
      url: 'https://cdn.oshun.io/assets/456.png',
    }),
  },
  assets: {
    get: vi.fn().mockResolvedValue({
      id: 'asset-456',
      url: 'https://cdn.oshun.io/assets/456.png',
    }),
  },
};

vi.mock('@isis/client', () => ({
  createIsisClient: () => mockIsisClient,
}));

// test/mocks/bellona.ts
export const mockBellonaClient = {
  exports: {
    create: vi.fn().mockResolvedValue({ id: 'export-789', status: 'pending' }),
    waitForCompletion: vi.fn().mockResolvedValue({
      id: 'export-789',
      status: 'completed',
      artifacts: [{ url: 'https://...', filename: 'project.zip' }],
    }),
  },
};

vi.mock('@bellona/client', () => ({
  createBellonaClient: () => mockBellonaClient,
}));

Integration Tests#

typescript
// test/integration/asset-generation.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { TestHarness } from '@oshun/testing';

describe('Asset Generation Integration', () => {
  const harness = new TestHarness();

  beforeAll(async () => {
    await harness.setup();
  });

  afterAll(async () => {
    await harness.teardown();
  });

  it('should generate asset and attach to project', async () => {
    // Create project
    const project = await harness.yemaya.createProject({ name: 'Test' });

    // Generate asset
    const job = await harness.yemaya.generateAsset(project.id, {
      type: 'image',
      prompt: 'A sunset',
    });

    // Wait for events
    await harness.waitForEvent('isis.asset.generated', {
      filter: (e) => e.metadata.projectId === project.id,
      timeout: 60000,
    });

    // Verify asset attached
    const assets = await harness.yemaya.getProjectAssets(project.id);
    expect(assets).toHaveLength(1);
  });
});

Common Issues#

Issue 1: Missing Cross-Domain Permissions#

text
Error: Forbidden - insufficient scope for isis.generation.create

Solution: Ensure service account has cross-domain permissions:

typescript
const yemaya = createYemayaClient({
  auth: {
    type: 'service-account',
    credentials: {
      clientId: process.env.SERVICE_CLIENT_ID,
      clientSecret: process.env.SERVICE_CLIENT_SECRET,
      scopes: ['yemaya:all', 'isis:generation:write', 'bellona:export:write'],
    },
  },
});

Issue 2: Event Handler Not Receiving Events#

text
No events received for 'isis.asset.generated'

Solution: Ensure consumer group is unique and handler is registered:

typescript
const eventBus = new EventBus({
  redis: { url: process.env.REDIS_URL },
  consumerGroup: 'yemaya-api-' + process.env.POD_NAME, // Unique per instance
});

// Register handlers BEFORE start()
registerIsisHandlers(eventBus);
await eventBus.start();

Issue 3: Asset URL Not Accessible#

text
Error: 403 Forbidden when accessing asset URL

Solution: Use CDN URLs and ensure proper CORS:

typescript
// Use CDN URL, not direct S3 URL
const asset = await isis.assets.get(assetId);
const url = asset.cdnUrl; // Not asset.storageUrl

Issue 4: Collaboration Sync Issues#

text
Yjs document diverged between clients

Solution: Ensure persistence and proper cleanup:

typescript
// Always persist updates
collaboration.on('update', async (update) => {
  await redis.append(`yjs:${docId}:updates`, update);
});

// Compact periodically
await collaboration.compact(docId);

Migration Checklist#

Phase 1: Setup#

  • Clone Oshun monorepo
  • Set up development environment
  • Configure environment variables
  • Set up database schemas

Phase 2: Code Migration#

  • Migrate apps to apps/yemaya/
  • Migrate libs to libs/yemaya/
  • Update import paths
  • Replace utilities with @oshun/*

Phase 3: AI Migration#

  • Replace direct AI calls with Isis
  • Migrate workflows to Isis workflows
  • Set up event handlers for generation
  • Add Sophia for research

Phase 4: Export Migration#

  • Replace export pipelines with Bellona
  • Update engine plugins
  • Test export for all target engines

Phase 5: Collaboration#

  • Migrate Yjs to shared infrastructure
  • Update real-time sync handlers
  • Test multi-user collaboration

Phase 6: Testing#

  • Update test configuration
  • Add cross-domain mocks
  • Run integration tests
  • Performance testing

Phase 7: SDK Updates#

  • Update TypeScript SDK
  • Update Python SDK
  • Update C++ SDK
  • Update plugin integrations

Phase 8: Deployment#

  • Update CI/CD
  • Configure Kubernetes
  • Staging deployment
  • Production rollout

Support#

For migration assistance:

  • Slack: #oshun-migration
  • Documentation: /docs/migration/
  • Issues: GitHub Issues with migration label