# PRD — edge_functions

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/edge_functions.git`, `main` at `7449104` (2024-05-02).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `readme.txt`, `supabase/functions/*/index.ts`, `supabase/config.toml`, `package.json`, and the Sonar pipeline files. Provenance is labelled.

The function comments include a local demo bearer token from the Supabase function scaffold. That token is not copied here.

`readme.txt` is a setup note. Where it disagrees with a handler, the handler is the implemented behaviour and the readme is called out.

## Product purpose

**Documented** in `readme.txt`: stand up the Supabase CLI on Windows, create an edge function whose directory name is the HTTP endpoint, serve it locally, and deploy `hello` to Supabase project ref `natpyffafbqetnbeoctm`.

**Implemented** beyond that note: three HTTP functions.

- `hello` greets using one row from `sites`.
- `callsheet` downloads a fixed Excel object and logs workbook sheet names. The response body stays empty.
- `upload_sj` aims to copy a second fixed Excel object into table `report_sj`.

**Inferred** from `project_id`, the lockfile name, and the workbook filenames: this was started as a Datafy-related Supabase function project that reads call-cycle and regional channel-report spreadsheets. The code never says "Datafy", "call cycle", or "channel report" in prose. Those words appear only as an id, a path, and a filename.

**Unknown / Requires Confirmation:** the product owner, the intended caller, what `sj` stands for, and whether any function is still in use.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| HTTP caller | `Deno.serve` in each function. No auth check inside the handler | **Implemented**. The caller application is not in this repo |
| Person setting up a workstation | `readme.txt` steps 1–3 (Deno install, `supabase init`, `functions new`, `start`, `serve`, `login`, `functions deploy`) | **Documented** |
| Person running CI | `bitbucket-pipelines.yml` on branch `main` | **Implemented** as pipeline config. Whether Sonar still runs is **Unknown** |
| Database and Storage | Handlers call PostgREST and Storage with an anon key | **Implemented** as client calls. Policies that allow or deny them are not in this repo |

No user role, tenant, or permission model exists in the handlers.

## Systems the product talks to

1. Supabase Edge Functions. Local base URL in the readme and in each function comment: `http://127.0.0.1:54321/functions/v1/<name>`. Hosted URL **Documented** only for hello: `https://natpyffafbqetnbeoctm.supabase.co/functions/v1/hello`. This inspection did not call it.
2. Postgres via `@supabase/supabase-js`, using `SUPABASE_URL` and `SUPABASE_ANON_KEY`. Tables named in code: `sites`, `report_sj`. **Implemented** as queries. Table definitions are **Unknown**.
3. Supabase Storage. `callsheet` uses the same client and bucket `uploads`. `upload_sj` reads `SUPABASE_STORAGE_URL` and `SUPABASE_ANON_STORAGE_KEY` and then calls `from('uploads/callsheet')`. **Implemented** as those calls.
4. Deno module hosts: `npm:@supabase/supabase-js`, `https://deno.land/x/flat/mod.ts`, `npm:@supabase/storage-js`, `npm:std/encoding/base64`, `npm:xlsx/mod`. **Implemented** as import lines. They were not resolved in this inspection.
5. SonarQube. **Implemented** as a Pipelines pipe. Host and token come from `SONAR_HOST_URL` and `SONAR_TOKEN`. Values are not in the repo.
6. Supabase CLI `1.151.1` for local start, serve, login, deploy, and secrets. **Documented** in `readme.txt` and **Implemented** as the locked devDependency.

No queue, mailer, cache, or monitoring vendor is configured in the functions. Local `config.toml` enables Realtime, Inbucket, Analytics, and Studio because that file is the `supabase init` template. The functions do not use them.

A Datafy AWS Supabase host is documented in Silicon Ark `terraform_scripts` (`supabase.datafy.co.za`). It is not named in this repository. Relationship: **Unknown / Requires Confirmation**.

## Major workflows

Routes are `POST` or any method. The handlers do not read `req.method`. Supabase's default function URL is `/functions/v1/<directory>`.

### WF-1 — Greet with one site name

**Implemented** in `supabase/functions/hello/index.ts`.

Trigger: HTTP request to `/functions/v1/hello`. The scaffold comment shows `POST` with body `{"name":"Functions"}`. The handler does not read the body, the query string, or headers.

Inputs: environment `SUPABASE_URL`, `SUPABASE_ANON_KEY`. Non-null assertions (`!`). Missing values are not handled in the function.

Steps:

1. At cold start, log `Hello from Functions!` and create a Supabase client.
2. On each request, `from('sites').select('name').limit(1).single()`.
3. If `error` is set, `console.log(error)`. The handler does not return early.
4. Build `{ message: "Hello ${name?.name}!" }`. `name` here is the row object (the query result was renamed to `name` in the destructure). The greeting uses the column `name` on that row.
5. Return HTTP 200, `Content-Type: application/json`, that object.

Outputs on the path that reaches `Response`: always that JSON. There is no other status in this file.

Validation: none. The request body in the comment is ignored.

Error conditions:

- Query error, or no row: `.single()` yields an error, the code logs it, and the message becomes `Hello undefined!` because of `name?.name`. Still HTTP 200. **Implemented**.
- Thrown client or network errors are not caught. This repository does not define the HTTP status the gateway returns for an unhandled exception.

JWT: `readme.txt` deploys this function with `--no-verify-jwt` and serves locally with the same flag. **Documented**. The handler itself does not check a user.

Provenance: **Implemented** since `7449104`. The comment's request body is the CLI scaffold and does not match the handler.

### WF-2 — Read call-cycle workbook sheet names

**Implemented** in `supabase/functions/callsheet/index.ts`.

Trigger: HTTP request to `/functions/v1/callsheet`. The comment shows the same scaffold `POST` and body as `hello`. The handler ignores the request.

Inputs: `SUPABASE_URL`, `SUPABASE_ANON_KEY`. Constants `bucketName = 'uploads'` and `xlsFilePath = 'callsheet/CMR_CALL_CYCLE_2023.xlsx'`.

Steps that run:

1. Create a client at cold start. Prepare `payload = { message: {}, result: {}, sent: {} }`.
2. `storage.from('uploads').download('callsheet/CMR_CALL_CYCLE_2023.xlsx')`.
3. `xlsx.read(data)` from `https://deno.land/x/flat/mod.ts`. `error` from the download is not checked.
4. `console.log` the workbook's `SheetNames`.
5. Return `payload` unchanged, HTTP 200, JSON.

Commented and not run: per-sheet CSV conversion, a download of `FState Region ALL CHANNEL REPORTS 10 MARCH 2024.xlsx` from `uploads/callsheet`, and a hello-style body. `readXLSX` is imported and not called.

Outputs: `{ "message": {}, "result": {}, "sent": {} }`. Sheet names are only on the function log.

Validation: none.

Error conditions: a failed download leaves `data` unset and `xlsx.read` is still called. There is no `try/catch`. Gateway status for that throw is not defined here.

Deploy: no deploy command for `callsheet` is written in `readme.txt`. `config.toml` has no `[functions.callsheet]` block and no `verify_jwt` key. The gateway JWT behaviour for a deploy that omits `--no-verify-jwt` is **Unknown / Requires Confirmation**.

### WF-3 — Load the Free State channel report into `report_sj`

**Implemented** as source in `supabase/functions/upload_sj/index.ts`. The file does not complete a successful HTTP response. Treat the steps below as what the source says, not as a working upload.

Trigger: HTTP request to `/functions/v1/upload_sj`. The request is not read. A commented line would have taken the path from `req.url`.

Inputs the source reads:

- `SUPABASE_URL`, `SUPABASE_ANON_KEY` for the data client.
- `SUPABASE_STORAGE_URL`, `SUPABASE_ANON_STORAGE_KEY` for a second `createClient` from `@supabase/supabase-js`.

Steps written in the file:

1. `createStorageClient(supabaseStorage)`, where `createStorageClient` is `createClient` imported from `npm:@supabase/storage-js`, and `supabaseStorage` is the second Supabase client object. **Inferred:** storage-js `createClient` expects a URL and headers, not a Supabase client. This inspection did not execute the import.
2. `storage.from('uploads/callsheet').download("FState Region ALL CHANNEL REPORTS 10 MARCH 2024.xlsx")`. The string passed to `from` includes a slash. `callsheet` uses bucket `uploads` and a path under `callsheet/`. These are not the same call shape.
3. On `downloadError`, `throw downloadError`. No JSON body is built for that path.
4. `read(file)` from `npm:xlsx/mod`, take `SheetNames[0]`, then `read(sheet, { header: 1 })`. **Inferred:** SheetJS `read` loads a workbook from bytes. The header option belongs to sheet-to-json helpers, which this file does not import. Not executed here.
5. `from('report_sj').upsert(parsedData)`.
6. Both the `if (error)` branch and the `else` branch assign a new object to `data`. `data` was declared with `const` in the destructure. Reaching either branch throws a TypeError before `return new Response`. The `if` branch also `throw error` after that assignment.

`decode` from `npm:std/encoding/base64` is imported and not used.

Outputs the author wrote, and that the `const` assignment prevents:

- Success object: `{ message: 'Uploaded successfully', status: 200 }`. The `status` field is JSON, not the HTTP status. `new Response` is called without a status option, so the HTTP status would be 200 if the return were reached.
- Failure object: `{ message: 'Uploaded unsuccessfully', status: 500 }`, then `throw error`, so that object is also not returned.

Validation: none. The workbook name, bucket, and table are constants. Upsert is called with no `onConflict` list. The primary key or unique columns of `report_sj` are **Unknown**.

Deploy: not documented. Same JWT gap as WF-2.

### WF-4 — Local serve and deploy hello

**Documented** in `readme.txt`. Not re-run in this inspection.

1. Install Deno on Windows with `irm https://deno.land/install.ps1 | iex`.
2. Follow the Supabase functions quickstart and a YouTube link recorded in the readme (`https://www.youtube.com/watch?v=5OWH9c4u68M`). What that video contains was not reviewed.
3. `npx supabase init`, then `npx supabase functions new hello`.
4. `npx supabase start`, then `npx supabase functions serve --no-verify-jwt`. Local URL `http://127.0.0.1:54321/functions/v1/hello`.
5. `npx supabase login`, then `npx supabase functions deploy hello --no-verify-jwt`.
6. Example, under "General": `supabase secrets set MY_NAME=Chewbacca`. No function reads `MY_NAME`.

`config.toml` `project_id` is `datafy_edge_fuctions`. Local API port `54321` matches the readme. DB port `54322`, Studio `54323`, Inbucket `54324`, shadow DB `54320`, pooler `54329` (disabled), analytics `54327`. Postgres major version `15`. Storage `file_size_limit` is `50MiB`. These match a `supabase init` template of that CLI generation, with the project id set. They are **Implemented** as local config. They are not evidence that a hosted project was created with the same ports.

## Functional requirements

### FR-001 — Three HTTP function names

Requirement: the system exposes functions `hello`, `callsheet`, and `upload_sj`, each as `supabase/functions/<name>/index.ts`.

Who: an HTTP caller. Trigger: a request to that function URL.

Inputs: see WF-1, WF-2, WF-3. The handlers do not take caller-supplied file names or site names.

Expected behaviour: each file calls `Deno.serve` and returns `application/json` when the handler reaches `return new Response`.

Outputs: WF-1 returns a greeting. WF-2 returns the empty payload. WF-3 does not reach `return` on the branches that are written.

Validation: none in the handlers.

Error conditions: unhandled exceptions in all three; WF-3 also throws on download error and would throw on `const` reassignment.

Dependencies: Deno, Supabase Edge Functions runtime, the env vars named in each file.

Implementation: **Implemented** as source since `7449104`.

Confidence: high that the source does this. Low that WF-3 has ever returned JSON in a deployed project. No deploy log is in git.

### FR-002 — `hello` reads `sites.name`

Requirement: `hello` selects `name` from `sites`, one row, and interpolates it into `Hello <name>!`.

Who: HTTP caller. Trigger: any request the gateway forwards. The JSON body is not an input.

Expected behaviour: WF-1.

Error conditions: query failure still returns HTTP 200 and `Hello undefined!`.

Dependencies: table `sites` with a `name` column, readable by the anon key. Schema and RLS are not in this repo.

Provenance: **Implemented** behaviour. The requirement that a `sites` row exists is **Inferred** from the query. It is not written as a rule.

Confidence: high for the query text. The table's existence outside this repo is **Unknown / Requires Confirmation**.

### FR-003 — `callsheet` opens a fixed workbook

Requirement: download `uploads` / `callsheet/CMR_CALL_CYCLE_2023.xlsx` and read it as a workbook.

Who: HTTP caller. The caller cannot choose another object. The path is a constant.

Expected behaviour that runs: log `SheetNames`, return `{ message: {}, result: {}, sent: {} }`.

The commented loop that would fill `result` with CSV per sheet is not a requirement. It is inactive source.

Error conditions: download errors are ignored; `xlsx.read` then runs on whatever `data` is.

Dependencies: bucket `uploads`, that object, and Storage access for the anon key. **Unknown** whether they exist.

Implementation: **Implemented**.

Confidence: high for the code path. The business meaning of the workbook is **Unknown / Requires Confirmation**.

### FR-004 — `upload_sj` targets `report_sj`

Requirement recovered from the source, not confirmed as a working feature: download `FState Region ALL CHANNEL REPORTS 10 MARCH 2024.xlsx` from `from('uploads/callsheet')`, parse the first sheet, and `upsert` into `report_sj`.

Who: HTTP caller. Trigger: a request. No body fields are read.

Expected behaviour the author coded: JSON `Uploaded successfully` or `Uploaded unsuccessfully`. **Implemented** source does not return either, because of the `const` assignment and the `throw`.

Validation: none.

Error conditions: download error is thrown; upsert error is thrown; both status objects are dead.

Dependencies: `SUPABASE_STORAGE_URL`, `SUPABASE_ANON_STORAGE_KEY`, bucket id `uploads/callsheet` as written, table `report_sj`. All **Unknown** outside this file.

Provenance: **Implemented** source, not demonstrated behaviour. Do not treat a successful upload as a confirmed requirement.

Confidence: high that the file cannot return on those branches. Whether a later uncommitted edit fixed it is **Unknown**. Git has no later commit.

### FR-005 — Local function serving without JWT verification

Requirement: `npx supabase functions serve --no-verify-jwt` is the documented local serve command.

Who: a developer. Trigger: manual CLI.

Expected behaviour: **Documented** URL `http://127.0.0.1:54321/functions/v1/hello`. The same serve process also mounts `callsheet` and `upload_sj`, because both directories exist and the command is not limited to one function. That mounting behaviour is **Inferred** from the CLI command plus the directory layout. It was not re-run here.

`config.toml` does not set `verify_jwt`. The flag is only on the command line in the readme.

### FR-006 — Deploy `hello` without JWT verification

Requirement: **Documented** command `npx supabase functions deploy hello --no-verify-jwt` after `npx supabase login`.

Output named in the readme: `https://natpyffafbqetnbeoctm.supabase.co/functions/v1/hello`. The readme says `natpyffafbqetnbeoctm` is the project reference.

Whether that deploy was done, and whether the function is still published, was not verified. **Unknown / Requires Confirmation**.

No matching deploy line exists for `callsheet` or `upload_sj`.

### FR-007 — SonarQube on main

Requirement: pushes to `main` run SonarQube scan pipe `sonarsource/sonarqube-scan:2.0.1` in image `maven:3.3.9`.

Who: Bitbucket Pipelines. Trigger: branch `main`. Clone depth is `full`.

Inputs: `SONAR_HOST_URL`, `SONAR_TOKEN` from repository or workspace variables. `sonar.projectKey=overdrive_edge_functions_d17c9661-3898-4839-85d2-04c487089cf1`. No `sonar.sources`, exclusions, or test paths.

Expected behaviour: the pipe runs. The pipeline file does not build Deno, does not start Supabase, and does not deploy.

Provenance: **Implemented** as YAML since `2f97818` / `323c389`. The Maven image for a Deno repo is what the file says. It is not explained.

## Constraints

- Handlers use the anon key, not `SUPABASE_SERVICE_ROLE_KEY`. Access depends on RLS and Storage policies that this repo does not contain.
- File paths and the site query are fixed. Callers cannot pass a different workbook or a site id.
- `hello`'s documented deploy turns gateway JWT verification off. The other two functions have no recorded deploy flag.
- Local Postgres in `config.toml` is major version 15. The hosted version is **Unknown**.
- Local API `max_rows` is 1000. `hello` already limits to 1. `upload_sj` upserts whatever `parsedData` is, with no row cap in the function.
- No automated test constrains the handlers.
- Secrets in the readme example (`MY_NAME`) and the scaffold bearer tokens are not product configuration. Do not rotate or document them as live credentials. The project ref in the readme is an identifier, not a key.

## Requirements that are assumptions

| Statement | Why it is not a confirmed requirement |
|---|---|
| The product is part of Datafy production on `supabase.datafy.co.za` | Only the strings `datafy_edge_fuctions` and the Terraform docs overlap |
| `callsheet` should return parsed rows | That loop is commented out. The live return value is empty |
| `upload_sj` successfully loads reports | The return path is unreachable as written |
| Callers must send `{"name":"Functions"}` | Scaffold comment only. Handlers ignore the body |
| `MY_NAME` is a required secret | Example in the readme. No reader in code |
| JWT is off for every function in production | Documented only for the `hello` deploy command and for local `functions serve` |
