# Business rules — intersect_backend

Evidence date: 2026-10-12. Revision: `main` `86347d4` (2024-09-26).

Rules below are what the functions do. The repository has no business-rules document and no README. Rules marked **Inferred** are consequences of the code, not text that states a policy. Do not treat an inference as an approved rule.

`supabase/config.toml` is the Supabase CLI local template plus `project_id = "intersect_backend"`. Auth, signup, MFA, and provider values in that file are template defaults. They are not listed here as product rules.

Secret values are omitted.

## Who may call

| Rule | Evidence | Label |
|---|---|---|
| `sent_que` does not read the request method, headers, or body | `Deno.serve(async (req) =>` and `req` is unused | **Implemented.** Any request that reaches the function starts a drain |
| The function does not check a JWT, an API key, or a role | No `Authorization` read | **Implemented** in the function. Gateway `verify_jwt` is not set in `config.toml`. Whether the host requires a JWT is **Requires Confirmation** |
| `send-aws-ses-email` allows POST only | `req.method !== "POST"` returns 405 | **Implemented** |
| That function does not check credentials on the request | No header read | **Implemented.** **Requires Confirmation** that public invoke is intended |
| `hello-world` and `send-email` do not check method or auth | `req.json()` only | **Implemented** |
| The queue client uses the service-role env var | `SUPABASE_SERVICE_ROLE_KEY` | **Implemented** as the key the code asks for. A service role bypasses RLS when the key is real. Whether RLS exists is **Unknown** |

## Queue selection

**Implemented** in `supabase/functions/sent_que/index.ts`. The same select is in `index_old.ts`.

| Rule | Label |
|---|---|
| Only rows with `status` equal to 0 are selected | **Implemented.** Comment: "Assuming status is the field marking unsent emails" |
| Every matching row is loaded. There is no `limit` | **Implemented** |
| No `order` is applied | **Implemented** as the absence of an order. Which row is first is Supabase's default, which is **Unknown** from this repo |
| The table name is `send_que`. The function folder is `sent_que` | **Implemented.** Both spellings are in the tree. The query uses `send_que` |
| Columns the current file reads: `id`, `recipient`, `subject`, `body`, `attachment`. Columns it writes: `status`, `result` | **Implemented.** `select("*")` also returns any other column, and the code ignores it. Other columns are **Unknown** |
| A select error does not update any row | **Implemented.** The handler `return []` |

### Status values the current file uses

| Value | When the code writes it | Label |
|---|---|---|
| 0 | Never written by this repo. Selected as not yet sent | **Inferred** meaning: pending. The writer that sets 0 is not in this repo |
| 1 | `sendMail` resolved (the inner function returns `success: true`) | **Implemented.** The code does not check nodemailer `accepted` |
| 2 | `sendMail` threw | **Implemented** |

No other status values are written. Nothing in this repo moves a row back to 0.

**Inferred:** `status` 2 is terminal for this function, because the select only loads 0. **Requires Confirmation** that the system which inserts rows agrees.

`index_old.ts` uses the same 0 / 1 / 2 split, except a thrown send sets 2 inside the loop's `catch`, and a select error does not set 2 (the helper returns `[]` and the handler still reports success).

## Queue send rules (`index.ts`)

| Rule | Label |
|---|---|
| From address is the literal `no-reply@simpleflowhub.app`. The row cannot override it | **Implemented** |
| `to` is `email.recipient`. One `to` field. The string is not split | **Implemented.** Format is not validated |
| `subject` is `email.subject` | **Implemented** |
| Text body and HTML body are both `email.body`, unchanged | **Implemented** |
| SMTP host is `email-smtp.eu-west-1.amazonaws.com`, port `587`, `secure: false` | **Implemented.** The comment says true for 465 and false for other ports |
| SMTP username and password come from `SES_SMTP_USERNAME` and `SES_SMTP_PASSWORD` | **Implemented** in `index.ts` |
| The stored success `result` is the fixed string for `{ success: true, message: "Email sent successfully" }` | **Implemented.** `info.response` is only logged |
| The stored failure `result` is the fixed string for `{ success: false, message: "Failed to send email" }` | **Implemented.** The caught error is logged, not stored |
| An update error is logged. The loop continues | **Implemented** |
| The HTTP response after the loop is the body `OK` | **Implemented.** Header says JSON. The body is plain text |

## Attachments (`index.ts` only)

| Rule | Label |
|---|---|
| No `attachment`, or a falsy `attachment`, means no attachment | **Implemented** |
| A truthy value adds exactly one attachment | **Implemented.** The column is not parsed as a list |
| `filename` is the substring after the last `/` | **Implemented.** No handling of `?` query strings or backslashes |
| `path` is the stored string | **Implemented.** The comment allows a URL or a local path. This file does not download from Supabase Storage itself |
| `index_old.ts` never attaches a file | **Implemented** |

## Failure and retry

| Situation | `index.ts` | `index_old.ts` | Label |
|---|---|---|---|
| `sendMail` / `client.send` throws | `status` 2, store the fixed failure JSON | `status` 2, do not store `result` | **Implemented** |
| Send resolves | `status` 1, even if no recipient was accepted | `status` 1 after `client.send` resolves | **Implemented.** The current file does not inspect `accepted` |
| Select fails | Return `[]`. No row update | Return 200 and the success text. No row update | **Implemented.** The two files disagree |
| Update fails after a successful send | Log only. Status can stay 0 | Log only inside `updateEmailStatus`. Status can stay 0 | **Implemented** log. A later run can send again. **Inferred** duplicate |
| Second overlapping run | No lock and no in-progress status | Same | **Inferred** race |

Nothing in this repository retries a `status` 2 row.

## Direct send (`send-aws-ses-email`)

**Implemented** in `supabase/supabase/functions/send-aws-ses-email/index.ts`. This function does not read `send_que`.

| Rule | Label |
|---|---|
| Method other than POST returns 405 and does not send | **Implemented.** There is no `OPTIONS` branch |
| Body fields are `to`, `subject`, `htmlContent` | **Implemented.** Missing fields are not rejected |
| `to` is a single SES destination (`ToAddresses: [to]`). It is not split | **Implemented** |
| Sender is `no-reply@simpleflowhub.app`. The request cannot choose the sender | **Implemented** |
| The message is HTML only | **Implemented** |
| Subject charset is `UTF-8`. `Charset` is set on `Message.Subject` and also on `Message.Body`. It is not set on `Message.Body.Html` | **Implemented** shape. Whether SES treats the HTML part as UTF-8 is **Unknown** |
| Success returns 200 and a text body that includes the SES response JSON | **Implemented** |
| A thrown error returns 500 and includes `error.message` | **Implemented** |
| The recipient is written to the log before the send | **Implemented** |
| Credentials are env `AWS_SES_KEY_ID`, `AWS_SES_SECRET_KEY`, and `AWS_REGION` | **Implemented** as `Deno.env.get`. The `.env` file committed beside the function holds non-placeholder values for those names. The values are omitted. Region in both `.env` files is `eu-west-1` |

## Greeting functions

| Rule | Label |
|---|---|
| `hello-world` and `send-email` return `{ message: "Hello <name>!" }` from JSON `name` | **Implemented** |
| They do not send email | **Implemented.** `send-email/.env` is unused by `send-email/index.ts` |
| A non-JSON body throws, and there is no catch | **Implemented** |

## Secrets

| Rule | Label |
|---|---|
| `index.ts` does not embed the SMTP password | **Implemented.** It reads env vars |
| `index_old.ts` embeds an SMTP username and password | **Implemented.** The values are in git at HEAD. They are omitted here. This is not an approved place to keep them |
| Both nested `.env` files are tracked and contain non-placeholder AWS key material | **Implemented** as git content. `.gitignore` names `.env` and the files are tracked anyway |
| The two `.env` files do not hold the same secret | **Implemented.** Their secret strings differ. Values omitted |

**Requires Confirmation:** that those credentials have been revoked. Keeping them in the documentation would repeat the exposure, so they are not repeated.

## What this repository does not decide

1. Who inserts `send_que` rows.
2. Whether `status` 2 should be retried.
3. Whether a second worker (`intersect_lambda`, as documented in Silicon Ark) may update the same rows. That other repository's success rule is `accepted.length > 0`, and its batch size is 15. This function's success rule is "send did not throw", and it takes every pending row. If both run, they do not implement one rule.
4. Whether email confirmation, signup, or MFA settings in `config.toml` apply to a hosted project. They are local CLI defaults.
5. Whether `no-reply@simpleflowhub.app` is a verified SES identity. The code uses the string. Verification is not in git.
