Extend your nself backend with serverless functions for custom business logic, webhooks, scheduled tasks, and API integrations.
nself provides multiple ways to add custom server-side logic through serverless functions, Hasura Actions, Event Triggers, and background workers. All options integrate seamlessly with your GraphQL API and database.
Extend your GraphQL API with custom business logic:
# Actions allow you to:
# - Add custom mutations and queries
# - Integrate with external APIs
# - Implement complex business logic
# - Handle authentication workflows
# - Process payments
# Example: Custom user registration action
mutation RegisterUser($input: RegisterUserInput!) {
registerUser(input: $input) {
id
email
success
message
}
}
React to database changes with automatic function execution:
# Triggers fire on:
# - INSERT operations
# - UPDATE operations
# - DELETE operations
# Example: Send welcome email on user signup
# Trigger: users table INSERT
# Function: send-welcome-email
# Payload includes:
# - Event type (INSERT/UPDATE/DELETE)
# - Old and new row data
# - Session variables
# - Headers
Run functions on a schedule (cron jobs):
# Examples:
# - Daily data cleanup: "0 2 * * *"
# - Hourly report generation: "0 * * * *"
# - Weekly backups: "0 0 * * 0"
# - Monthly billing: "0 0 1 * *"
# Cron expressions support:
# - Minutes (0-59)
# - Hours (0-23)
# - Day of month (1-31)
# - Month (1-12)
# - Day of week (0-7, 0 or 7 is Sunday)
Use TypeScript with the NestJS framework for structured, enterprise-ready functions:
# In .env.local:
NESTJS_ENABLED=true
NESTJS_PORT=3001
# Rebuild and start
nself build && nself up
// functions/nestjs/src/actions/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('actions')
export class UserController {
constructor(private userService: UserService) {}
@Post('register-user')
async registerUser(@Body() input: any) {
const { email, password, name } = input.input;
try {
// Custom business logic
const user = await this.userService.create({
email,
password,
name
});
// Send welcome email
await this.userService.sendWelcomeEmail(user);
return {
id: user.id,
email: user.email,
success: true,
message: 'User registered successfully'
};
} catch (error) {
return {
success: false,
message: error.message
};
}
}
}
Simple functions using plain Node.js:
// functions/node/register-user.js
const bcrypt = require('bcryptjs');
const { sendEmail } = require('./utils/email');
module.exports = async (req, res) => {
const { email, password, name } = req.body.input;
try {
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user into database
const query = `
INSERT INTO users (email, password_hash, name, email_verified)
VALUES ($1, $2, $3, false)
RETURNING id, email
`;
const result = await db.query(query, [email, hashedPassword, name]);
const user = result.rows[0];
// Send welcome email
await sendEmail({
to: email,
subject: 'Welcome to our platform!',
template: 'welcome',
data: { name }
});
res.json({
id: user.id,
email: user.email,
success: true,
message: 'User registered successfully'
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message
});
}
};
Use Python for data processing, ML, and AI workloads:
# functions/python/sentiment_analysis.py
from flask import Flask, request, jsonify
import pandas as pd
from textblob import TextBlob
app = Flask(__name__)
@app.route('/actions/analyze-sentiment', methods=['POST'])
def analyze_sentiment():
data = request.get_json()
text = data['input']['text']
# Perform sentiment analysis
blob = TextBlob(text)
sentiment = blob.sentiment
return jsonify({
'polarity': sentiment.polarity,
'subjectivity': sentiment.subjectivity,
'classification': 'positive' if sentiment.polarity > 0 else 'negative' if sentiment.polarity < 0 else 'neutral'
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3002)
High-performance functions with Go:
// functions/go/image-processor/main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"image"
"image/jpeg"
_ "image/png"
)
type ResizeRequest struct {
Input struct {
ImageURL string `json:"image_url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"input"`
}
func resizeImageHandler(w http.ResponseWriter, r *http.Request) {
var req ResizeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Download and resize image
resizedURL, err := processImage(req.Input.ImageURL, req.Input.Width, req.Input.Height)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"resized_url": resizedURL,
"success": true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/actions/resize-image", resizeImageHandler)
fmt.Println("Go function server starting on :3003")
http.ListenAndServe(":3003", nil)
}
Functions inherit environment variables from your nself configuration:
# Available to functions:
HASURA_GRAPHQL_ENDPOINT=http://hasura:8080/v1/graphql
HASURA_GRAPHQL_ADMIN_SECRET=your-admin-secret
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DATABASE=myproject
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-password
# Custom function variables:
SENDGRID_API_KEY=your-sendgrid-key
STRIPE_SECRET_KEY=your-stripe-key
JWT_SECRET=your-jwt-secret
Functions are accessible at predictable endpoints:
# NestJS functions
http://localhost:3001/actions/[action-name]
http://localhost:3001/webhooks/[webhook-name]
# Node.js functions
http://localhost:3000/functions/[function-name]
# Python functions
http://localhost:3002/actions/[action-name]
# Go functions
http://localhost:3003/actions/[action-name]
Connect functions to your GraphQL API:
# Action Definition:
type Mutation {
registerUser(input: RegisterUserInput!): RegisterUserOutput
}
# Input Type:
input RegisterUserInput {
email: String!
password: String!
name: String!
}
# Output Type:
type RegisterUserOutput {
id: UUID
email: String
success: Boolean!
message: String!
}
# Handler URL: http://nestjs:3001/actions/register-user
# Headers:
# - Content-Type: application/json
# - X-Hasura-User-Id: {{X-Hasura-User-Id}}
# - Authorization: Bearer {{SESSION_VARIABLES['x-hasura-auth-token']}}
# Example: User welcome email trigger
# Table: users
# Operations: INSERT
# Webhook URL: http://nestjs:3001/webhooks/user-created
# Headers:
# - Content-Type: application/json
# - X-Hasura-Admin-Secret: {{ADMIN_SECRET}}
Process long-running tasks asynchronously:
// Enable BullMQ in .env.local:
BULLMQ_ENABLED=true
REDIS_URL=redis://redis:6379
// Queue job from action:
const emailQueue = new Queue('email', { connection: redis });
await emailQueue.add('welcome-email', {
userId: user.id,
email: user.email,
name: user.name
});
// Process in worker:
const worker = new Worker('email', async job => {
const { userId, email, name } = job.data;
await sendWelcomeEmail(email, name);
}, { connection: redis });
# Create scheduled event in Hasura:
# Name: daily-cleanup
# Webhook: http://nestjs:3001/scheduled/cleanup
# Schedule: 0 2 * * * (2 AM daily)
# Payload: {"task": "cleanup_old_data"}
// Handler:
@Post('scheduled/cleanup')
async dailyCleanup(@Body() payload: any) {
// Delete old temporary files
await this.fileService.cleanupTempFiles();
// Archive old logs
await this.logService.archiveOldLogs();
// Update analytics
await this.analyticsService.updateDailyStats();
return { success: true };
}
# Test functions locally
curl -X POST http://localhost:3001/actions/register-user \
-H "Content-Type: application/json" \
-d '{
"input": {
"email": "test@example.com",
"password": "password123",
"name": "Test User"
}
}'
# Test with Hasura variables
curl -X POST http://localhost:3001/actions/get-user-profile \
-H "Content-Type: application/json" \
-H "X-Hasura-User-Id: 123" \
-d '{"input": {}}'
// NestJS testing
import { Test } from '@nestjs/testing';
import { UserController } from './user.controller';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [UserController],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should register user successfully', async () => {
const input = {
email: 'test@example.com',
password: 'password123',
name: 'Test User'
};
const result = await controller.registerUser({ input });
expect(result.success).toBe(true);
expect(result.email).toBe(input.email);
});
});
# Production .env settings
NODE_ENV=production
NESTJS_ENABLED=true
PYTHON_ENABLED=true
# Function-specific settings
MAX_REQUEST_SIZE=10mb
TIMEOUT=30s
MEMORY_LIMIT=512mb
# Security
CORS_ORIGINS=https://yourdomain.com
RATE_LIMIT_ENABLED=true
RATE_LIMIT_MAX=100
# Function monitoring
nself logs nestjs -f
nself logs python-worker -f
# Performance metrics
nself resources | grep functions
# Error tracking
nself logs --grep "error" --since "1h"