Skip to content

Functions Deep Dive

A comprehensive guide to writing and deploying functions in the MolnOS Functions-as-a-Service platform.

All MolnOS functions must follow this basic structure:

export async function handler(request, context) {
// Your function logic here
return {
statusCode: 200,
body: { message: 'Success' }
};
}

The function must:

  • Be named handler and exported (using export async function handler or export { handler })
  • Be an async function
  • Accept two parameters: request and context
  • Return a response object

Note: Functions are automatically wrapped in ES module format during deployment. Both named exports (export { handler }) and default exports (export default handler) are supported, including minified code.

Bindings provide secure, controlled access to MolnOS services from your functions. Instead of managing API credentials, bindings automatically inject authenticated service clients.

Currently supported bindings:

  • databases - Access to PikoDB (key-value database)
  • events - Emit events to the in-process event bus
  • schemas - Validate data against schemas in the Schema Registry

When deploying a function, specify which services it needs access to:

const bindings = [
{
service: 'databases', // Service name
permissions: [
{
resource: 'table', // Resource type
actions: ['read', 'write'], // Allowed actions
targets: ['todos'] // Specific tables/resources
}
]
}
];

Resource Types:

  • table - Database table operations

Actions:

  • read - Get, list operations
  • write - Create, update operations
  • delete - Delete operations

Targets:

  • Specify exact resource names (e.g., ['todos', 'users'])
  • Omit for access to all resources of that type

Access bound services via context.bindings:

async function handler(request, context) {
// Access the databases client
const { databases } = context.bindings;
// Use the client methods
const data = await databases.get('todos', 'item-123');
return {
statusCode: 200,
body: { data }
};
}

The databases binding provides these methods:

// Get a single item
await databases.get(tableName, key);
// Get all items in a table
await databases.get(tableName);
// Write/update an item
await databases.write(tableName, key, value);
// Write with expiration (TTL in seconds)
await databases.write(tableName, key, value, expiration);
// Delete an item
await databases.delete(tableName, key);
// List all tables
await databases.listTables();
// Get table metadata
await databases.getTableInfo(tableName);
// Create a new table
await databases.createTable(tableName);
// Delete a table
await databases.deleteTable(tableName);

Every function must declare how it can be invoked using the triggers array. There are two trigger types:

  • { "type": "http" } — Makes the function callable via HTTP requests.
  • { "type": "event", "eventName": "..." } — Subscribes the function to a named event from the in-process event bus.

If a function only has event triggers (no http trigger), it will not be accessible via HTTP and returns a 404 if called directly.

HTTP-only function:

{
"name": "my-api",
"code": "...",
"triggers": [{ "type": "http" }]
}

Event-only function (not HTTP-accessible):

{
"name": "order-processor",
"code": "...",
"triggers": [
{ "type": "event", "eventName": "order-created" }
],
"bindings": [
{ "service": "databases", "permissions": [{ "resource": "table", "actions": ["read", "write"], "targets": ["orders"] }] }
]
}

A function can have multiple triggers, including both HTTP and event triggers:

{
"triggers": [
{ "type": "http" },
{ "type": "event", "eventName": "order-created" },
{ "type": "event", "eventName": "order-updated" }
]
}

When triggered by an event, the function receives an event object (instead of a standard HTTP request) and a context object:

export async function handler(event, context) {
// event.eventName - The event that triggered this function
// event.data - The event payload
// event.timestamp - When the event was emitted
// event.id - Unique event ID
const { databases } = context.bindings;
await databases.write('orders', event.data.orderId, event.data);
return { processed: true };
}

To prevent infinite loops (e.g., function A emits an event that triggers function B, which emits an event that triggers function A), MolnOS enforces a maximum event chain depth of 10. Events exceeding this limit are dropped with a console error.

{
method: 'GET', // HTTP method
path: '/run/abc123/users', // Full path
subpath: '/users', // Path after /run/{functionId}
query: { page: '1' }, // Query parameters as object
headers: { // Request headers
'content-type': 'application/json',
'authorization': 'Bearer ...'
},
body: { ... } // Parsed JSON body (POST/PUT/PATCH)
}
{
functionId: 'abc123', // Unique function ID
functionName: 'my-func', // Function name
bindings: { // Service clients
databases: DatabasesClient,
events: EventsClient,
schemas: SchemasClient
},
request: { ... } // Same as request parameter
}

The events binding lets functions emit events to the in-process event bus:

// Emit a named event with data
await events.emit('order-created', { orderId: 'abc-123', amount: 99.99 });

Other functions with event triggers subscribed to order-created will execute automatically. Events can also be consumed by any other listener on the event bus.

The schemas binding provides validation against schemas in the Schema Registry:

// Validate data against a named schema (latest version)
const result = schemas.validate(data, 'order-created');
// result: { valid: true } or { valid: false, errors: [...] }
// Validate against a specific version
const result = schemas.validate(data, 'order-created', 2);

Validation is synchronous and in-process — no HTTP calls are made.

See the Schema Registry feature page for full details on creating and managing schemas.

return {
statusCode: 200,
body: { message: 'Success' },
headers: { // Optional custom headers
'X-Custom-Header': 'value'
}
};
// Automatically converted to JSON with 200 status
return { message: 'Success' };
// Returned as text/plain
return 'Hello, World!';
// Numbers and booleans wrapped in object
return 42;
// Returns: { "result": 42 }

See function-simple.js

async function handler(request, context) {
const name = request.query.name || 'World';
return {
statusCode: 200,
body: { message: `Hello, ${name}!` }
};
}

See function-with-bindings.js

async function handler(request, context) {
const { databases } = context.bindings;
if (request.method === 'POST') {
const current = await databases.get('counters', 'visitors');
const newCount = (current?.value || 0) + 1;
await databases.write('counters', 'visitors', newCount);
return {
statusCode: 200,
body: { count: newCount }
};
}
// GET request
const result = await databases.get('counters', 'visitors');
return {
statusCode: 200,
body: { count: result?.value || 0 }
};
}

See function-todo-api.js

This example shows how to handle different HTTP methods and subpaths to create a complete CRUD API in a single function.

async function handler(request, context) {
const { schemas, events } = context.bindings;
const data = request.body;
const check = schemas.validate(data, 'order-created');
if (!check.valid) {
return {
statusCode: 400,
body: { error: 'Invalid event data', details: check.errors }
};
}
await events.emit('order-created', data);
return {
statusCode: 200,
body: { success: true }
};
}

MolnOS Functions are deployed via HTTP POST requests to the /functions/deploy endpoint.

Basic Deployment (HTTP-triggered, No Bindings)

Section titled “Basic Deployment (HTTP-triggered, No Bindings)”
Terminal window
curl -X POST {MOLNOS_API_BASE}/functions/deploy \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-function",
"code": "export async function handler(request, context) { return { statusCode: 200, body: { message: \"Hello\" } }; }",
"triggers": [{ "type": "http" }],
"methods": ["GET", "POST"]
}'

Request Body Schema:

  • name (string, required) - Function name
  • code (string, required) - Complete function code as a string
  • methods (array, optional) - Allowed HTTP methods (default: all methods)
  • triggers (array, recommended) - Trigger configurations (see Triggers). Each entry must have a type of "http" or "event". Event triggers require an eventName. If omitted, the function defaults to being HTTP-accessible
  • bindings (array, optional) - Service bindings for accessing MolnOS services
  • passAllHeaders (boolean, optional) - Pass all headers including internal system headers (default: false)
  • allowUnauthenticated (boolean, optional) - Allow execution without authentication (default: false)
Terminal window
curl -X POST {MOLNOS_API_BASE}/functions/deploy \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "todo-api",
"code": "export async function handler(request, context) { const { databases } = context.bindings; const data = await databases.get(\"todos\"); return { statusCode: 200, body: { data } }; }",
"triggers": [{ "type": "http" }],
"methods": ["GET", "POST", "DELETE"],
"bindings": [
{
"service": "databases",
"permissions": [
{
"resource": "table",
"actions": ["read", "write"],
"targets": ["todos"]
}
]
}
]
}'
Terminal window
curl -X POST {MOLNOS_API_BASE}/functions/deploy \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "order-processor",
"code": "export async function handler(event, context) { const { databases } = context.bindings; await databases.write(\"orders\", event.data.orderId, event.data); return { processed: true }; }",
"triggers": [{ "type": "event", "eventName": "order-created" }],
"bindings": [
{
"service": "databases",
"permissions": [
{
"resource": "table",
"actions": ["read", "write"],
"targets": ["orders"]
}
]
}
]
}'

Control which HTTP methods can invoke your function:

{
"methods": ["GET", "POST"]
}

Valid methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS

If methods is omitted or empty, all HTTP methods are allowed.

Update an existing function using PUT /functions/{functionId}:

Terminal window
curl -X PUT {MOLNOS_API_BASE}/functions/abc123 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "export async function handler(request, context) { ... }",
"methods": ["GET", "POST"]
}'

By default, all functions require authentication. Users must provide a valid Bearer token:

Terminal window
curl {MOLNOS_API_BASE}/functions/run/abc123 \
-H "Authorization: Bearer YOUR_TOKEN"

The function receives the user’s token and can use it with bindings to access services on behalf of the user.

To allow public access without authentication, set allowUnauthenticated: true during deployment:

{
"name": "public-api",
"code": "export async function handler(request, context) { ... }",
"allowUnauthenticated": true
}

Use cases:

  • Public APIs
  • Webhooks from external services
  • Health check endpoints
  • Public documentation or status pages

Security considerations:

  • Unauthenticated functions cannot use bindings with user context
  • Rate limiting should be implemented separately
  • Input validation is critical

By default, internal MolnOS system headers (x-molnos-*) are filtered from request.headers to prevent functions from accessing internal service authentication. User authorization headers (Authorization, Cookie, etc.) are always passed through.

To disable filtering and pass all headers to your function:

{
"name": "my-function",
"code": "...",
"passAllHeaders": true
}
  1. Error Handling - Always wrap binding calls in try-catch

    try {
    const data = await databases.get('table', 'key');
    } catch (error) {
    console.error('Database error:', error);
    return { statusCode: 500, body: { error: error.message } };
    }
  2. Logging - Use console.log for debugging (automatically sent to Observability service)

    console.log('Processing request for user:', userId);
    console.error('Failed to process:', error);
  3. Validation - Validate input before processing

    if (!request.body || !request.body.title) {
    return { statusCode: 400, body: { error: 'Missing title' } };
    }
  4. Least Privilege - Only request binding permissions you need

    // Good: Specific permissions
    permissions: [{ resource: 'table', actions: ['read'], targets: ['users'] }]
    // Avoid: Overly broad permissions
    permissions: [{ resource: 'table', actions: ['read', 'write', 'delete'] }]
  5. Subpaths - Use request.subpath for routing within a function

    if (request.subpath === '/users') { /* ... */ }
    if (request.subpath.startsWith('/users/')) { /* ... */ }
  • Full Node.js Privileges: Functions run with complete Node.js access, including file system, network, and system commands. Only deploy trusted code.
  • Environment Variables: NOT exposed to functions by design to prevent credential leakage
  • Code Execution: All user code runs in the same Node.js process with full privileges
  • Internal Headers: System headers (x-molnos-*, x-molnos-token, x-molnos-service-token, x-molnos-internal) are filtered by default
  • User Headers: Authorization, Cookie, and other user headers are always passed through
  • Override: Set passAllHeaders: true to disable filtering (use with caution)
  • Default Behavior: Functions require authentication unless allowUnauthenticated: true
  • Service Accounts: Bindings use automatic service account tokens for inter-service communication
  • User Context: When authenticated, functions receive the user’s Bearer token in request.headers.authorization
  • Permissions: Deploying users must have functions.function.create permission
  • Least Privilege: Bindings implement fine-grained access control (service/resource/action/target levels)
  • Automatic Authentication: Service account tokens are automatically generated and managed
  • User Impersonation: When a user calls a function, bindings use their token to access services on their behalf
  • Permission Validation: Deployment fails if the user lacks permission to create requested bindings
  • Check that name and code are provided and are strings
  • Verify methods array contains valid HTTP methods
  • Ensure bindings structure is correct (service, permissions array)

“Function code must include an async handler function”

Section titled ““Function code must include an async handler function””
  • Ensure your function exports a function named handler
  • Use export async function handler or export { handler }
  • Check for syntax errors in your function code
  • The requested HTTP method is not in the function’s allowed methods array
  • Check function metadata with GET /functions/get/{functionId}
  • Update allowed methods with PATCH /functions/update/{functionId}
  • Function requires authentication but no Bearer token was provided
  • Token is invalid or expired
  • Consider setting allowUnauthenticated: true if public access is intended
  • User deploying the function needs functions.function.create permission
  • For bindings, user must have permission to create bindings for the target service
  • Check user’s permissions in the Identity service
  • Function ID is incorrect or function has been deleted
  • The function may only have event triggers and is not HTTP-accessible. Check the function’s triggers configuration—if it only contains event triggers without an http trigger, it cannot be called via HTTP
  • Use GET /functions/list to see all deployed functions
  • Ensure the target service (databases, storage, etc.) is running and accessible
  • Verify binding permissions match the operations your function performs
  • Check service account token generation is working
  • Review binding structure for correct service names and permission format