Custom Apps
Custom Apps provide advanced capabilities beyond the App Builder’s visual templates. Build sophisticated applications with custom code, specialized UI components, complex business logic, and deep integration with M3 Forge’s APIs.
When to Build Custom Apps
Choose custom development when:
- Unique requirements — Use case not covered by App Builder templates
- Complex UI — Advanced interactions, visualizations, or custom components
- External integrations — Deep integration with third-party systems
- Performance optimization — Need fine-grained control over rendering and data flow
- Custom business logic — Complex validation, calculation, or processing rules
For standard use cases, App Builder is faster and requires no code.
Development Approaches
React Component
React Component (Embedded)
Build React components that embed in M3 Forge:
- Leverage M3 Forge’s UI framework
- Access tRPC APIs directly
- Hot module reloading during development
- Deploy as part of M3 Forge build
Best for: Internal tools, admin interfaces, specialized dashboards.
React Component Development
Build apps as React components within M3 Forge.
Project Setup
Clone M3 Forge Repository
git clone https://github.com/marieai/marie-studio.git
cd marie-studioInstall Dependencies
pnpm installStart Development Server
pnpm devFrontend runs at http://localhost:5173.
Create Component
Create new component in packages/frontend/ui/src/features/apps/:
// packages/frontend/ui/src/features/apps/my-app/MyApp.tsx
import { useState } from 'react';
import { trpc } from '@/lib/trpc';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
export function MyApp() {
const [data, setData] = useState(null);
const { mutate: processData } = trpc.myApp.process.useMutation({
onSuccess: (result) => setData(result),
});
return (
<Card className="p-6">
<h1 className="text-2xl font-bold mb-4">My Custom App</h1>
<Button onClick={() => processData({ input: 'test' })}>
Process
</Button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</Card>
);
}Add Route
Register route in packages/frontend/ui/src/App.tsx:
import { MyApp } from '@/features/apps/my-app/MyApp';
// In router configuration
{
path: '/apps/my-app',
element: <MyApp />,
}Test Component
Navigate to http://localhost:5173/apps/my-app to see your app.
UI Components
M3 Forge provides comprehensive component library:
Base Components:
- Button, Input, Textarea, Select
- Card, Modal, Dialog, Sheet
- Table, List, Tabs, Accordion
Complex Components:
- PageContainer, PageHeader
- ResizableSidePanel
- LoadingSpinner, ErrorAlert
- Badge, Tag, Toast
Workflow Components:
- DagFlow (React Flow wrapper)
- NodePalette
- WorkflowExecutionStatus
Document Components:
- DocumentViewer
- AnnotationTools
- FieldEditor
Import from @/components/ui or @/components/.
M3 Forge uses shadcn/ui components. See shadcn/ui docs for component reference.
Styling
Use Tailwind CSS for styling:
<div className="flex items-center gap-4 p-6 bg-surface-1 rounded-lg">
<Button className="bg-brand hover:bg-brand-hover">
Primary Action
</Button>
</div>Design Tokens:
- Colors:
text-content-primary,bg-surface-1,border-line - Spacing:
gap-2,p-4,m-6 - Typography:
text-sm,font-medium,leading-relaxed
See packages/frontend/ui/src/index.css for full token list.
API Integration
Use tRPC for type-safe API calls:
Query (Read):
const { data, isLoading, error } = trpc.myResource.list.useQuery({
filter: 'active',
});Mutation (Write):
const { mutate, isPending } = trpc.myResource.create.useMutation({
onSuccess: () => {
// Refetch data or navigate
},
});
// Call mutation
mutate({ name: 'New Item' });Subscription (Real-Time):
trpc.myResource.subscribe.useSubscription(undefined, {
onData: (update) => {
// Handle real-time update
},
});tRPC provides full TypeScript types from backend to frontend.
Backend Development
Add custom backend logic for your app.
Creating tRPC Router
Create Router File
Create router in packages/api/src/server/trpc/routers/:
// packages/api/src/server/trpc/routers/my-app.router.ts
import { z } from 'zod';
import { publicProcedure, router } from '../index.js';
export const myAppRouter = router({
process: publicProcedure
.input(z.object({
input: z.string()
}))
.mutation(async ({ input, ctx }) => {
// Process data
const result = await processData(input.input);
return { success: true, result };
}),
list: publicProcedure
.input(z.object({
filter: z.string().optional()
}))
.query(async ({ input, ctx }) => {
// Fetch data
const items = await fetchItems(input.filter);
return items;
}),
});Register Router
Add to main router in packages/api/src/server/trpc/index.ts:
import { myAppRouter } from './routers/my-app.router.js';
export const appRouter = router({
// ... existing routers
myApp: myAppRouter,
});Use in Frontend
Frontend automatically gets types:
// TypeScript knows exact shape of inputs/outputs
const { data } = trpc.myApp.list.useQuery({ filter: 'active' });
// ^? { id: string; name: string }[]Database Access
Use Prisma for database operations:
import { prisma } from '@marie/db';
export async function fetchItems(filter?: string) {
return prisma.myTable.findMany({
where: filter ? { status: filter } : undefined,
});
}Add database models in packages/@marie/db/prisma/schema.prisma.
Workflow Integration
Trigger workflows from your app:
import { WorkflowService } from '@marie/core';
const workflowService = container.get(WorkflowService);
export async function runWorkflow(input: any) {
const execution = await workflowService.execute({
workflowId: 'my-workflow',
inputs: input,
});
return execution.id;
}Monitor execution via WorkflowExecutionService.
Standalone App Development
Build separate app that uses M3 Forge APIs.
API Authentication
Obtain API token:
- Navigate to Admin → API Tokens
- Click Create Token
- Set permissions and expiration
- Copy token (shown once)
Use token in requests:
curl -H "Authorization: Bearer <token>" \
https://api.m3studio.ai/trpc/workflows.listREST vs tRPC
M3 Forge supports both:
tRPC (Recommended):
- Type-safe
- Automatic serialization
- Batching support
- Generated TypeScript client
REST:
- Standard HTTP
- Works with any language
- OpenAPI documentation
- Simpler for external integrations
Sample Integration
TypeScript
TypeScript Client
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@marie/api';
const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'https://api.m3studio.ai/trpc',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
},
}),
],
});
// Type-safe API calls
const workflows = await client.workflows.list.query();Webhook Integration
Receive events from M3 Forge:
Create Webhook Endpoint
Implement endpoint in your app:
app.post('/webhooks/m3studio', (req, res) => {
const { event, data } = req.body;
// Verify signature
const signature = req.headers['x-m3-signature'];
if (!verifySignature(signature, req.body)) {
return res.status(401).send('Invalid signature');
}
// Process event
switch (event) {
case 'workflow.completed':
handleWorkflowComplete(data);
break;
case 'document.processed':
handleDocumentProcessed(data);
break;
}
res.status(200).send('OK');
});Register Webhook
Add webhook in M3 Forge:
- Navigate to Admin → Webhooks
- Click Add Webhook
- Enter URL:
https://your-app.com/webhooks/m3studio - Select events to receive
- Save webhook
Test Webhook
Trigger test event to verify endpoint.
Plugin Runner
Package apps as installable plugins.
Plugin Structure
my-plugin/
├── plugin.json # Metadata and configuration
├── src/
│ ├── index.tsx # Entry point
│ ├── components/ # React components
│ └── api/ # Backend logic
├── assets/ # Images, styles
└── README.md # DocumentationPlugin Manifest
plugin.json defines plugin:
{
"name": "my-custom-app",
"version": "1.0.0",
"displayName": "My Custom App",
"description": "Custom application for specialized use case",
"author": "Your Organization",
"entrypoint": "src/index.tsx",
"permissions": [
"workflows:read",
"documents:write"
],
"routes": [
{
"path": "/apps/my-custom-app",
"component": "MyApp"
}
],
"dependencies": {
"react": "^19.0.0",
"@marie/core": "workspace:*"
}
}Plugin Development
Create plugin components:
// src/index.tsx
import { PluginApp } from '@marie/plugin-sdk';
export const MyPlugin: PluginApp = {
id: 'my-custom-app',
initialize: (context) => {
// Setup code
console.log('Plugin initialized');
},
routes: [
{
path: '/apps/my-custom-app',
component: () => import('./components/MyApp'),
},
],
cleanup: () => {
// Cleanup code
},
};Plugin Installation
Install plugin in M3 Forge:
pnpm install my-custom-app-pluginOr upload via UI:
- Navigate to Admin → Plugins
- Click Upload Plugin
- Select plugin ZIP file
- Configure permissions
- Enable plugin
Plugins run in sandbox with restricted permissions. Request only necessary permissions in manifest.
Testing
Test custom apps thoroughly:
Unit Tests
Test components and logic:
import { render, screen } from '@testing-library/react';
import { MyApp } from './MyApp';
test('renders app title', () => {
render(<MyApp />);
expect(screen.getByText('My Custom App')).toBeInTheDocument();
});Run tests:
pnpm testIntegration Tests
Test API integration:
import { trpc } from '@/lib/trpc';
test('processes data correctly', async () => {
const result = await trpc.myApp.process.mutate({
input: 'test'
});
expect(result.success).toBe(true);
});E2E Tests
Test full user flows:
import { test, expect } from '@playwright/test';
test('completes workflow', async ({ page }) => {
await page.goto('/apps/my-app');
await page.click('text=Start Process');
await expect(page.locator('.success-message')).toBeVisible();
});Deployment
Deploy custom apps to production:
Embedded Apps
Apps built as React components deploy with M3 Forge:
- Commit code to repository
- Run
pnpm build - Deploy built artifacts
- App available at configured route
Standalone Apps
Deploy separately from M3 Forge:
- Build app:
pnpm buildor equivalent - Deploy to hosting (Vercel, Netlify, AWS, etc.)
- Configure CORS for M3 Forge API
- Set environment variables (API URL, tokens)
Plugin Apps
Publish plugins for installation:
- Build plugin:
pnpm build - Package as ZIP or publish to npm
- Upload to M3 Forge plugin marketplace (if public)
- Users install via UI or package manager
Best Practices
Code Organization
- Feature folders — Group related files together
- Shared utilities — Extract common logic
- Type safety — Use TypeScript strictly
- Component composition — Build reusable components
Performance
- Code splitting — Lazy load routes and heavy components
- Memoization — Use React Compiler (no manual
useMemo) - Pagination — Load data incrementally
- Caching — Use tRPC query caching
Security
- Input validation — Validate all user inputs
- Authorization — Check permissions on every request
- XSS prevention — Sanitize user content
- CSRF protection — Use CSRF tokens for mutations
Accessibility
- Semantic HTML — Use proper elements
- ARIA labels — Add for screen readers
- Keyboard navigation — Support keyboard-only use
- Color contrast — Meet WCAG standards
Next Steps
- Use Webhooks for event-driven integration
- Build on App Builder templates
- Integrate Chat Assistant capabilities
- Connect to Workflows and Processors