Functions Deep Dive
A comprehensive guide to writing and deploying functions in the MolnOS Functions-as-a-Service platform.
Table of Contents
Section titled “Table of Contents”- Function Structure
- Bindings
- Triggers
- Request and Context Objects
- Response Formats
- Examples
- Deployment
- Authentication
- Best Practices
- Security Notes
- Troubleshooting
Function Structure
Section titled “Function Structure”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
handlerand exported (usingexport async function handlerorexport { handler }) - Be an
asyncfunction - Accept two parameters:
requestandcontext - 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
Section titled “Bindings”Bindings provide secure, controlled access to MolnOS services from your functions. Instead of managing API credentials, bindings automatically inject authenticated service clients.
Available Services
Section titled “Available Services”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
Binding Configuration
Section titled “Binding Configuration”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 } ] }];Permission Levels
Section titled “Permission Levels”Resource Types:
table- Database table operations
Actions:
read- Get, list operationswrite- Create, update operationsdelete- Delete operations
Targets:
- Specify exact resource names (e.g.,
['todos', 'users']) - Omit for access to all resources of that type
Using Bindings in Functions
Section titled “Using Bindings in Functions”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 } };}Databases Client API
Section titled “Databases Client API”The databases binding provides these methods:
// Get a single itemawait databases.get(tableName, key);
// Get all items in a tableawait databases.get(tableName);
// Write/update an itemawait databases.write(tableName, key, value);
// Write with expiration (TTL in seconds)await databases.write(tableName, key, value, expiration);
// Delete an itemawait databases.delete(tableName, key);
// List all tablesawait databases.listTables();
// Get table metadataawait databases.getTableInfo(tableName);
// Create a new tableawait databases.createTable(tableName);
// Delete a tableawait databases.deleteTable(tableName);Triggers
Section titled “Triggers”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.
Configuring Triggers
Section titled “Configuring Triggers”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" } ]}Event Handler Signature
Section titled “Event Handler Signature”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 };}Chain Depth Limit
Section titled “Chain Depth Limit”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.
Request and Context Objects
Section titled “Request and Context Objects”Request Object
Section titled “Request Object”{ 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)}Context Object
Section titled “Context Object”{ functionId: 'abc123', // Unique function ID functionName: 'my-func', // Function name bindings: { // Service clients databases: DatabasesClient, events: EventsClient, schemas: SchemasClient }, request: { ... } // Same as request parameter}Events Client API
Section titled “Events Client API”The events binding lets functions emit events to the in-process event bus:
// Emit a named event with dataawait 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.
Schemas Client API
Section titled “Schemas Client API”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 versionconst 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.
Response Formats
Section titled “Response Formats”Standard Lambda-style Response
Section titled “Standard Lambda-style Response”return { statusCode: 200, body: { message: 'Success' }, headers: { // Optional custom headers 'X-Custom-Header': 'value' }};Direct Object Response
Section titled “Direct Object Response”// Automatically converted to JSON with 200 statusreturn { message: 'Success' };String Response
Section titled “String Response”// Returned as text/plainreturn 'Hello, World!';Other Types
Section titled “Other Types”// Numbers and booleans wrapped in objectreturn 42;// Returns: { "result": 42 }Examples
Section titled “Examples”1. Simple Function Without Bindings
Section titled “1. Simple Function Without Bindings”async function handler(request, context) { const name = request.query.name || 'World';
return { statusCode: 200, body: { message: `Hello, ${name}!` } };}2. Counter with Database Binding
Section titled “2. Counter with Database Binding”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 } };}3. RESTful Todo API
Section titled “3. RESTful Todo API”This example shows how to handle different HTTP methods and subpaths to create a complete CRUD API in a single function.
4. Event Validation with Schemas
Section titled “4. Event Validation with Schemas”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 } };}Deployment
Section titled “Deployment”Using the HTTP API
Section titled “Using the HTTP API”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)”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 namecode(string, required) - Complete function code as a stringmethods(array, optional) - Allowed HTTP methods (default: all methods)triggers(array, recommended) - Trigger configurations (see Triggers). Each entry must have atypeof"http"or"event". Event triggers require aneventName. If omitted, the function defaults to being HTTP-accessiblebindings(array, optional) - Service bindings for accessing MolnOS servicespassAllHeaders(boolean, optional) - Pass all headers including internal system headers (default: false)allowUnauthenticated(boolean, optional) - Allow execution without authentication (default: false)
Deployment with Bindings
Section titled “Deployment with Bindings”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"] } ] } ] }'Deployment with Event Trigger
Section titled “Deployment with Event Trigger”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"] } ] } ] }'HTTP Method Restrictions
Section titled “HTTP Method Restrictions”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.
Updating Functions
Section titled “Updating Functions”Update an existing function using PUT /functions/{functionId}:
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"] }'Authentication
Section titled “Authentication”Authenticated Functions (Default)
Section titled “Authenticated Functions (Default)”By default, all functions require authentication. Users must provide a valid Bearer token:
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.
Unauthenticated Functions
Section titled “Unauthenticated Functions”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
Header Filtering
Section titled “Header Filtering”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}Best Practices
Section titled “Best Practices”-
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 } };} -
Logging - Use
console.logfor debugging (automatically sent to Observability service)console.log('Processing request for user:', userId);console.error('Failed to process:', error); -
Validation - Validate input before processing
if (!request.body || !request.body.title) {return { statusCode: 400, body: { error: 'Missing title' } };} -
Least Privilege - Only request binding permissions you need
// Good: Specific permissionspermissions: [{ resource: 'table', actions: ['read'], targets: ['users'] }]// Avoid: Overly broad permissionspermissions: [{ resource: 'table', actions: ['read', 'write', 'delete'] }] -
Subpaths - Use
request.subpathfor routing within a functionif (request.subpath === '/users') { /* ... */ }if (request.subpath.startsWith('/users/')) { /* ... */ }
Security Notes
Section titled “Security Notes”Runtime Security
Section titled “Runtime Security”- 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
Header Filtering
Section titled “Header Filtering”- 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: trueto disable filtering (use with caution)
Authentication & Authorization
Section titled “Authentication & Authorization”- 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.createpermission
Bindings Security
Section titled “Bindings Security”- 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
Troubleshooting
Section titled “Troubleshooting””Invalid input” or validation errors
Section titled “”Invalid input” or validation errors”- Check that
nameandcodeare provided and are strings - Verify
methodsarray contains valid HTTP methods - Ensure
bindingsstructure 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 handlerorexport { handler } - Check for syntax errors in your function code
”Method Not Allowed” (405)
Section titled “”Method Not Allowed” (405)”- The requested HTTP method is not in the function’s allowed
methodsarray - Check function metadata with GET
/functions/get/{functionId} - Update allowed methods with PATCH
/functions/update/{functionId}
”Unauthorized” (401)
Section titled “”Unauthorized” (401)”- Function requires authentication but no Bearer token was provided
- Token is invalid or expired
- Consider setting
allowUnauthenticated: trueif public access is intended
”Insufficient permissions” errors
Section titled “”Insufficient permissions” errors”- User deploying the function needs
functions.function.createpermission - For bindings, user must have permission to create bindings for the target service
- Check user’s permissions in the Identity service
”Function not found” (404)
Section titled “”Function not found” (404)”- 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
triggersconfiguration—if it only containseventtriggers without anhttptrigger, it cannot be called via HTTP - Use GET
/functions/listto see all deployed functions
Binding-related errors
Section titled “Binding-related errors”- 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