Skip to content

Infrastructure

Environments

Environment Web API Purpose
Production Not yet built Not yet built Production service
dev https://survey-design-web.heineken.dev.4digit.ai https://survey-design-api.heineken.dev.4digit.ai Verification and integration tests. Shared by the team
Local http://localhost:3000 http://localhost:8787 Local development

dev is a shared environment

dev holds other members' real data. The integration tests create a new survey every run, so clear them out with bun run cleanup in integration-tests when they pile up. The dev API can be called with just the x-user-email and x-user-name headers.

Architecture

graph TD
  User[Survey designer]
  Ext[Chrome extension]
  Google[Google OAuth]
  AppRunner[AWS App Runner<br/>frontend Next.js]
  APILambda[AWS Lambda<br/>backend API]
  MigLambda[AWS Lambda<br/>migration]
  DB[(PostgreSQL)]
  ECR[Amazon ECR]
  GHA[GitHub Actions]
  CS[(Creative Survey)]

  User --> AppRunner
  Google -.-> AppRunner
  AppRunner --> APILambda
  Ext --> APILambda
  Ext --> CS
  APILambda --> DB
  MigLambda --> DB
  GHA --> ECR
  ECR --> AppRunner
  ECR --> APILambda
  ECR --> MigLambda

Services

Service Purpose
AWS App Runner Hosts the frontend (Next.js), started from a container image
AWS Lambda Hosts the backend API as a container image (based on public.ecr.aws/lambda/nodejs:22)
AWS Lambda (migration) Runs the Drizzle migrations, as a function separate from the API
Amazon ECR Registry for the three container images (web / backend / migration)
PostgreSQL The main database
GitHub Actions Builds, pushes to ECR, and deploys

The region is ap-northeast-1 (Tokyo).

Components and Repositories

Component Runtime ECR repository (dev) Lambda / service name (dev)
frontend (web) App Runner dev-heineken-survey-design-web dev-heineken-survey-design-web
backend API Lambda dev-heineken-survey-design-backend dev-heineken-survey-design-backend
Migration Lambda dev-heineken-survey-design-backend-migration dev-heineken-survey-design-backend-migration

Container Images

backend API (Dockerfile)

Stage Contents
build (oven/bun) Installs dependencies and bundles src/lambda.ts into CommonJS with bun build
runtime (public.ecr.aws/lambda/nodejs:22) Places the bundle, node_modules, drizzle/, and openapi.json, and starts lambda.handler

Only the postgres package is excluded from the bundle (--external postgres) and loaded from node_modules.

Migration (Dockerfile.migration)

Almost identical to the API image, but the entry point is migrate-handler.handler. Both postgres and drizzle-orm are excluded from the bundle.

frontend (Dockerfile)

A three-stage build (deps / build / release) based on oven/bun, running next start on port 3000.

NEXT_PUBLIC_API_URL is baked in as a build argument, so changing the target API requires rebuilding the image.

Deployment Flow

Branch Deploys to Timing
main (backend) The dev API Lambda Automatic (on push)
main (backend, when drizzle/** changes) The dev migration Lambda Automatic (on push) or manual
main (frontend) The dev App Runner service Automatic (on push)

backend: Dev Deploy

graph LR
  Push[Push to main] --> Checkout[Checkout]
  Checkout --> Creds[Configure AWS credentials]
  Creds --> Login[Log in to ECR]
  Login --> Build[docker build]
  Build --> PushImg[Push to ECR<br/>tags: commit SHA and latest]
  PushImg --> Update[aws lambda update-function-code]

backend: Dev Migration

Triggered by a push to main that touches drizzle/**, or manually (workflow_dispatch).

  1. Build the migration image and push it to ECR
  2. Check whether the migration Lambda exists (skip the rest if it does not)
  3. Update the Lambda's image and wait for the update to finish
  4. Invoke the Lambda and print the logs

Why migrations are separated

The API Lambda starts per request, so running migrations at startup would run them many times over. They live in an independent Lambda that is invoked explicitly exactly once.

frontend: Dev Deploy

  1. Build the image with NEXT_PUBLIC_API_URL as a build argument
  2. Push it to ECR (tags: commit SHA and latest)
  3. Start the App Runner deployment with aws apprunner start-deployment

Required GitHub Secrets

Secret Purpose
AWS_ACCESS_KEY_ID Operating ECR / Lambda / App Runner
AWS_SECRET_ACCESS_KEY Same as above

Environment Variables

backend

Name Purpose
DATABASE_URL PostgreSQL connection string. Certificate verification is disabled when it contains sslmode=no-verify
SURVEY_ADMIN_EMAILS Administrator email addresses (comma separated)
SURVEY_IMPORT_API_KEY API key for the import API. Key authentication is disabled when unset

frontend

Name Purpose
NEXTAUTH_URL Base URL for next-auth
NEXTAUTH_SECRET Session encryption key
GOOGLE_CLIENT_ID Google OAuth client ID
GOOGLE_CLIENT_SECRET Google OAuth client secret
NEXT_PUBLIC_API_URL Base URL of the backend API (baked in at build time)

Authentication and Access Paths

Path Authentication
User โ†’ frontend Google login via next-auth. /surveys/* and /library/* protected by middleware
frontend โ†’ backend User information in the x-user-email / x-user-name / x-user-image headers
Chrome extension โ†’ backend (import) API key authentication via x-api-key (compared against SURVEY_IMPORT_API_KEY)
Chrome extension โ†’ CS Uses the existing session of the CS editing screen

The backend allows CORS from any origin and accepts the Content-Type / x-user-email / x-user-name / x-user-image / x-api-key headers.

Known Issues

Confirmed problems that need addressing before a production rollout.

Issue Contents
Impersonation The backend trusts the x-user-email header, so forging it allows acting as any user
Permission bypass The snapshot direct-read fallback in importSection skips the permission check (survey-store.ts)
Fully open CORS origin: "*" allows every origin
Default users auto-created DEFAULT_ACTORS (@survey.local) are created automatically even in production
Missing Makefile No Makefile exists for the make commands in the READMEs