Skip to Content
ApplicationsCustom Apps

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 (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-studio

Install Dependencies

pnpm install

Start Development Server

pnpm dev

Frontend 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:

  1. Navigate to Admin → API Tokens
  2. Click Create Token
  3. Set permissions and expiration
  4. Copy token (shown once)

Use token in requests:

curl -H "Authorization: Bearer <token>" \ https://api.m3studio.ai/trpc/workflows.list

REST 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 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:

  1. Navigate to Admin → Webhooks
  2. Click Add Webhook
  3. Enter URL: https://your-app.com/webhooks/m3studio
  4. Select events to receive
  5. 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 # Documentation

Plugin 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-plugin

Or upload via UI:

  1. Navigate to Admin → Plugins
  2. Click Upload Plugin
  3. Select plugin ZIP file
  4. Configure permissions
  5. 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 test

Integration 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:

  1. Commit code to repository
  2. Run pnpm build
  3. Deploy built artifacts
  4. App available at configured route

Standalone Apps

Deploy separately from M3 Forge:

  1. Build app: pnpm build or equivalent
  2. Deploy to hosting (Vercel, Netlify, AWS, etc.)
  3. Configure CORS for M3 Forge API
  4. Set environment variables (API URL, tokens)

Plugin Apps

Publish plugins for installation:

  1. Build plugin: pnpm build
  2. Package as ZIP or publish to npm
  3. Upload to M3 Forge plugin marketplace (if public)
  4. 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

Last updated on