# Technical documentation — intersect_lambda

Evidence date: 2026-10-12. Revision: `main` `5f2193a` (2024-12-13, "updated code").

Source: `git@bitbucket.org:overdrive/intersect_lambda.git`. Remote has only `main`.

This document records the architecture that is in the repository. It is not a target architecture.

## Architecture

Two separate AWS SAM applications in one git repository. They are not nested stacks. Each has its own `template.yaml` and `samconfig.toml`.

| App | Path | Stack name | Description in the template | Account in SSM ARNs |
|---|---|---|---|---|
| Simpleflow | `Lambda/` | `Simpleflow` | `Simpleflow` / `SAM Template for Simpleflow` | `021891581915` |
| prod | `prod/` | `prod` | `prod` / `Sample SAM Template for prod` | `761018868594` |

Region is not in `samconfig.toml`. Handlers and ARNs use `eu-west-1`. **Inferred** region: `eu-west-1`. **Requires confirmation** against the deployed stacks.

```text
Lambda/template.yaml                         prod/template.yaml
  SendMailFunction                             (no API)
    POST  /sendmail
    OPTIONS /sendmail
  SendQueFunction                            SendQueFunction
    POST  /sendque                             Schedule rate(1 minute)
    OPTIONS /sendque
    Schedule rate(1 minute)
```

SAM creates one implicit API Gateway (`ServerlessRestApi`) for the Simpleflow stack because that template has `Api` events. The prod template has no `Api` event, so it does not create that API. Commented hello-world outputs in `prod/template.yaml` still mention `ServerlessRestApi`. They are comments.

Both queue functions share a shape: SSM, then Supabase, then SES SMTP. They are copied files, not a shared module. They diverged at `5f2193a`.

## Languages and frameworks

| Item | Evidence |
|---|---|
| JavaScript, ESM (`.mjs`) | `app.mjs` files. `package.json` `"main"` is `app.js`, which does not exist. Node treats `.mjs` as ESM, and the SAM handler is `app.lambdaHandler` |
| Node.js 20.x | `Runtime: nodejs20.x` in `Lambda/template.yaml` globals and on the prod function |
| `x86_64` | Both templates |
| AWS SAM, transform `AWS::Serverless-2016-10-31` | Both templates. Format version `2010-09-09` |
| `@aws-sdk/client-ses` `^3.649.0` | `Lambda/send-mail/package.json`. Imported by `send-mail/app.mjs` |
| `@supabase/supabase-js` `^2.0.0` | Both queue `package.json` files |
| `nodemailer` `^6.7.0` | Both queue handlers |
| `aws-sdk` `^2.1691.0` | Both queue handlers, used as `aws.SSM()` |
| `@aws-sdk/client-ssm` | Declared on the queue functions (`^3.658.1` on Simpleflow, `^3.693.0` on prod). Not imported by the live handlers |
| `@aws-sdk/client-ses` on the queue functions | Declared (`^3.649.0` Simpleflow, `^3.693.0` prod). Not imported by the live queue handlers |
| `axios` `>=0.21.1` | Declared on all three functions. Not imported |
| Mocha `^10.2.0` and Chai `^4.3.6` | devDependencies. No tests committed |
| SonarQube scan pipe `sonarsource/sonarqube-scan:2.0.1` | `bitbucket-pipelines.yml` |
| Pipeline image `maven:3.3.9` | The pipe's container image from the Bitbucket Sonar example. The application is not Java |

License field in each `package.json` is `MIT`. The send-mail package still says `"description": "hello world sample for NodeJS"` and points `repository` at the SAM cookiecutter. The queue packages say `"description": "scheduled email sender , 15 unsent every 60 sec"`, `"name": "end-que"`, `"author": "SAM CLI"`.

## Application structure

```text
bitbucket-pipelines.yml          SonarQube on main
sonar-project.properties         project key only
Lambda/
  template.yaml                  Simpleflow stack (the file SAM uses)
  samconfig.toml                 stack_name Simpleflow
  README.md                      SAM hello-world starter
  package-lock.json              name Lambda, empty packages, no package.json
  events/event.json              hello-world sample event
  send-mail/app.mjs              SendMailFunction
  send-mail/package.json
  send-que/app.mjs               SendQueFunction (deployed)
  send-que/app copy.mjs          unused draft, syntactically invalid
  send-que/package.json
prod/
  template.yaml                  prod stack (the file SAM uses)
  template copy.yaml             unused copy, CodeUri send-que/
  samconfig.toml                 stack_name prod
  README.md                      SAM hello-world starter, stack name edited to prod
  events/event.json              hello-world sample event
  sendque/app.mjs                SendQueFunction
  sendque/package.json
```

Each function directory has `.npmignore` containing `tests/*`. `.gitignore` files ignore `node_modules/`, `.aws-sam`, and `.env`. No `.env` or `.env.example` is committed. Runtime configuration for the queue is SSM, not env files.

Handler contract: SAM `Handler: app.lambdaHandler`. The live files export `lambdaHandler`. `app copy.mjs` exports `handler` and would not match.

## Lambda settings

| Setting | Simpleflow (`Lambda/template.yaml` globals) | Prod (`prod/template.yaml`) |
|---|---|---|
| Timeout | 30 | Not set. AWS Lambda's default is 3 seconds when the property is omitted. Whether the deployed function overrides that is **Unknown** |
| Memory | 1024 | Not set. Lambda's default is 128 MB when the property is omitted. Same caveat |
| Handler | `app.lambdaHandler` | `app.lambdaHandler` on the function |
| Runtime | `nodejs20.x` | `nodejs20.x` |
| Architecture | `x86_64` | `x86_64` |
| Logging | JSON | JSON |
| VPC | Commented. Security group `sg-0a538f29be2b53340`. Subnets `subnet-0c093a542188a6495` and `subnet-00f6da152554fe00e`. Comment: "lambda vpc security group ( needs to be created )" | No VPC block |
| Tracing, reserved concurrency, layers, environment variables | Not set | Not set |

`Lambda/samconfig.toml` and `prod/samconfig.toml` both set `capabilities = "CAPABILITY_IAM"`, `confirm_changeset = true`, `resolve_s3 = true`, cached parallel builds, lint on validate, and `warm_containers = "EAGER"` for local start. Neither file sets `region`, `s3_bucket`, `profile`, or `parameter_overrides`.

## HTTP API

Simpleflow only. Implicit API. No `Auth`, no CORS configuration on the API resource, no usage plan, no custom domain.

| Method and path | Function | Behaviour |
|---|---|---|
| `POST /sendmail` | `SendMailFunction` | WF-1 in the PRD |
| `OPTIONS /sendmail` | `SendMailFunction` | 200 CORS preflight, no send |
| `POST /sendque` | `SendQueFunction` | Runs the drain. Response has no CORS headers |
| `OPTIONS /sendque` | `SendQueFunction` | Also runs the drain. The handler does not test `httpMethod` |

Template output `SimpleflowApi`:

```text
https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/sendmail/
```

`Prod` here is the SAM default stage. It is not the `prod/` stack. The output does not include `/sendque`.

`events/event.json` is the SAM proxy sample (`httpMethod` POST, body `{"message":"hello world"}`). It is not a fixture for either real payload.

Direct-send success body is `JSON.stringify(\`Email sent successfully: ${JSON.stringify(response)}\`)`, so the HTTP body is a JSON string, not an object. Error bodies are the same shape.

## Schedule

Both `SendQueFunction` resources:

```yaml
Type: Schedule
Properties:
  Schedule: rate(1 minute)
  Input: '{}'
```

No `Enabled: false`, no retry policy, no dead-letter queue. The handler ignores the input.

## Supabase

Client: `createClient(SUPABASE_URL, SUPABASE_KEY)` inside the handler, so the client is built on every invocation, after SSM reads.

| Call | Code |
|---|---|
| Select | `.from('send_que').select('*').eq('status', 0).limit(15)` |
| Update | `.from('send_que').update({ status, result }).eq('id', email.id)` |

`result` on success is `JSON.stringify` of the nodemailer result. On a Simpleflow send exception it is `JSON.stringify(err.message)`, which is a JSON string of a string.

There is no schema migration in this repository. Column types, indexes, unique keys, and row-level security are **Unknown**.

### SSM parameter names

All paths are read with `WithDecryption: true`. IAM `Resource` entries are `arn:aws:ssm:eu-west-1:<account>:parameter<path-without-leading-slash>`.

| Purpose | Simpleflow name | Prod name |
|---|---|---|
| Supabase URL | `/intersectcloud-supabase/url` | `/intersectcloud-supabase/url` |
| Supabase service-role key | `/intersectcloud-supabase-deployment/JwtSecret/ServiceRoleKey` | `/Supabase-Simpleflow-Production/JwtSecret/ServiceRoleKey` |
| SES SMTP username | `/intersectcloud-ses/username` | `/intersectcloud-ses/username` |
| SES SMTP password | `/intersectcloud-ses/password` | `/intersectcloud-ses/password` |
| No-reply address | `/intersectcloud-ses/no-reply-email` | `/intersectcloud-ses/no-reply-email` |

The no-reply parameter was added to the Simpleflow IAM statement in `5f2193a`. Prod has had it since `ff6800d`.

Values are not in git. The unused draft `app copy.mjs` hardcodes `https://alb.simpleflowhub.app` and different parameter names (`SES_SMTP_USERNAME`, `SES_SMTP_PASSWORD`, and a service-role name without the leading slash). That file is not deployed.

## Amazon SES

| Path | Client | Identity |
|---|---|---|
| `SendMailFunction` | `@aws-sdk/client-ses` `SendEmailCommand`, client region `eu-west-1` | `Source: "no-reply@simpleflowhub.app"` |
| `SendQueFunction` | nodemailer SMTP `email-smtp.eu-west-1.amazonaws.com:587`, `secure: false` | `from` = SSM no-reply parameter |

`SendEmailCommand` on the direct path sets `Destination.ToAddresses` to a one-element array, `Message.Subject.Data` to `subject`, `Message.Subject.Charset` to `UTF-8`, and `Message.Body.Html.Data` to `htmlContent`. `Charset: "UTF-8"` is also set on `Message.Body` itself, which is not the SES field for the HTML part.

Queue mail options: `from`, `to`, `subject`, `text`, `html`, `attachments`. Status 1 is `result.accepted.length > 0`. The code does not inspect `rejected` beyond that.

IAM is the managed policy `AmazonSESFullAccess` on every function, plus the SSM statement on the queue functions. There is no `ses:SendEmail` / `ses:SendRawEmail` resource limit in the template.

## Auth

No API Gateway authorizer, no Lambda authorizer, no API key, and no JWT check in either handler.

The queue's Supabase key is a service-role JWT stored in SSM. **Inferred** from the parameter name `ServiceRoleKey`. The value is not in the repo.

Direct send logs the recipient. It does not log `htmlContent`. Queue logs the nodemailer result on success and the error message on failure. A 500 body from the queue includes `err.message`, which can include an SSM or Supabase error string.

## Storage

No bucket, disk volume, or database is created here. Attachments are whatever string is already in `send_que.attachment`. The live handler does not download to S3 or generate a presigned URL.

`app copy.mjs` calls `getSignedUrl` on `GetObjectCommand` for bucket `your-bucket-name`. The AWS SDK v3 `S3Client` does not implement `getSignedUrl` that way, and the file does not parse. It is not the live design.

## Environments and deployment

| Item | Evidence |
|---|---|
| Two stacks, two accounts, as written in the templates | Table at the top of this document |
| Deploy command configured | `sam deploy` via `samconfig.toml`. Changeset confirmation is on, so deploy stops for a human |
| CI | SonarQube only, on `main`. Image `maven:3.3.9`. Clone depth `full`. Variables `SONAR_HOST_URL`, `SONAR_TOKEN`. No `sam build` and no `sam deploy` |
| Sonar project key | `overdrive_intersect_lambda_bbe07fc8-a622-4cdd-8c0a-9c4d505fc60c` |
| Local invoke docs | Starter READMEs. They name `HelloWorldFunction`, which does not exist |

`prod/template copy.yaml` is a second template in that directory. Default `sam deploy` uses `template.yaml`. Deploying the copy would fail looking for `send-que/` under `prod/`, and it would request IAM for the old service-role parameter name.

Nothing in git records the last successful deploy, the API id, or a CloudFormation export.

The discovery scan named `sonarqube_scan.bat`. It is not in this commit and not in history.

## Security notes (observed, not a pentest)

- `/sendmail` and `/sendque` on the Simpleflow template are callable by anyone who can reach the API. `/sendmail` sends as `no-reply@simpleflowhub.app`. `/sendque` drains the queue.
- CORS on `/sendmail` is `*`.
- `AmazonSESFullAccess` is attached to both functions.
- The queue uses the Supabase service-role key.
- Failed prod sends stay at `status` 0 and are retried every minute with no cap.
- There is no row lock. Overlapping invocations can send the same row twice if the first invocation has not written `status` yet, or if the status update failed.
- Attachment `path` is taken from the row. If that string is a URL, nodemailer fetches it from inside the function.
- VPC is not enabled. The functions use the default Lambda network path. They can reach public SES and a public Supabase URL. A private Supabase URL would not be reachable with the current template.

## Development and testing

No test directory, no fixture for `to` / `subject` / `htmlContent`, and no fixture for a `send_que` row. `npm test` in each function runs `mocha tests/unit/`, and those paths are absent. `.npmignore` would exclude them from the deployment bundle if they were added beside the function.

`samconfig.toml` sets `lint = true` for `sam validate`. No `samconfig` test environment.

Starter READMEs tell the reader to install Node.js 20, Docker, and the SAM CLI, then `sam build` and `sam deploy --guided`. The guided path is how `samconfig.toml` gets written. Both config files exist, so that path was used at least far enough to save a config. They do not record a region.

## Monitoring

JSON logs to the function's CloudWatch log group (SAM `LoggingConfig`). No retention, metric filter, alarm, or subscription is in the template.

Application Insights was declared on the prod stack in `ff6800d` and commented out in `0b5cd27` the same day. It is not part of the current template.

The queue logs `SES Response:` plus the nodemailer result, and logs per-row update failures. Direct send logs the recipient and the string `sending...`.

## File map for the next reader

| Question | Read |
|---|---|
| What is deployed on Simpleflow? | `Lambda/template.yaml` |
| What is deployed on prod? | `prod/template.yaml` |
| How does immediate send work? | `Lambda/send-mail/app.mjs` |
| How does the queue work, and how do the stacks differ? | `Lambda/send-que/app.mjs` and `prod/sendque/app.mjs` |
| What is safe to ignore? | `Lambda/send-que/app copy.mjs`, `prod/template copy.yaml`, both `README.md` files, both `events/event.json` files |
| How does CI behave? | `bitbucket-pipelines.yml`, `sonar-project.properties` |
| What changed last? | `5f2193a` (2024-12-13): SSM no-reply on the Simpleflow queue, `status` 2 on send exceptions there, prod service-role parameter rename |
