Serverless adapters for the universal runtime.
$ npm install @adobe/helix-universalHelix Universal is part of the Helix Deploy ecosystem. Helix Deploy is the parent project that provides deployment capabilities for universal functions across multiple serverless platforms.
Helix Deploy can deploy universal functions to:
- AWS Lambda - Traditional serverless functions
- Google Cloud Functions - Serverless functions on Google Cloud Platform
- Apache OpenWhisk - Open-source serverless platform
Helix Deploy Plugin Edge extends the reach of Helix Universal to edge compute runtimes, enabling deployment to:
- Fastly Compute@Edge - Edge computing platform
- Cloudflare Workers - Edge computing platform
This allows you to write universal functions that can run at the edge, closer to your users, while maintaining the same universal function interface.
When deploying with Helix Deploy, secrets follow a naming convention that includes the helix-deploy prefix:
- AWS Secrets Manager:
/helix-deploy/{package-name}/all - Google Secret Manager:
projects/{project-id}/secrets/helix-deploy--{package-name}/versions/latest
This convention ensures secrets are properly organized and accessible to deployed functions across all platforms.
This library provides adapters that allow you to write universal serverless functions that work across multiple platforms (AWS Lambda, Google Cloud Functions, and Apache OpenWhisk). Your function receives a standardized Request object and Context object, regardless of the underlying platform.
All universal functions follow this signature:
async function main(request, context) {
// Your function logic here
return new Response('Hello, World!', {
status: 200,
headers: {
'content-type': 'text/plain',
},
});
}Parameters:
request(Request): A standard Fetch API Request object containing the HTTP request detailscontext(UniversalContext): A context object providing runtime information, function metadata, and utilities
Returns: A Fetch API Response object
The context object provides a standardized interface to access platform-specific information and utilities. Additionally, plugins and wrappers may extend the context with additional properties (see Available Plugin Packages section below).
A Resolver instance for creating URLs to invoke other functions/actions. Supports version locking via the x-ow-version-lock header.
Methods:
createURL(options): Creates a URL for invoking another functionoptions.name(string, required): Function/action nameoptions.package(string, optional): Package nameoptions.version(string, optional): Version to invoke
Example:
// Invoke another function
const url = context.resolver.createURL({
package: 'helix-services',
name: 'content-proxy',
version: '1.2.3',
});
// url is a URL object pointing to the functionPath information about the current request.
Properties:
suffix(string): The request path suffix (e.g.,/foo/barfrom/api/function/foo/bar)
Information about the runtime environment.
Properties:
name(string): Runtime name ('aws-lambda','googlecloud-functions', or'apache-openwhisk')region(string): Deployment region (e.g.,'us-east-1','us-central1')accountId(string, AWS only): AWS account ID
Information about the current function.
Properties:
name(string): Function name (stemmed name without package/version)package(string): Package nameversion(string): Function versionapp(string): Application/namespace namefqn(string): Fully qualified name (platform-specific format)
Example values:
{
name: 'dispatch',
package: 'helix-services',
version: '4.3.1',
app: 'helix-pages',
fqn: 'arn:aws:lambda:us-east-1:118435662149:function:helix-services--dispatch:4_3_1'
}Information about the current invocation.
Properties:
id(string): Unique invocation ID (activation ID for OpenWhisk, request ID for AWS/Google)deadline(number): Unix timestamp (milliseconds) when the function will timeouttransactionId(string, optional): Transaction ID for tracing across multiple invocationsrequestId(string, optional): Request ID identifying the HTTP requestevent(object, AWS only): Raw AWS Lambda event object
Environment variables object. This includes:
- All
process.envvariables - Secrets loaded from platform-specific secret managers (when using secrets plugins)
- Function-specific environment variables
Note: The following environment variables are automatically set:
HELIX_UNIVERSAL_RUNTIME: Runtime nameHELIX_UNIVERSAL_NAME: Function nameHELIX_UNIVERSAL_PACKAGE: Package nameHELIX_UNIVERSAL_APP: Application nameHELIX_UNIVERSAL_VERSION: Function version
A logger instance compatible with helix-log. Provides the following methods:
log(...args)fatal(...args)error(...args)warn(...args)info(...args)debug(...args)verbose(...args)silly(...args)trace(...args)
Example:
context.log.info('Processing request', { url: request.url });
context.log.error('Something went wrong', error);Storage API for generating presigned URLs for cloud storage.
Methods:
presignURL(bucket, path, blobParams, method, expires): Generate a presigned URLbucket(string): Storage bucket namepath(string): Object path within the bucketblobParams(object, optional): Additional parameters (e.g.,ContentType,ContentDisposition)method(string, optional): HTTP method ('GET'or'PUT'), defaults to'GET'expires(number, optional): Expiration time in seconds, defaults to60
Example:
const url = await context.storage.presignURL(
'my-bucket',
'path/to/file.jpg',
{ ContentType: 'image/jpeg' },
'GET',
3600
);An object for storing user-defined attributes. This is particularly useful for passing data between wrappers and middleware functions. Wrappers can store computed values or initialized resources in context.attributes so they can be reused across the request lifecycle without re-initialization.
Common Use Cases:
- Caching initialized resources (e.g., storage clients, database connections)
- Passing data between middleware layers
- Storing request-scoped metadata
Example:
// In a wrapper function
function storageWrapper(fn) {
return async (request, context) => {
// Initialize storage once and cache it in attributes
if (!context.attributes.storage) {
context.attributes.storage = new HelixStorage({
// ... configuration
});
}
return fn(request, context);
};
}
// In your main function
export async function main(request, context) {
// Access the cached storage instance
const storage = context.attributes.storage;
const bucket = storage.contentBus();
// Use other attributes
context.attributes.userId = '12345';
context.attributes.requestStartTime = Date.now();
return new Response('OK');
}Note: The context.attributes object is heavily used by packages like @adobe/helix-shared-storage and helix-admin to cache resources and share data between middleware layers.
You can create custom adapters using the createAdapter function:
import { createAdapter } from '@adobe/helix-universal/aws';
const adapter = createAdapter({
factory: async () => {
// Custom factory function to load your main function
return (await import('./my-main.js')).main;
},
});
export const handler = adapter;Adapters support a plugin system for extending functionality:
Secrets plugins automatically load secrets from platform-specific secret managers and inject them into process.env as environment variables. Both plugins are included by default in their respective adapters, so secrets are automatically loaded on every invocation.
How Secrets Work:
- Secrets are stored as JSON objects in the platform's secret manager
- Each key-value pair in the JSON becomes an environment variable
- Only secrets that don't already exist in
process.envare set (existing variables take precedence) - Secrets are available in
context.envandprocess.envwithin your function
AWS Secrets Plugin
The AWS secrets plugin loads secrets from AWS Secrets Manager.
Secret Naming Convention:
- Secret ID format:
/helix-deploy/{package-name}/all - The package name is extracted from the Lambda function name (everything before
--) - Example: For function
helix-services--dispatch, secrets are loaded from/helix-deploy/helix-services/all
Configuration:
import { lambda, awsSecretsPlugin } from '@adobe/helix-universal';
// Already included in default lambda export, but you can customize:
const customLambda = lambda.raw.with(awsSecretsPlugin, {
expiration: 3600000, // Cache expiration time in milliseconds (default: 1 hour)
checkDelay: 60000, // Modification check delay in milliseconds (default: 1 minute)
emulateEnv: { // For testing: provide mock secrets instead of calling AWS
API_KEY: 'test-key',
DB_PASSWORD: 'test-password',
},
});Caching Behavior:
- Secrets are cached in memory for performance
- Cache expires after
expirationtime (default: 1 hour) - Every
checkDelay(default: 1 minute), the plugin checks if the secret was modified - If the secret was modified, it's reloaded automatically
- This balances performance with the ability to update secrets without redeploying
AWS Credentials: The plugin requires AWS credentials, which are typically provided via:
- IAM role attached to the Lambda function (recommended)
- Environment variables:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN - AWS region:
AWS_REGION(defaults to Lambda's region)
Error Handling:
ResourceNotFoundException: Returns empty object{}, allowing the function to continue without secretsThrottlingException: Throws an error withstatusCode: 429- Other errors: Throws an error, which will be caught by the adapter's error handler
Custom Endpoint (for local testing):
// Use localstack or other AWS-compatible endpoint
process.env.AWS_ENDPOINT_URL = 'http://localhost:4566';Example Secret Structure:
Store this JSON in AWS Secrets Manager at /helix-deploy/my-package/all:
{
"API_KEY": "your-api-key-here",
"DATABASE_URL": "postgresql://...",
"JWT_SECRET": "your-jwt-secret"
}Google Secrets Plugin
The Google secrets plugin loads secrets from Google Secret Manager.
Secret Naming Convention:
- Secret path format:
projects/{project-id}/secrets/helix-deploy--{package-name}/versions/latest - The project ID is extracted from the Cloud Function hostname subdomain
- The package name is extracted from
K_SERVICEenvironment variable (everything before--) - Dots in package names are replaced with underscores
- Example: For function
helix-services--dispatchin projecthelix-225321, secrets are loaded fromprojects/helix-225321/secrets/helix-deploy--helix_services/versions/latest
Configuration:
import { google, googleSecretsPlugin } from '@adobe/helix-universal';
// Already included in default google export, but you can customize:
const customGoogle = google.raw.with(googleSecretsPlugin, {
emulateEnv: { // For testing: provide mock secrets instead of calling Google
API_KEY: 'test-key',
DB_PASSWORD: 'test-password',
},
});Caching Behavior:
- No caching: Secrets are fetched on every invocation
- This ensures you always have the latest secrets, but may impact performance
Google Cloud Credentials: The plugin requires Google Cloud credentials, which are typically provided via:
- Service account attached to the Cloud Function (recommended)
GOOGLE_APPLICATION_CREDENTIALSenvironment variable pointing to a service account key file- Default credentials from the Cloud Function's runtime environment
Error Handling:
- Any error during secret retrieval: Returns empty object
{}, allowing the function to continue - Errors are logged to console but don't fail the invocation
Example Secret Structure:
Store this JSON in Google Secret Manager at projects/{project-id}/secrets/helix-deploy--my_package/versions/latest:
{
"API_KEY": "your-api-key-here",
"DATABASE_URL": "postgresql://...",
"JWT_SECRET": "your-jwt-secret"
}Using Secrets in Your Function:
export async function main(request, context) {
// Secrets are automatically available in context.env and process.env
const apiKey = context.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;
// Existing environment variables take precedence
// If API_KEY is already set in process.env, the secret won't override it
return new Response('OK');
}Disabling Secrets Plugin:
If you want to disable automatic secret loading:
// Use the raw adapter without secrets plugin
import { lambda } from '@adobe/helix-universal';
export const handler = lambda.raw; // No secrets pluginTesting with Secrets:
For testing, you can provide mock secrets using the emulateEnv option:
import { lambda, awsSecretsPlugin } from '@adobe/helix-universal';
const testLambda = lambda.raw.with(awsSecretsPlugin, {
emulateEnv: {
API_KEY: 'test-api-key',
TEST_MODE: 'true',
},
});
// Use testLambda in your testsYou can create custom plugins:
function myPlugin(adapter, options) {
return async (event, context) => {
// Pre-processing
context.attributes.customData = 'value';
// Call the adapter
const response = await adapter(event, context);
// Post-processing
response.headers.set('x-custom-header', 'value');
return response;
};
}
const customAdapter = lambda.raw.with(myPlugin, { option: 'value' });The Helix ecosystem provides several pre-built plugins that extend the universal runtime functionality. These plugins use the wrap utility to compose middleware around your functions.
Body Data Plugin (@adobe/helix-shared-body-data)
Parses request bodies (JSON, form data, URL-encoded) and makes the data available in context.data:
import wrap from '@adobe/helix-shared-wrap';
import bodyData from '@adobe/helix-shared-body-data';
export const main = wrap(async (request, context) => {
// Access parsed body data
const { name, email } = context.data;
return new Response(`Hello ${name}!`);
})
.with(bodyData);Bounce Plugin (@adobe/helix-shared-bounce)
Provides fast pro-forma responses from slow-running functions. The faster of two responses (a quick responder function or the slow main function) is returned:
import wrap from '@adobe/helix-shared-wrap';
import bounce from '@adobe/helix-shared-bounce';
async function fastResponder(req, context) {
return new Response(`Processing... Use ${context.invocation.bounceId} to track status.`);
}
export const main = wrap(async (request, context) => {
// Slow operation
await doSlowWork();
return new Response('Done');
})
.with(bounce, { responder: fastResponder });Secrets Plugin (@adobe/helix-shared-secrets)
Loads secrets from cloud provider secret managers (currently AWS Secrets Manager) and adds them to context.env and process.env:
import wrap from '@adobe/helix-shared-wrap';
import secrets from '@adobe/helix-shared-secrets';
export const main = wrap(async (request, context) => {
// Secrets are available in context.env
const apiKey = context.env.API_KEY;
return new Response('OK');
})
.with(secrets);Note: This is different from the built-in secrets plugins (awsSecretsPlugin and googleSecretsPlugin) that are automatically included in the adapters. The @adobe/helix-shared-secrets plugin provides additional customization options and can be used with custom name functions.
IMS Plugin (@adobe/helix-shared-ims)
Provides Adobe Identity Management System (IMS) authentication. Handles OAuth2 flow and makes user profile available in context.ims:
import wrap from '@adobe/helix-shared-wrap';
import ims from '@adobe/helix-shared-ims';
export const main = wrap(async (request, context) => {
if (context.ims.profile) {
// User is authenticated
const { name, email, userId } = context.ims.profile;
return new Response(`Hello ${name}!`);
}
return new Response('Not authenticated', { status: 401 });
})
.with(ims, {
clientId: 'my-client-id',
env: 'prod',
forceAuth: true, // Require authentication
});The IMS plugin adds the following to context.ims:
context.ims.config: Resolved IMS configurationcontext.ims.accessToken: Current access tokencontext.ims.profile: Authenticated user profile (name, email, userId)
Server Timing Plugin (@adobe/helix-shared-server-timing)
Adds performance monitoring by tracking execution time and adding Server-Timing HTTP headers:
import wrap from '@adobe/helix-shared-wrap';
import serverTiming from '@adobe/helix-shared-server-timing';
export const main = wrap(async (request, context) => {
const { timer } = context;
timer.update('fetch-data');
const data = await fetchData();
timer.update('process-data');
const result = processData(data);
timer.update('render');
const html = render(result);
return new Response(html);
})
.with(serverTiming);The plugin adds a timer object to context with an update(name) method to record execution milestones. Timing data is automatically added to the Server-Timing response header, viewable in browser DevTools.
Combining Multiple Plugins:
You can chain multiple plugins together:
import wrap from '@adobe/helix-shared-wrap';
import bodyData from '@adobe/helix-shared-body-data';
import ims from '@adobe/helix-shared-ims';
import serverTiming from '@adobe/helix-shared-server-timing';
export const main = wrap(async (request, context) => {
// context.data - parsed body data
// context.ims.profile - authenticated user
// context.timer - performance timer
return new Response('OK');
})
.with(serverTiming)
.with(ims, { clientId: 'my-client-id' })
.with(bodyData);Note: Execution order is reversed - the last plugin added executes first.
The resolver supports version locking, allowing you to pin specific versions of functions via the x-ow-version-lock header:
// Client sends header: x-ow-version-lock: content-proxy=1.2.3&dispatch=2.0.0
// In your function:
const url = context.resolver.createURL({
package: 'helix-services',
name: 'content-proxy',
version: '1.5.0', // This will be overridden to 1.2.3
});Here's a complete example of a universal function:
import { Response } from '@adobe/fetch';
export async function main(request, context) {
// Log request
context.log.info('Processing request', {
method: request.method,
url: request.url,
function: context.func.name,
version: context.func.version,
});
// Access environment variables
const apiKey = context.env.API_KEY;
// Invoke another function
const otherFunctionUrl = context.resolver.createURL({
package: 'helix-services',
name: 'content-proxy',
version: '1.2.3',
});
// Generate presigned URL
const presignedUrl = await context.storage.presignURL(
'my-bucket',
'path/to/file.jpg',
{},
'GET',
3600
);
// Process request
const body = await request.json();
// Return response
return new Response(JSON.stringify({
message: 'Success',
function: context.func.name,
runtime: context.runtime.name,
presignedUrl,
}), {
status: 200,
headers: {
'content-type': 'application/json',
},
});
}Type definitions are included. Import types as needed:
import type { UniversalContext, UniversalFunction } from '@adobe/helix-universal';
async function main(
request: Request,
context: UniversalContext
): Promise<Response> {
// TypeScript will provide full type checking
return new Response('OK');
}$ npm install$ npm test$ npm run lint