diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..6f4030b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "packages/workflow-ui"]
+ path = packages/workflow-ui
+ url = https://github.com/Anil-matcha/workflow-ui.git
diff --git a/app/api/app/[[...path]]/route.js b/app/api/app/[[...path]]/route.js
new file mode 100644
index 0000000..5b8eb8d
--- /dev/null
+++ b/app/api/app/[[...path]]/route.js
@@ -0,0 +1,145 @@
+import { NextResponse } from 'next/server';
+
+const MUAPI_BASE = 'https://api.muapi.ai';
+
+function getApiKey(request) {
+ // Priority 1: Direct x-api-key header
+ const headerKey = request.headers.get('x-api-key');
+ if (headerKey) return headerKey;
+
+ // Priority 2: muapi_key cookie (used by the fixed builder library)
+ const cookieKey = request.cookies.get('muapi_key')?.value;
+ return cookieKey;
+}
+
+function cleanHeaders(request) {
+ const headers = new Headers(request.headers);
+ headers.delete('host');
+ headers.delete('connection');
+ headers.delete('cookie'); // CRITICAL: Stop forwarding browser cookies to MuAPI to avoid auth conflicts
+ return headers;
+}
+
+export async function GET(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ // Handle alias: get_upload_file -> get_file_upload_url
+ const effectivePath = path === 'get_upload_file' ? 'get_file_upload_url' : path;
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/app/${effectivePath}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, {
+ headers,
+ method: 'GET',
+ });
+
+ const data = await response.json();
+
+ // SPECIAL CASE: Intercept upload URL and redirect to local binary proxy
+ if (effectivePath === 'get_file_upload_url' && data.url) {
+ const originalS3Url = data.url;
+ // We pass the real S3 URL as a header to our proxy
+ data.url = `/api/upload-binary`;
+
+ // Store target in a temporary way?
+ // Better: Return the target URL as an extra field that our proxy will look for
+ data.fields = {
+ ...data.fields,
+ 'x-proxy-target-url': originalS3Url
+ };
+ }
+
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function POST(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/app/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ const response = await fetch(targetUrl, {
+ method: 'POST',
+ headers,
+ body
+ });
+
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function DELETE(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/app/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, {
+ method: 'DELETE',
+ headers
+ });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function PUT(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/app/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ const response = await fetch(targetUrl, {
+ method: 'PUT',
+ headers,
+ body
+ });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/app/api/upload-binary/route.js b/app/api/upload-binary/route.js
new file mode 100644
index 0000000..038e5cf
--- /dev/null
+++ b/app/api/upload-binary/route.js
@@ -0,0 +1,44 @@
+import { NextResponse } from 'next/server';
+
+export async function POST(request) {
+ try {
+ const formData = await request.formData();
+
+ // Extract the original S3 target URL we injected earlier
+ const targetUrl = formData.get('x-proxy-target-url');
+
+ if (!targetUrl) {
+ return NextResponse.json({ error: 'Missing proxy target URL' }, { status: 400 });
+ }
+
+ // Reconstruct the FormData for S3 (excluding our internal proxy marker)
+ const s3FormData = new FormData();
+
+ // S3 is very sensitive to field ordering. We must ensure 'file' is likely last
+ // or at least that all signature fields come before what S3 expects.
+ // The original library code appends 'file' last, so iterating should preserve that.
+ for (const [key, value] of formData.entries()) {
+ if (key !== 'x-proxy-target-url') {
+ s3FormData.append(key, value);
+ }
+ }
+
+ // Perform the server-to-server POST to S3
+ // This bypasses browser CORS/Preflight security entirely
+ const s3Response = await fetch(targetUrl, {
+ method: 'POST',
+ body: s3FormData,
+ });
+
+ if (s3Response.ok || s3Response.status === 204) {
+ return new Response(null, { status: 204 });
+ } else {
+ const errorText = await s3Response.text();
+ console.error('S3 Proxy Error:', errorText);
+ return new Response(errorText, { status: s3Response.status });
+ }
+ } catch (error) {
+ console.error('Upload Proxy Exception:', error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/app/api/workflow/[[...path]]/route.js b/app/api/workflow/[[...path]]/route.js
new file mode 100644
index 0000000..527809b
--- /dev/null
+++ b/app/api/workflow/[[...path]]/route.js
@@ -0,0 +1,137 @@
+import { NextResponse } from 'next/server';
+
+const MUAPI_BASE = 'https://api.muapi.ai';
+
+function getApiKey(request) {
+ // Priority 1: Direct x-api-key header
+ const headerKey = request.headers.get('x-api-key');
+ if (headerKey) return headerKey;
+
+ // Priority 2: muapi_key cookie (used by the fixed builder library)
+ const cookieKey = request.cookies.get('muapi_key')?.value;
+ return cookieKey;
+}
+
+function cleanHeaders(request) {
+ const headers = new Headers(request.headers);
+ headers.delete('host');
+ headers.delete('connection');
+ headers.delete('cookie'); // CRITICAL: Stop forwarding browser cookies to MuAPI to avoid auth conflicts
+ return headers;
+}
+
+export async function GET(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/workflow/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ console.log(`[proxy GET] ${targetUrl} | apiKey: ${apiKey ? apiKey.slice(0,8)+'...' : 'MISSING'}`);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, {
+ headers,
+ method: 'GET',
+ });
+ const data = await response.json();
+ if (path.includes('get-workflow-def')) {
+ console.log(`[proxy GET] get-workflow-def response: is_owner=${data?.is_owner}, workflow_id=${data?.workflow_id}`);
+ }
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function POST(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/workflow/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ console.log(`[proxy POST] ${targetUrl} | apiKey: ${apiKey ? apiKey.slice(0,8)+'...' : 'MISSING'} | cookie: ${request.cookies.get('muapi_key')?.value?.slice(0,8) || 'NONE'} | header: ${request.headers.get('x-api-key')?.slice(0,8) || 'NONE'}`);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ // Decode body to see what workflow_id is being sent
+ try {
+ const parsed = JSON.parse(Buffer.from(body).toString('utf-8'));
+ console.log(`[proxy POST] body: workflow_id=${parsed.workflow_id}, source_workflow_id=${parsed.source_workflow_id}, name=${parsed.name}`);
+ } catch(e) { /* ignore decode errors */ }
+
+ const response = await fetch(targetUrl, {
+ method: 'POST',
+ headers,
+ body
+ });
+ const data = await response.json();
+ console.log(`[proxy POST] response: status=${response.status}`, JSON.stringify(data).slice(0, 200));
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function DELETE(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/workflow/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, {
+ method: 'DELETE',
+ headers
+ });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function PUT(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/workflow/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ const response = await fetch(targetUrl, {
+ method: 'PUT',
+ headers,
+ body
+ });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/app/studio/page.js b/app/studio/[[...slug]]/page.js
similarity index 100%
rename from app/studio/page.js
rename to app/studio/[[...slug]]/page.js
diff --git a/app/workflow/[id]/[tab]/page.js b/app/workflow/[id]/[tab]/page.js
new file mode 100644
index 0000000..21142c1
--- /dev/null
+++ b/app/workflow/[id]/[tab]/page.js
@@ -0,0 +1,9 @@
+import StandaloneShell from '@/components/StandaloneShell';
+
+export const metadata = {
+ title: 'Workflow — Open Generative AI',
+};
+
+export default function WorkflowTabPage() {
+ return