Motia Icon
Development Guide

Overriding the default configuration

Configure your Motia application with motia.config.ts - Express customization, Redis, security, file uploads, and more.

The motia.config.ts file is the central configuration for your Motia application. Here you can customize Express, configure Redis, add security middleware, handle file uploads, set up stream authentication, and more.

Quick Start

Create a motia.config.ts file in your project root:

motia.config.ts
import { defineConfig } from 'motia'
 
export default defineConfig({
  // All configuration is optional
})

That's it. Motia works out of the box with sensible defaults.


Critical Requirement: ES Modules

Your package.json must have "type": "module"

Motia uses ES modules internally. Without this setting, you'll get import/export errors at runtime.

package.json
{
  "name": "my-motia-app",
  "type": "module",
  "scripts": {
    "dev": "motia dev",
    "start": "motia start",
    "build": "motia build"
  }
}

Configuration Options

OptionTypeDescription
app(app: Express) => voidCustomize the Express instance
pluginsMotiaPluginBuilder[]Add Workbench plugins
adaptersAdapterConfigCustom adapters for scaling
streamAuthStreamAuthConfigSecure real-time streams
redisRedisConfigRedis connection settings

Express Customization

Use the app callback to add middleware, routes, or any Express configuration. This runs before Motia's built-in middleware.

motia.config.ts
import { defineConfig } from 'motia'
 
export default defineConfig({
  app: (app) => {
    // Your middleware runs first
    app.use((req, res, next) => {
      console.log('Request:', req.method, req.path)
      next()
    })
  }
})

Health Check Endpoint

Add a simple health check for load balancers and monitoring:

motia.config.ts
import { defineConfig } from 'motia'
 
export default defineConfig({
  app: (app) => {
    app.get('/health', (req, res) => {
      res.json({ 
        status: 'healthy',
        timestamp: new Date().toISOString()
      })
    })
  }
})

Security with Helmet

Helmet adds security headers to protect against common vulnerabilities.

Install:

npm install helmet

Configure:

motia.config.ts
import { defineConfig } from 'motia'
import helmet from 'helmet'
 
export default defineConfig({
  app: (app) => {
    app.use(helmet())
  }
})

For more control:

motia.config.ts
import { defineConfig } from 'motia'
import helmet from 'helmet'
 
export default defineConfig({
  app: (app) => {
    app.use(helmet({
      contentSecurityPolicy: false, // Disable if using inline scripts
      crossOriginEmbedderPolicy: false
    }))
  }
})

CORS Configuration

Built-in CORS: Motia automatically adds permissive CORS headers to all responses. You only need custom CORS if you want to restrict origins.

Default Behavior

Motia automatically sets these headers on all responses:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true

Custom CORS

To restrict origins or customize CORS behavior:

Install:

npm install cors

Configure:

motia.config.ts
import { defineConfig } from 'motia'
import cors from 'cors'
 
export default defineConfig({
  app: (app) => {
    // This runs before Motia's default CORS
    app.use(cors({
      origin: ['https://myapp.com', 'https://admin.myapp.com'],
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'DELETE']
    }))
  }
})

Environment-based origins:

motia.config.ts
import { defineConfig } from 'motia'
import cors from 'cors'
 
export default defineConfig({
  app: (app) => {
    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
    
    app.use(cors({
      origin: allowedOrigins,
      credentials: true
    }))
  }
})

Middleware

Add any Express middleware in the app callback. Middleware runs in the order you define it.

motia.config.ts
import { defineConfig } from 'motia'
import helmet from 'helmet'
import cors from 'cors'
import morgan from 'morgan'
 
export default defineConfig({
  app: (app) => {
    // Security headers
    app.use(helmet())
    
    // CORS
    app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }))
    
    // Request logging
    app.use(morgan('combined'))
    
    // Custom middleware
    app.use((req, res, next) => {
      req.startTime = Date.now()
      next()
    })
  }
})

Middleware Order: The app callback runs before Motia's body parsers. If you need to access req.body, your middleware will need to parse it or wait for Motia's parsers.


File Uploads

Limitation: Motia Step handlers receive a simplified ApiRequest object, not the raw Express request. This means traditional Multer middleware cannot pass file data directly to your handlers.

For file uploads, use one of these approaches:

Option 1: Base64 Encoding (Small Files)

For small files (under a few MB), encode as Base64 in the request body:

steps/upload-file.step.ts
import { ApiRouteConfig, Handlers } from 'motia'
import { z } from 'zod'
import fs from 'fs/promises'
 
export const config: ApiRouteConfig = {
  type: 'api',
  name: 'UploadFile',
  path: '/upload',
  method: 'POST',
  bodySchema: z.object({
    filename: z.string(),
    contentType: z.string(),
    data: z.string() // Base64 encoded
  })
}
 
export const handler: Handlers['UploadFile'] = async (req, { logger }) => {
  const { filename, data } = req.body
  
  // Decode Base64 and save
  const buffer = Buffer.from(data, 'base64')
  await fs.writeFile(`uploads/${filename}`, buffer)
  
  logger.info('File uploaded', { filename, size: buffer.length })
  
  return { 
    status: 200, 
    body: { message: 'File uploaded', filename } 
  }
}

Option 2: Direct Express Route (Large Files)

For large files, bypass Steps and create a direct Express route in your config:

motia.config.ts
import { defineConfig } from 'motia'
import multer from 'multer'
 
const upload = multer({ dest: 'uploads/' })
 
export default defineConfig({
  app: (app) => {
    // Direct Express route - not a Step
    app.post('/upload', upload.single('file'), (req, res) => {
      const file = req.file
      
      if (!file) {
        return res.status(400).json({ error: 'No file uploaded' })
      }
      
      res.json({ 
        message: 'File uploaded',
        filename: file.originalname,
        path: file.path
      })
    })
  }
})

Direct Express routes don't have access to Motia's emit, state, or streams. If you need these features after upload, emit an event from the Express route to trigger a Step.


Redis Configuration

Motia uses Redis for internal coordination. By default, it includes an embedded in-memory Redis server - no installation required.

Default: In-Memory Redis

motia.config.ts
import { defineConfig } from 'motia'
 
export default defineConfig({
  // Uses embedded Redis by default
})
 
// Or explicitly:
export default defineConfig({
  redis: {
    useMemoryServer: true
  }
})

External Redis

For production or when you have your own Redis instance:

motia.config.ts
import { defineConfig } from 'motia'
 
export default defineConfig({
  redis: {
    useMemoryServer: false,
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
    password: process.env.REDIS_PASSWORD,
    username: process.env.REDIS_USERNAME,
    db: parseInt(process.env.REDIS_DB || '0')
  }
})

Skip Embedded Redis

When creating a new project, skip the embedded Redis binary:

npx motia create my-app --skip-redis

This creates a project configured for external Redis from the start.

Redis Options

OptionTypeDescription
useMemoryServerbooleanUse embedded Redis (default: true)
hoststringRedis host
portnumberRedis port
passwordstringRedis password
usernamestringRedis username (Redis 6.0+)
dbnumberDatabase number (default: 0)

Stream Authentication

Secure your real-time streams by authenticating WebSocket connections.

Basic Setup

motia.config.ts
import { defineConfig, type StreamAuthRequest } from 'motia'
import { z } from 'zod'
 
// Define the shape of your auth context
const authContextSchema = z.object({
  userId: z.string(),
  role: z.enum(['admin', 'user']).optional()
})
 
export default defineConfig({
  streamAuth: {
    contextSchema: z.toJSONSchema(authContextSchema),
    authenticate: async (request: StreamAuthRequest) => {
      const token = extractToken(request)
      
      if (!token) {
        return null // No auth - connection rejected
      }
      
      const user = await validateToken(token)
      if (!user) {
        throw new Error('Invalid token')
      }
      
      return {
        userId: user.id,
        role: user.role
      }
    }
  }
})
 
function extractToken(request: StreamAuthRequest): string | undefined {
  // Check WebSocket protocol header
  const protocol = request.headers['sec-websocket-protocol'] as string | undefined
  if (protocol?.includes('Authorization')) {
    const [, token] = protocol.split(',')
    return token?.trim()
  }
  
  // Check query parameter
  if (request.url) {
    try {
      const url = new URL(request.url)
      return url.searchParams.get('authToken') ?? undefined
    } catch {
      return undefined
    }
  }
  
  return undefined
}

Using Auth in Streams

Once configured, use the auth context in your stream's canAccess callback:

steps/streams/notifications.stream.ts
import { StreamConfig } from 'motia'
import { z } from 'zod'
 
export const config: StreamConfig = {
  name: 'notifications',
  schema: z.object({
    message: z.string(),
    timestamp: z.string()
  }),
  baseConfig: { storageType: 'default' },
  canAccess: (subscription, authContext) => {
    // Only allow authenticated users
    if (!authContext) return false
    
    // Only allow users to access their own notifications
    return subscription.groupId === authContext.userId
  }
}

Built-in Features

Motia automatically configures several features you should know about:

Body Parsers

Motia automatically parses JSON, URL-encoded, and text request bodies with a 1GB limit:

// These are already configured:
app.use(bodyParser.json({ limit: '1gb' }))
app.use(bodyParser.urlencoded({ extended: true, limit: '1gb' }))
app.use(bodyParser.text({ limit: '1gb' }))

Raw Body Access

The raw request body is available as req.rawBody. Useful for webhook signature verification:

steps/webhook.step.ts
import { ApiRouteConfig, Handlers } from 'motia'
import crypto from 'crypto'
 
export const config: ApiRouteConfig = {
  type: 'api',
  name: 'StripeWebhook',
  path: '/webhooks/stripe',
  method: 'POST'
}
 
export const handler: Handlers['StripeWebhook'] = async (req, { logger }) => {
  const signature = req.headers['stripe-signature']
  const rawBody = req.rawBody
  
  // Verify webhook signature using raw body
  const isValid = verifyStripeSignature(rawBody, signature)
  
  if (!isValid) {
    return { status: 401, body: { error: 'Invalid signature' } }
  }
  
  // Process webhook...
  return { status: 200, body: { received: true } }
}

Plugins

Add Workbench UI components and custom steps with plugins.

Using Built-in Plugins

motia.config.ts
import { defineConfig } from 'motia'
import statesPlugin from '@motiadev/plugin-states/plugin'
import logsPlugin from '@motiadev/plugin-logs/plugin'
import observabilityPlugin from '@motiadev/plugin-observability/plugin'
 
export default defineConfig({
  plugins: [observabilityPlugin, statesPlugin, logsPlugin]
})

Creating Local Plugins

motia.config.ts
import path from 'node:path'
import { defineConfig, type MotiaPlugin, type MotiaPluginContext } from 'motia'
 
function myPlugin(motia: MotiaPluginContext): MotiaPlugin {
  // Register custom API endpoints
  motia.registerApi(
    { method: 'GET', path: '/__motia/my-plugin' },
    async (req, ctx) => {
      return { status: 200, body: { hello: 'world' } }
    }
  )
  
  return {
    workbench: [{
      componentName: 'MyComponent',
      packageName: '~/plugins/my-component',
      label: 'My Plugin',
      position: 'top'
    }],
    onShutdown: async () => {
      // Cleanup when server stops
    }
  }
}
 
export default defineConfig({
  plugins: [myPlugin]
})

👉 Learn more about Plugins →


What's Next?

Need help? See our Community Resources for questions, examples, and discussions.
Overriding the default configuration | motia