Run on AWS Lambda

Run Understudy as one AWS Lambda behind a Function URL, with Amazon S3 for state. Develop locally, then deploy the same handler.

Understudy runs on AWS Lambda as a single function behind a Function URL. The function adapts each request onto Understudy and returns its response. A Lambda keeps nothing on local disk between calls, so the data lives in Amazon S3, and the same code runs on your machine and in the cloud.

Store data in S3

Understudy does not ship an S3 store, and it does not need to. A store is three methods over keys, so wrapping a bucket takes a few lines. Each key becomes an object.

interface Store {
  get(key: string): Promise<Uint8Array | undefined>;
  put(key: string, value: Uint8Array): Promise<void>;
  delete(key: string): Promise<void>;
}
// s3-store.mjs
import {
  S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand,
} from '@aws-sdk/client-s3';

export class S3Store {
  constructor(bucket, client = new S3Client({})) {
    this.bucket = bucket;
    this.client = client;
  }
  async get(key) {
    try {
      const out = await this.client.send(
        new GetObjectCommand({ Bucket: this.bucket, Key: key }));
      return new Uint8Array(await out.Body.transformToByteArray());
    } catch (err) {
      if (err.name === 'NoSuchKey') return undefined;
      throw err;
    }
  }
  async put(key, value) {
    await this.client.send(
      new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: value }));
  }
  async delete(key) {
    await this.client.send(
      new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
  }
}

The Lambda handler

Wire Understudy to the store, then adapt the Lambda event onto understudy.handle. This uses a Function URL, which delivers the version 2.0 event shape.

// index.mjs
import { createUnderstudy } from '@xtrable-ltd/understudy/host';
import { S3Store } from './s3-store.mjs';

const understudy = createUnderstudy({
  store: new S3Store(process.env.UNDERSTUDY_BUCKET),
});

export const handler = async (event) => {
  const bodyBytes = event.body
    ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
    : new Uint8Array();

  const result = await understudy.handle({
    method: event.requestContext.http.method,
    path: event.rawPath,
    query: new URLSearchParams(event.rawQueryString),
    headers: event.headers,
    body: async () => new Uint8Array(bodyBytes),
  });

  const isBinary = result.body instanceof Uint8Array;
  return {
    statusCode: result.status,
    headers: result.headers,
    body: isBinary ? Buffer.from(result.body).toString('base64') : (result.body ?? ''),
    isBase64Encoded: isBinary,
  };
};

Run it locally

The handler is just transport adapting, so you do not need AWS to develop. Run the same Understudy from a tiny node server, and point the store at a local folder while you build:

// dev.mjs
import { createServer } from 'node:http';
import { createUnderstudy } from '@xtrable-ltd/understudy/host';
import { FsStore } from '@xtrable-ltd/understudy/adapter-fs';

const understudy = createUnderstudy({ store: new FsStore('./understudy-data') });

createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost');
  const chunks = [];
  for await (const c of req) chunks.push(c);

  const result = await understudy.handle({
    method: req.method,
    path: url.pathname,
    query: url.searchParams,
    headers: req.headers,
    body: async () => new Uint8Array(Buffer.concat(chunks)),
  });

  res.writeHead(result.status, result.headers);
  res.end(result.body instanceof Uint8Array ? Buffer.from(result.body) : (result.body ?? ''));
}).listen(8080, () => console.log('http://localhost:8080'));

Prefer to run the real Lambda locally? AWS SAM (sam local start-api) and the Serverless framework's offline mode both invoke the handler above, against a local S3 such as LocalStack.

Deploy it

Package the handler with its dependencies, and create a Lambda with a Function URL:

  • Runtime Node.js 20, handler index.handler.
  • Set UNDERSTUDY_BUCKET to your bucket name.
  • Give the function's role s3:GetObject, s3:PutObject and s3:DeleteObject on that bucket.
  • Add a Function URL so the simulation has an endpoint. Understudy's own login guards the editor, so you can leave the URL open, or put it behind your gateway.

The same handler runs unchanged; only the store differs from local. Point your app's base URL at the Function URL, and you have a shared stand-in your whole team and your pipelines can use.