oneapp-backstage
Visual AI workflow designer with drag-and-drop interface. Build complex AI agents without code. 8 node types, Monaco editor, external integrations (Linear, Slack). Microfrontend embedded in oneapp-onstage.
Quick Start
Create your first AI workflow in 3 minutes:
pnpm dev:oneappAccess at localhost:3500/backstage. Drag nodes, connect edges, deploy. Skip to Quick Start →
Why oneapp-backstage?
AI workflows built with hard-coded logic. Agent changes require code deployment. No visual debugging for complex chains. External integrations duplicated across projects. Prompt templates scattered in codebases. Team collaboration on AI agents requires technical skills.
oneapp-backstage solves this by providing a visual workflow designer where anyone can create, test, and deploy AI agents with drag-and-drop nodes.
Production-ready with 8 node categories, Monaco code editor, external integrations, microfrontend architecture, real-time workflow execution, and CSP-compliant security.
Use cases
- AI Agent Design — Build conversational agents with LLM, RAG, and tool nodes
- Workflow Automation — Connect AI to Linear, Slack, GitHub, Notion
- Admin Dashboard — Manage AI workflows, monitor execution, debug failures
- Prompt Engineering — Test and iterate on prompts visually with real data
- Guardrail Configuration — Add safety checks (PII, toxicity, jailbreak) to workflows
How it works
oneapp-backstage provides a visual workflow designer built on React Flow:
// app/workflows/page.tsx
import { WorkflowDesigner } from "#/components/flow/FlowCanvas";
export default function WorkflowsPage() {
return (
<div className="h-screen">
<WorkflowDesigner />
);
}
// components/flow/FlowCanvas.tsx
import { ReactFlow, useNodesState, useEdgesState } from "@xyflow/react";
import { InputNode, LLMNode, RAGNode, OutputNode } from "#/components/nodes";
const nodeTypes = {
inputNode: InputNode,
llmNode: LLMNode,
ragNode: RAGNode,
outputNode: OutputNode,
};
export function WorkflowDesigner() {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
fitView
/>
);
}Uses @xyflow/react for canvas, Monaco for code editing, and microfrontend architecture for seamless embedding in oneapp-onstage.
Key features
8 Node Categories — Input, LLM, RAG, Tool, Logic, Output, Integration, Guardrail
Visual Designer — Drag-and-drop interface with React Flow
Monaco Editor — In-node code editing with syntax highlighting
External Integrations — Linear, Slack, GitHub, Notion connectors
Microfrontend — Embedded at /backstage in oneapp-onstage
Real-time Execution — Test workflows with live data
Quick Start
1. Start the microfrontend
# Start all OneApp services
pnpm dev:oneapp2. Access the designer
Open http://localhost:3500/backstage in your browser.
3. Create your first workflow
// Drag nodes onto canvas:
// 1. Input Node (type: text) → captures user question
// 2. LLM Node (provider: anthropic, model: claude-3-sonnet) → processes question
// 3. Output Node (format: markdown) → returns answer
// Connect edges: Input → LLM → Output
// Click "Test Workflow" to execute with sample data4. Deploy the workflow
import { executeWorkflow } from "@repo/oneapp-shared/ai-agent";
export async function POST(request: Request) {
const { workflowId, input } = await request.json();
const result = await executeWorkflow(workflowId, input);
return Response.json(result);
}That's it! Your AI workflow is now accessible via API and can be called from oneapp-onstage or external services.
Standalone Development
Run backstage independently:
pnpm --filter oneapp-backstage devAccess at localhost:3600 for faster iteration during development.
Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | platform/apps/oneapp-backstage |
| Port | 3600 (standalone), embedded at /backstage in oneapp-onstage |
| Framework | Next.js 16 (App Router, React 19, Turbopack) |
| UI Library | @xyflow/react (React Flow) |
| Editor | Monaco Editor (@monaco-editor/react) |
| Tech Stack | TypeScript 5, Tailwind CSS 4, Better Auth, Prisma ORM |
Project Structure
oneapp-backstage/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (dashboard)/ # Dashboard routes (layout groups)
│ │ │ ├── workflows/ # Workflow management
│ │ │ │ ├── page.tsx # Workflows list
│ │ │ │ ├── [id]/ # Workflow editor
│ │ │ │ │ └── page.tsx # Visual designer
│ │ │ │ └── new/ # Create workflow
│ │ │ ├── agents/ # Agent management
│ │ │ ├── integrations/ # External service config
│ │ │ └── settings/ # App settings
│ │ ├── api/ # API routes
│ │ │ ├── workflows/ # Workflow CRUD
│ │ │ │ ├── execute/ # Execute workflow
│ │ │ │ └── validate/ # Validate workflow
│ │ │ └── integrations/ # Integration endpoints
│ │ └── layout.tsx # Root layout
│ ├── components/
│ │ ├── flow/ # React Flow components
│ │ │ ├── FlowCanvas.tsx # Main canvas wrapper
│ │ │ ├── FlowControls.tsx # Zoom, fit view controls
│ │ │ ├── FlowMinimap.tsx # Minimap component
│ │ │ ├── FlowToolbar.tsx # Node palette
│ │ │ └── FlowSidebar.tsx # Node properties panel
│ │ ├── nodes/ # Node implementations
│ │ │ ├── InputNode.tsx # Input node
│ │ │ ├── LLMNode.tsx # LLM node
│ │ │ ├── RAGNode.tsx # RAG node
│ │ │ ├── ToolNode.tsx # Tool node
│ │ │ ├── LogicNode.tsx # Logic node
│ │ │ ├── OutputNode.tsx # Output node
│ │ │ ├── IntegrationNode.tsx # Integration node
│ │ │ └── GuardrailNode.tsx # Guardrail node
│ │ ├── editors/ # Code editors
│ │ │ ├── MonacoEditor.tsx # Monaco wrapper
│ │ │ └── PromptEditor.tsx # Prompt template editor
│ │ └── ui/ # Shared UI components
│ ├── hooks/ # Custom React hooks
│ │ ├── useWorkflow.ts # Workflow state management
│ │ ├── useNodes.ts # Node operations
│ │ ├── useEdges.ts # Edge operations
│ │ └── useExecution.ts # Workflow execution
│ ├── lib/ # Utilities and helpers
│ │ ├── workflow-executor.ts # Workflow runtime
│ │ ├── node-validators.ts # Node validation logic
│ │ └── integration-client.ts # External API clients
│ └── types/ # TypeScript definitions
│ ├── nodes.ts # Node type definitions
│ ├── workflow.ts # Workflow types
│ └── integrations.ts # Integration types
├── env.ts # Environment config (@t3-oss/env-nextjs)
├── next.config.ts # Next.js config with CSP
└── package.json # DependenciesNode Types
1. Input Nodes (📥)
Capture user input and trigger workflows.
// components/nodes/InputNode.tsx
import { Handle, Position } from "@xyflow/react";
import { z } from "zod/v4";
interface InputNodeData {
type: "text" | "file" | "voice" | "webhook";
label: string;
placeholder?: string;
validation?: z.ZodSchema;
defaultValue?: string;
}
export function InputNode({ data }: { data: InputNodeData }) {
return (
<div className="rounded-lg border bg-white p-4 shadow-md">
<div className="text-sm font-semibold">📥 {data.label}
<div className="mt-2 text-xs text-gray-500">Type: {data.type}
{/* Output handle */}
<Handle type="source" position={Position.Right} />
);
}Supported Input Types:
text— Single-line or multi-line text inputfile— File upload (PDF, DOCX, images)voice— Audio input with transcriptionwebhook— External HTTP triggers
2. LLM Nodes (🤖)
Process text with large language models.
// components/nodes/LLMNode.tsx
interface LLMNodeData {
provider: "openai" | "anthropic" | "google";
model: string;
systemPrompt: string;
temperature: number;
maxTokens: number;
streaming?: boolean;
}
export function LLMNode({ data }: { data: LLMNodeData }) {
return (
<div className="rounded-lg border bg-blue-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">🤖 LLM
<div className="mt-2 space-y-1 text-xs">
<div>Provider: {data.provider}
<div>Model: {data.model}
<div>Temperature: {data.temperature}
<Handle type="source" position={Position.Right} />
);
}Supported Providers:
- OpenAI — GPT-4, GPT-3.5 Turbo
- Anthropic — Claude 3 Sonnet, Claude 3 Opus
- Google — Gemini Pro, Gemini Flash
3. RAG Nodes (📚)
Vector search and document retrieval.
// components/nodes/RAGNode.tsx
interface RAGNodeData {
vectorStore: "pinecone" | "upstash" | "qdrant";
topK: number;
threshold: number;
namespace?: string;
embeddingModel?: string;
}
export function RAGNode({ data }: { data: RAGNodeData }) {
return (
<div className="rounded-lg border bg-purple-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">📚 RAG
<div className="mt-2 space-y-1 text-xs">
<div>Store: {data.vectorStore}
<div>Top K: {data.topK}
<div>Threshold: {data.threshold}
<Handle type="source" position={Position.Right} />
);
}Vector Store Integrations:
- Pinecone — Managed vector database
- Upstash Vector — Serverless vector database
- Qdrant — Self-hosted vector search engine
4. Tool Nodes (🔧)
Execute functions and external API calls.
// components/nodes/ToolNode.tsx
interface ToolNodeData {
toolId: string;
toolName: string;
parameters: Record<string, unknown>;
timeout: number;
retries?: number;
}
export function ToolNode({ data }: { data: ToolNodeData }) {
return (
<div className="rounded-lg border bg-green-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">🔧 {data.toolName}
<div className="mt-2 text-xs text-gray-500">
Timeout: {data.timeout}ms
<Handle type="source" position={Position.Right} />
);
}Available Tools:
- Web Search — Google, Brave Search API
- Calculator — Mathematical expressions
- Code Executor — Run Python, JavaScript code
- API Call — Generic HTTP requests
- Database Query — SQL/Prisma queries
5. Logic Nodes (🔀)
Conditional routing and branching.
// components/nodes/LogicNode.tsx
interface LogicNodeData {
condition: string; // JavaScript expression
branches: {
label: string;
targetNodeId: string;
condition?: string;
}[];
}
export function LogicNode({ data }: { data: LogicNodeData }) {
return (
<div className="rounded-lg border bg-yellow-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">🔀 Logic
<div className="mt-2 text-xs">
{data.branches.length} branches
{/* Multiple output handles for branches */}
{data.branches.map((branch, i) => (
<Handle
key={branch.label}
type="source"
position={Position.Right}
id={branch.label}
style={{ top: `${30 + i * 20}px` }}
/>
))}
);
}Logic Operators:
- Conditional —
if/elsebranching - Switch — Multiple condition matching
- Loop — Iterate over arrays
- Filter — Conditional filtering
6. Output Nodes (📤)
Generate artifacts and responses.
// components/nodes/OutputNode.tsx
interface OutputNodeData {
format: "text" | "json" | "markdown" | "html" | "stream";
template?: string;
transformation?: string; // JS code
}
export function OutputNode({ data }: { data: OutputNodeData }) {
return (
<div className="rounded-lg border bg-gray-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">📤 Output
<div className="mt-2 text-xs text-gray-500">
Format: {data.format}
);
}Output Formats:
- Text — Plain text response
- JSON — Structured data
- Markdown — Formatted text
- HTML — Rich content
- Stream — Real-time streaming response
7. Integration Nodes (🔌)
Connect to external services.
// components/nodes/IntegrationNode.tsx
interface IntegrationNodeData {
service: "slack" | "linear" | "github" | "notion";
action: string;
credentials: string; // Reference to stored credentials
config: Record<string, unknown>;
}
export function IntegrationNode({ data }: { data: IntegrationNodeData }) {
return (
<div className="rounded-lg border bg-indigo-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">🔌 {data.service}
<div className="mt-2 text-xs text-gray-500">
Action: {data.action}
<Handle type="source" position={Position.Right} />
);
}Supported Integrations:
- Slack — Send messages, create channels, manage users
- Linear — Create issues, update status, search
- GitHub — Create PRs, issues, search code
- Notion — Create pages, update databases
8. Guardrail Nodes (🛡️)
Safety and validation checks.
// components/nodes/GuardrailNode.tsx
interface GuardrailNodeData {
checks: ("pii" | "toxicity" | "jailbreak" | "custom")[];
action: "block" | "warn" | "flag" | "sanitize";
customRules?: string[];
severity?: "low" | "medium" | "high";
}
export function GuardrailNode({ data }: { data: GuardrailNodeData }) {
return (
<div className="rounded-lg border border-red-300 bg-red-50 p-4 shadow-md">
<Handle type="target" position={Position.Left} />
<div className="text-sm font-semibold">🛡️ Guardrail
<div className="mt-2 text-xs">
{data.checks.length} checks active
<Handle type="source" position={Position.Right} />
);
}Safety Checks:
- PII Detection — Detect emails, SSNs, credit cards
- Toxicity — Detect harmful, offensive content
- Jailbreak — Prevent prompt injection attacks
- Custom Rules — Regex or LLM-based validation
Workflow Execution
Workflow Runtime
// lib/workflow-executor.ts
import type { Node, Edge } from "@xyflow/react";
import { executeNode } from "./node-executors";
export async function executeWorkflow(nodes: Node[], edges: Edge[], input: Record<string, unknown>) {
// Build execution graph
const graph = buildExecutionGraph(nodes, edges);
// Find start nodes (no incoming edges)
const startNodes = nodes.filter((node) => !edges.some((edge) => edge.target === node.id));
// Execute nodes in topological order
const results = new Map<string, unknown>();
for (const node of startNodes) {
await executeNodeRecursive(node, graph, results, input);
}
// Return final output
const outputNodes = nodes.filter((n) => n.type === "outputNode");
return outputNodes.map((n) => results.get(n.id));
}
async function executeNodeRecursive(
node: Node,
graph: Map<string, Node[]>,
results: Map<string, unknown>,
input: Record<string, unknown>
) {
// Get input from previous nodes
const nodeInput = collectNodeInput(node, results, input);
// Execute node logic
const result = await executeNode(node, nodeInput);
results.set(node.id, result);
// Execute downstream nodes
const downstream = graph.get(node.id) || [];
for (const nextNode of downstream) {
await executeNodeRecursive(nextNode, graph, results, input);
}
}Node Execution
// lib/node-executors/index.ts
export async function executeNode(node: Node, input: Record<string, unknown>) {
switch (node.type) {
case "inputNode":
return input[node.data.label] || node.data.defaultValue;
case "llmNode":
return executeLLMNode(node.data, input);
case "ragNode":
return executeRAGNode(node.data, input);
case "toolNode":
return executeToolNode(node.data, input);
case "logicNode":
return executeLogicNode(node.data, input);
case "outputNode":
return executeOutputNode(node.data, input);
case "integrationNode":
return executeIntegrationNode(node.data, input);
case "guardrailNode":
return executeGuardrailNode(node.data, input);
default:
throw new Error(`Unknown node type: ${node.type}`);
}
}
// lib/node-executors/llm.ts
async function executeLLMNode(data: LLMNodeData, input: Record<string, unknown>) {
const { provider, model, systemPrompt, temperature, maxTokens } = data;
// Call AI provider
if (provider === "anthropic") {
const response = await anthropic.messages.create({
model,
system: systemPrompt,
messages: [{ role: "user", content: String(input.prompt) }],
temperature,
max_tokens: maxTokens
});
return response.content[0].text;
}
// ... other providers
}Monaco Editor Integration
Code Editor Component
// components/editors/MonacoEditor.tsx
"use client";
import Editor from "@monaco-editor/react";
interface MonacoEditorProps {
value: string;
onChange: (value: string) => void;
language?: "javascript" | "typescript" | "python" | "json";
height?: string;
}
export function MonacoEditor({
value,
onChange,
language = "javascript",
height = "300px",
}: MonacoEditorProps) {
return (
<Editor
height={height}
defaultLanguage={language}
value={value}
onChange={(value) => onChange(value || "")}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: "on",
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
);
}Prompt Template Editor
// components/editors/PromptEditor.tsx
import { MonacoEditor } from "./MonacoEditor";
export function PromptEditor({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
return (
<div className="space-y-2">
<label className="text-sm font-medium">System Prompt</label>
<MonacoEditor
value={value}
onChange={onChange}
language="markdown"
height="200px"
/>
<div className="text-xs text-gray-500">
Use {{variable}} for dynamic values
);
}Microfrontend Architecture
Host Configuration (oneapp-onstage)
// oneapp-onstage/next.config.ts
const microfrontends = {
"oneapp-backstage": {
routes: ["/backstage", "/backstage-preview"],
assetPrefix: "/oneapp-backstage-assets",
localPort: 3600,
remoteUrl:
process.env.NODE_ENV === "production"
? "https://backstage.yourdomain.com"
: `http://localhost:3600`,
},
};
// Proxy configuration
async rewrites() {
return [
{
source: "/backstage/:path*",
destination: `http://localhost:3600/backstage/:path*`,
},
{
source: "/oneapp-backstage-assets/:path*",
destination: `http://localhost:3600/_next/:path*`,
},
];
}Remote Configuration (oneapp-backstage)
// oneapp-backstage/next.config.ts
const nextConfig = {
basePath: process.env.NODE_ENV === "production" ? "" : "/backstage",
assetPrefix: process.env.NODE_ENV === "production" ? "" : "/oneapp-backstage-assets"
};Route Configuration
| Route | Description | Feature Flag |
|---|---|---|
/backstage | Workflows list | None |
/backstage/workflows | Workflow management | None |
/backstage/workflows/[id] | Workflow editor | None |
/backstage/agents | Agent management | None |
/backstage/integrations | Integration settings | None |
/backstage-preview/* | Preview/beta features | backstage-preview |
Environment Variables
Required Variables
# Database (Neon Postgres)
DATABASE_URL="postgresql://user:pass@host/db?sslmode=require"
DATABASE_URL_UNPOOLED="postgresql://user:pass@host/db?sslmode=require"
# Authentication (Better Auth)
BETTER_AUTH_SECRET="generate-random-secret-key"
BETTER_AUTH_URL="http://localhost:3500"
TRUSTED_ORIGINS="http://localhost:3500,http://localhost:3600"
# Feature Flags (for backstage-preview route)
NEXT_PUBLIC_FEATURE_BACKSTAGE_PREVIEW="false"AI Providers
# AI Gateway (optional proxy)
AI_GATEWAY_API_KEY="your-gateway-key"
# OpenAI
OPENAI_API_KEY="sk-..."
# Anthropic
ANTHROPIC_API_KEY="sk-ant-..."
# Google AI
GOOGLE_AI_API_KEY="..."Vector Stores
# Pinecone
PINECONE_API_KEY="..."
PINECONE_ENVIRONMENT="us-east1-gcp"
# Upstash Vector
UPSTASH_VECTOR_REST_URL="https://..."
UPSTASH_VECTOR_REST_TOKEN="..."
# Qdrant
QDRANT_URL="http://localhost:6333"
QDRANT_API_KEY="..."External Integrations
# Linear
LINEAR_API_KEY="lin_api_..."
# Slack
SLACK_BOT_TOKEN="xoxb-..."
SLACK_SIGNING_SECRET="..."
# GitHub
GITHUB_TOKEN="ghp_..."
# Notion
NOTION_API_KEY="secret_..."Email (Resend)
RESEND_TOKEN="re_..."
RESEND_FROM="noreply@yourdomain.com"Content Security Policy
oneapp-backstage configures CSP for Monaco Editor and external API calls:
// next.config.ts
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self' data:;
connect-src 'self'
https://api.anthropic.com
https://api.openai.com
https://generativelanguage.googleapis.com
https://api.linear.app
https://slack.com
wss://slack.com;
worker-src 'self' blob:;
frame-src 'self';
`;
export default {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: cspHeader.replace(/\n/g, "")
}
]
}
];
}
};Key CSP Rules:
unsafe-eval— Required for Monaco Editorunsafe-inline— Required for Monaco syntax highlightingworker-src blob:— Required for Monaco web workersconnect-src— Whitelist AI provider APIs
API Routes
Execute Workflow
// app/api/workflows/execute/route.ts
import { NextRequest } from "next/server";
import { executeWorkflow } from "#/lib/workflow-executor";
import { db } from "@repo/db-prisma";
export async function POST(request: NextRequest) {
const { workflowId, input } = await request.json();
// Fetch workflow from database
const workflow = await db.workflow.findUnique({
where: { id: workflowId },
include: { nodes: true, edges: true }
});
if (!workflow) {
return Response.json({ error: "Workflow not found" }, { status: 404 });
}
// Execute workflow
const result = await executeWorkflow(workflow.nodes, workflow.edges, input);
return Response.json({ result });
}Validate Workflow
// app/api/workflows/validate/route.ts
import { validateWorkflow } from "#/lib/node-validators";
export async function POST(request: NextRequest) {
const { nodes, edges } = await request.json();
const validation = validateWorkflow(nodes, edges);
if (!validation.valid) {
return Response.json({ valid: false, errors: validation.errors }, { status: 400 });
}
return Response.json({ valid: true });
}Testing
Type Checking
pnpm --filter oneapp-backstage typecheckLinting
pnpm --filter oneapp-backstage lintManual Testing
- Start the app:
pnpm dev:oneapp - Navigate to
http://localhost:3500/backstage - Create a new workflow:
- Drag Input node → LLM node → Output node
- Connect edges between nodes
- Configure LLM node with provider, model, prompt
- Click "Test Workflow" with sample input
- Verify execution completes successfully
- Check output format matches expected result
Deployment
oneapp-backstage deploys as a microfrontend embedded in oneapp-onstage. No separate deployment needed.
Build Process
# Build backstage (happens automatically during oneapp-onstage build)
pnpm --filter oneapp-backstage buildAsset Routing
Assets are served from /oneapp-backstage-assets/* on the host application:
# Production URLs
https://yourdomain.com/backstage → oneapp-backstage pages
https://yourdomain.com/oneapp-backstage-assets → oneapp-backstage static assetsEnvironment Variables (Production)
Set all required environment variables in your hosting platform (Vercel, Railway, etc.):
- Database:
DATABASE_URL,DATABASE_URL_UNPOOLED - Auth:
BETTER_AUTH_SECRET,BETTER_AUTH_URL,TRUSTED_ORIGINS - AI Providers:
OPENAI_API_KEY,ANTHROPIC_API_KEY,GOOGLE_AI_API_KEY - Integrations:
LINEAR_API_KEY,SLACK_BOT_TOKEN,GITHUB_TOKEN,NOTION_API_KEY
Troubleshooting
Monaco Editor Not Loading
Issue: Monaco Editor fails to load with CSP errors.
Solution: Ensure CSP allows unsafe-eval and worker-src blob::
const cspHeader = `
script-src 'self' 'unsafe-eval';
worker-src 'self' blob:;
`;Nodes Not Connecting
Issue: Edges don't connect between nodes.
Solution: Verify Handle components have correct type and position:
// Source node
<Handle type="source" position={Position.Right} />
// Target node
<Handle type="target" position={Position.Left} />Workflow Execution Fails
Issue: Workflow execution fails with timeout errors.
Solution: Increase node timeout or add error handling:
const result = await executeNode(node, input).catch((error) => {
console.error(`Node ${node.id} failed:`, error);
return { error: error.message };
});Microfrontend Routes Not Working
Issue: /backstage routes return 404.
Solution: Verify proxy configuration in oneapp-onstage:
// next.config.ts
async rewrites() {
return [
{
source: "/backstage/:path*",
destination: "http://localhost:3600/backstage/:path*",
},
];
}Related Documentation
- oneapp-onstage — Host application embedding backstage
- oneapp-api — REST API for workflow execution
- @repo/oneapp-shared — Shared Prisma schema and ORM functions