Express backend wrapped in serverless-http to run as an ARM64 Lambda container

dropthejase pulled the Express app into a shared `app.ts` module, added a `lambda.ts` entrypoint that wraps it with `serverless-http` and Lambda Powertools middleware, and built an ARM64 container image. Local dev still runs the Express server directly against Supabase and R2.

infrastructure

The refactor separates the Express app definition (app.ts, exported for both local and Lambda use) from the entrypoints - index.ts for local dev and lambda.ts for Lambda. The Lambda entrypoint calls loadSupabaseSecrets() on cold start, which fetches JSON from Secrets Manager and injects the values into process.env so existing createClient() calls in the codebase need no changes.

Auth middleware gains a dual path: in Lambda, it reads userId and userEmail from the API Gateway request context injected by the Lambda Token authorizer (no network call per request). In local dev, it falls back to supabase.auth.getUser(). The storage module similarly branches on AWS_LAMBDA_FUNCTION_NAME to pick S3 with IAM role credentials in Lambda or R2 with explicit credentials locally.

The Lambda handler is wrapped with three Powertools middleware layers: injectLambdaContext (logger), captureLambdaHandler (X-Ray tracer), and logMetrics with cold-start metric capture.

A later commit (c96d725) removes both fallback paths entirely, making auth and storage Lambda-only. This is the right production posture but means local dev needs alternative wiring if you want to test the Lambda code path locally.

This whole container era is superseded - the Lambda later moves to ZIP via NodejsFunction + esbuild (see post 222) and the code physically relocates to infra/lambda/. Read these commits as architecture reference, not as the current state.

So what Worth a look for the `app.ts` extraction pattern and the Secrets Manager cold-start loader if you are putting an Express app on Lambda. Skip the container-specific pieces - the fork itself abandoned them.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

12 commits from dropthejase/louis, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
c6ff8a18 chore(backend): add Lambda + Powertools + Bedrock deps Jason Lee 2026-05-06 ↗ GitHub
472e65ec chore(backend): ensure CommonJS output and add build:lambda script Jason Lee 2026-05-06 ↗ GitHub
e2da4284 refactor(backend): extract app.ts - shared Express app for local dev and Lambda Jason Lee 2026-05-06 ↗ GitHub
c3c58397 feat(backend): add lambda.ts - serverless-http + Powertools handler Jason Lee 2026-05-06 ↗ GitHub
6afb46e7 feat(backend): add secrets.ts - Secrets Manager cold-start loader for Supabase creds Jason Lee 2026-05-06 ↗ GitHub
f2278ac8 feat(backend): auth middleware - prod reads API Gateway context; dev falls back to Supabase getUser Jason Lee 2026-05-06 ↗ GitHub
ab6f4e58 feat(backend): storage.ts - S3 IAM role in Lambda, R2 fallback for local dev Jason Lee 2026-05-06 ↗ GitHub
4c11ec5e feat(backend): Dockerfile.lambda - arm64 Lambda container for Express app Jason Lee 2026-05-06 ↗ GitHub
ea417f3b feat(backend): Dockerfile.lambda - arm64 Lambda container for Express app Jason Lee 2026-05-06 ↗ GitHub
4536d02b docs(backend): add .env.lambda.example documenting Lambda env vars Jason Lee 2026-05-06 ↗ GitHub
eef3665b Merge feature/backend-migration: Express → Lambda (serverless-http, Bedrock, S3, auth) Jason Lee 2026-05-06 ↗ GitHub
c96d725d refactor(backend): auth middleware Lambda-only, drop Supabase fallback; storage S3-only, drop R2 fallback Jason Lee 2026-05-07 ↗ GitHub
commit body
Auth middleware now returns 401 immediately if the API Gateway authorizer
context is absent, removing the Supabase getUser() local dev path and
the @supabase/supabase-js import. Storage is simplified to a single
S3Client using the IAM role; removes isLambda() branching, R2 env vars,
and the storageEnabled export.

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-328.md from inside the repo you want the changes in.

⬇ Download capture-thread-328.md