# Technical documentation — footprint-app-api

Evidence date: 2026-10-14.

Source: `git@bitbucket.org:overdrive/footprint-app-api.git`, `master` at `fb96734` (2020-06-25, Jaco Brits, "Update the active member"). Only branch. 22 commits, 2020-03-26 through 2020-06-25.

Authors on those commits: Jaco Brits and JacquesOverdrive.

## What this repository is

A Serverless Framework service, name `footprint-reboot-api`, npm package `footprintApp-api` version `1.1.0`. README title: "Evolv Foorprint Reboot API".

The first commit (`c0c86b4`) is the [Serverless Node.js Starter](https://github.com/AnomalyInnovations/serverless-nodejs-starter) (MIT, copyright 2017 Anomaly Innovations). `package.json` `repository.url` still points at that GitHub project. The current tree is the member and permission API described below.

## Repository layout

| Path | Role |
|---|---|
| `serverless.yml` | Service, provider, 26 functions, commented `grouptomembercustom`, gateway-error include |
| `resources/api-gateway-errors.yml` | API Gateway `DEFAULT_4XX` and `DEFAULT_5XX` CORS |
| `create.js`, `get.js` | DynamoDB note put and get. `get.js` uses the wrapper. `create.js` uses a callback |
| `getuser.js`, `getfilteredusers.js`, `getuseruuid.js`, `getbyemail.js`, `getallusers.js` | Member reads |
| `updateusergeneral.js`, `updateuseridentity.js`, `updateuserother.js`, `updateuseraddress.js`, `updateusernextofkin.js` | Profile section updates |
| `updateemail.js`, `updateuuid.js`, `updatememberactive.js` | Email, Cognito uuid, status |
| `getpermissionmodules.js`, `getpermissions.js`, `getmoduledetails.js`, `getuserpermissions.js`, `getallcardpermissions.js`, `updatepermissions.js`, `updatecustompermissions.js`, `deletepermission.js` | Permission reads and writes |
| `getalltypes.js`, `getallgroups.js`, `getrole.js` | Lookup reads |
| `getgroups.js`, `savepermissions.js`, `grouptomembercustom.js` | Not functions in the template. The last is commented out in YAML |
| `handler.js` | Starter `hello`. Not a function in the template |
| `logging.js` | DynamoDB audit put |
| `libs/handler-lib.js` | Async wrapper, CORS, 200 or 500 |
| `libs/response-lib.js` | `success` / `failure` response objects |
| `libs/dynamodb-lib.js` | Document client `get`, `put`, `query`, `update`, `delete`. Used by `get.js` only |
| `libs/images-lib.js` | Empty file. Not imported |
| `mocks/*.json` | Local invoke events |
| `tests/handler.test.js` | One test, for `hello` |
| `env.example` | One sample variable. Not the names the template uses |
| `Member_Manage.txt` | Sixteen module-like names. Not read by code |
| `Untitled-2.txt` | Sample product JSON. Not read by code |
| `package.json`, `package-lock.json` | Dependencies |
| `.gitignore` | Ignores `node_modules`, `.serverless`, `.webpack`, `.env`, `env.yml` |

No `bitbucket-pipelines.yml`, Dockerfile, or infrastructure beyond `serverless.yml`.

## Runtime and deploy template

From `serverless.yml`:

- `provider.name`: aws
- `runtime`: nodejs12.x
- `stage`: prod
- `region`: eu-west-1
- `profile`: evolvqa
- `memorySize`: 512
- `package.individually`: true
- Plugins: `serverless-bundle`, `serverless-offline`, `serverless-dotenv-plugin`

VPC (active, not commented):

```yaml
vpc:
  securityGroupIds:
    - ${env:SECURITYGROUPIDS}
  subnetIds:
    - ${env:SUBNETIDS1}
    - ${env:SUBNETIDS2}
```

Provider environment:

| Name in the Lambda | Source |
|---|---|
| `tableName` | literal `footprint-log` |
| `databaseHost` | `${env:DBHOST}` |
| `databaseUser` | `${env:DBUSERNAME}` |
| `databasePass` | `${env:DBPASSWORD}` |
| `databaseName` | `${env:DBNAME}` |

IAM role statements allow DynamoDB `DescribeTable`, `Query`, `Scan`, `GetItem`, `PutItem`, `UpdateItem`, `DeleteItem` on `arn:aws:dynamodb:eu-west-1:*:*`.

README deploy command: `serverless deploy -v --aws-profile evolvqa`. README also says `npm i -g serverless`. Node engine is not pinned in `package.json`.

A missing `.env` value for any `${env:...}` name fails deployment. That is how Serverless variable resolution works. This repo does not include a sample `.env` with those keys.

## Environment

`env.example` contains only `SAMPLE_ENV_VAR`. The comment says to rename it to `.env`. `.env` and `env.yml` are gitignored.

`serverless-dotenv-plugin` loads `.env` into the Serverless variable space and, by that plugin's usual behaviour, into the Lambda environment. This repository does not set `dotenv` plugin options, so defaults apply. **Requires confirmation** if a deploy host relied on the shell environment instead of a file.

No secret value is committed. Do not copy identity ids out of `mocks/` into new docs; they are sample request context, not configuration.

## Route table

Stage prefix `/prod` is implied by `stage: prod`. No custom domain resource is declared.

| Function | Method and path | Handler | Store |
|---|---|---|---|
| `create` | `POST /footprint` | `create.main` | DynamoDB |
| `get` | `GET /footprint/{id}` | `get.main` | DynamoDB |
| `getfilteredusers` | `POST /footprint/members` | `getfilteredusers.main` | MySQL |
| `getuser` | `GET /footprint/user/{id}` | `getuser.main` | MySQL |
| `getuseruuid` | `GET /footprint/useruuid/{id}` | `getuseruuid.main` | MySQL |
| `getbyemail` | `GET /footprint/email/{id}` | `getbyemail.main` | MySQL |
| `getallusers` | `GET /footprint/users` | `getallusers.main` | MySQL |
| `updateusergeneral` | `POST /footprint/updateusergeneral` | `updateusergeneral.main` | MySQL |
| `updateuseridentity` | `POST /footprint/updateuseridentity` | `updateuseridentity.main` | MySQL |
| `updateuserother` | `POST /footprint/updateuserother` | `updateuserother.main` | MySQL |
| `updateuseraddress` | `POST /footprint/updateuseraddress` | `updateuseraddress.main` | MySQL |
| `updateusernextofkin` | `POST /footprint/updateusernextofkin` | `updateusernextofkin.main` | MySQL |
| `updateemail` | `POST /footprint/updateemail` | `updateemail.main` | MySQL |
| `updateuuid` | `POST /footprint/updateuuid` | `updateuuid.main` | MySQL |
| `updatememberactive` | `POST /footprint/updatememberactive` | `updatememberactive.main` | MySQL |
| `getpermissionmodules` | `GET /footprint/getpermissionmodules/{id}` | `getpermissionmodules.main` | MySQL |
| `getpermissions` | `POST /footprint/permissions` | `getpermissions.main` | MySQL |
| `getmoduledetails` | `POST /footprint/getmoduledetails` | `getmoduledetails.main` | MySQL |
| `getuserpermissions` | `POST /footprint/userpermissions` | `getuserpermissions.main` | MySQL |
| `updatepermissions` | `POST /footprint/updatepermissions` | `updatepermissions.main` | MySQL |
| `updatecustompermissions` | `POST /footprint/updatecustompermissions` | `updatecustompermissions.main` | MySQL |
| `getallcardpermissions` | `GET /footprint/getallcardpermissions` | `getallcardpermissions.main` | MySQL |
| `deletepermission` | `GET /footprint/deletepermission/{id}` | `deletepermission.main` | MySQL |
| `getalltypes` | `GET /footprint/types/{id}` | `getalltypes.main` | MySQL |
| `getallgroups` | `GET /footprint/groups` | `getallgroups.main` | MySQL |
| `getrole` | `GET /footprint/roles/{id}` | `getrole.main` | MySQL |

Commented and therefore not deployed: `grouptomembercustom` at `GET /footprint/grouptomembercustom`.

`GET /footprint/{id}` is declared beside literal `GET /footprint/users`, `GET /footprint/groups`, and `GET /footprint/getallcardpermissions`. This repo does not include a routing test.

## Handler wrapper

`libs/handler-lib.js`:

1. Run the async function.
2. On success, HTTP 200 and `JSON.stringify` of the return value.
3. On throw, HTTP 500 and `{"error":"<message>"}`.
4. Headers: `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Credentials: true`.

`libs/response-lib.js` `success(body)` and `failure(body)` already return `{ statusCode, headers, body: JSON.stringify(body) }` with status 200 or 500.

MySQL handlers return those objects from inside the wrapper. The client therefore receives HTTP 200 and a JSON object that itself contains `statusCode`, `headers`, and a string `body`. A `failure()` result is still HTTP 200.

`get.js` returns the DynamoDB item, so it is wrapped once. `create.js` does not use the wrapper. Its callback sets 200 plus the item, or 500 plus `{"status":false}`.

## DynamoDB

`libs/dynamodb-lib.js` builds `AWS.DynamoDB.DocumentClient` with no region override. Region comes from the Lambda environment (`eu-west-1` in the template).

`get.js` calls `dynamoDb.get`. `create.js` and `logging.js` construct their own `DocumentClient` and call `put`. The shared lib's `query`, `update`, and `delete` are unused.

Table name: `process.env.tableName` → `footprint-log`. The template does not create the table.

| Writer | Attributes |
|---|---|
| `create.js` / `get.js` | `userId`, `noteId`, `content`, `attachment`, `createdAt` |
| `logging.js` | `userID`, `logID`, `event`, `query`, `result`, `createdAt` |

`userId` and `userID` are different attribute names. Key schema is **Unknown / Requires Confirmation**.

`logging.js` calls `put` with a callback that returns the data and does not surface errors. The handler does not `await` it. `log` is invoked with `(event, result, query)`. The fourth parameter `callback` on `log` is unused.

## MySQL

Every MySQL file creates a module-level client:

```js
const mysql = require('serverless-mysql')({
  config: {
    host: process.env.databaseHost,
    database: process.env.databaseName,
    user: process.env.databaseUser,
    password: process.env.databasePass
  }
});
```

Handlers `await mysql.query(...)` then `await mysql.end()`. Statements are string concatenation. No `mysql.query(sql, params)` binding appears.

Tables named in SQL: `tbl_member`, `tbl_member_role`, `tbl_member_group`, `tbl_gender`, `tbl_ethnicity`, `tbl_organisation`, `tbl_organisation_group`, `tbl_status`, `tbl_country`, `tbl_city`, `tbl_identitytype`, `tbl_relationship`, `tbl_sportscode`, `tbl_position`, `tbl_suburb`, `tbl_province`, `tbl_socialmedia`, `tbl_additional_member_info`, `tbl_media_hub`, `tbl_medical_aid`, `tbl_permissions_group`, `tbl_permissions_member`.

There is no migration directory. Column types, indexes, and foreign keys are not in git.

SQL that is not valid as written, or that is inconsistent:

- `updateuseraddress.js`: comma before `WHERE`.
- `updatecustompermissions.js` insert branch: a column assignment inside `VALUES`.
- `savepermissions.js` (not routed): missing separator before the creator subquery.
- `getuser.js`: secondary position `ON` uses `pripos.PositionID`.
- `getallcardpermissions.js`: `GROUP BY CardPermission` while also selecting `PermissionGroupID`.
- `getbyemail.js` filters `email`; other files use `Email`.
- `getuserpermissions.js` filters `GroupID` and `ModulePermission`; the upsert uses `PermissionGroupID`.

`getallusers.js` does not call `log`.

## Auth

API Gateway authorizer on every route: `aws_iam`. Callers sign requests with AWS credentials that are allowed to invoke the API. This repository does not list the IAM principal.

Cognito appears only as:

- `event.requestContext.identity.cognitoIdentityId` on notes and on the audit item.
- `tbl_member.CognitoUuid`, read by path on `getuseruuid` and written by `updateuuid`.
- `data.personId` treated as a `CognitoUuid` when resolving `ModifiedUserID`.

No JWT check, no user-pool trigger, and no comparison of `personId` to the request identity.

## Response and error behaviour

| Case | HTTP status | Body |
|---|---|---|
| `create.js` DynamoDB success | 200 | the item JSON |
| `create.js` DynamoDB error | 500 | `{"status":false}` |
| `get.js` item found | 200 | the item JSON |
| `get.js` item missing | 500 | `{"error":"Item not found."}` |
| MySQL handler returns `success(...)` | 200 | JSON of the inner 200 response, whose `body` is a string of rows or the driver result |
| MySQL handler returns `failure(...)` | 200 | JSON of the inner 500 response |
| Thrown error inside `handler()` | 500 | `{"error":"<exception message>"}` |
| `grouptomembercustom` (if it were invoked) | 200 | JSON of the implicit `undefined` return is not a JSON value; the wrapper passes that return straight to `JSON.stringify` |

Gateway `DEFAULT_4XX` and `DEFAULT_5XX` add CORS headers. They do not change the application body.

`getfilteredusers.js` throws if `data.search` is missing, because it reads `.length` before the query.

## Dependencies

`package.json`:

| Package | Range | Role |
|---|---|---|
| `serverless-mysql` | ^1.5.4 | MySQL |
| `uuid` | ^7.0.3 | `v1()` ids for notes and logs |
| `aws-sdk` | ^2.661.0 (dev) | DynamoDB document client. On Lambda the runtime also provides a v2 SDK; the package is bundled because it is imported |
| `serverless-bundle` | ^1.2.5 (dev) | Webpack package and `npm test` |
| `serverless-dotenv-plugin` | ^2.1.1 (dev) | `.env` loading |
| `serverless-offline` | ^5.3.3 (dev) | Local API Gateway |

`npm test` → `serverless-bundle test`. Lockfile is `package-lock.json`. Node engine is not pinned.

## Security observations from the source

These are facts about the code, not a recommended design.

- SQL is string concatenation of path parameters and JSON fields.
- Member routes do not check the caller's identity against `MemberID` or `CognitoUuid`.
- Permission flag columns are not consulted before those routes run.
- `getallusers` returns up to 100 full `tbl_member` rows (`SELECT *`) to any IAM caller of that route.
- Filtered member search has no row cap.
- `deletepermission` is an unauthenticated-beyond-IAM GET that deletes a row.
- The audit log stores `JSON.stringify(event)`, which includes headers and the body.
- DynamoDB IAM is regional (`eu-west-1:*:*`), not one table.
- CORS allows any origin with credentials.
- `nodejs12.x` is the configured runtime.
- No rate limit, WAF, or authorizer scope beyond `aws_iam` appears in the template.
- `updateuseraddress.js`, the insert branch of `updatecustompermissions.js`, and `savepermissions.js` contain SQL that is not valid as written.

No password, token, or key material is committed. Database secrets are expected in `.env`, which is gitignored. `mocks/` does contain sample Cognito identity ids. Those values are omitted from Silicon Ark.

## Logging and monitoring

Application logging:

- `console.log(results)` in `getuser.js`.
- `console.log` of each member and each insert result in `grouptomembercustom.js`.
- `console.log("result :", results.length)` in `updatecustompermissions.js`.
- `logging.js` DynamoDB put, described above.

No CloudWatch metric filter, alarm, X-Ray, or log subscription is in the template. Lambda's default CloudWatch log group is an AWS default, not something this repo configures. Whether anyone reads those logs is **Unknown**.

## Development and testing

Documented in `README.md`:

- `serverless invoke local --function updateemail --path mocks/updateemail.json`
- `serverless deploy -v --aws-profile evolvqa`
- `npm i -g serverless`

`mocks/updateemail.json` is not in the tree, so that invoke command does not match the committed mocks.

`npm test` runs `tests/handler.test.js`. The test imports `handler.hello`, passes the strings `'event'` and `'context'`, and expects the callback form `statusCode === 200` and a string body. `handler.hello` returns a promise and does not call the callback argument. The test's expectations sit inside a callback that `hello` never invokes.

No test covers MySQL, DynamoDB, or the routes.

There is no CI configuration, so nothing in git runs tests on push.

## Local mocks

`mocks/*.json` are Lambda proxy-style events: `pathParameters` and/or string `body`, and `requestContext.identity.cognitoIdentityId`. They are not HTTP requests. Identity values are not copied here.

| Mock | File it resembles | Body or path fields |
|---|---|---|
| `getfilteredusers.json` | `getfilteredusers.js` | body `union`, `group`, `role`, `search` |
| `getuser.json` | `getuser.js` | path `id` |
| `getuseruuid.json` | `getuseruuid.js` | path `id` |
| `getbyemail.json` | `getbyemail.js` | path `id` |
| `getpermissionmodules.json` | `getpermissionmodules.js` | path `id` |
| `getpermissions.json` | `getpermissions.js` | body `GroupID`, `ModulePermission` |
| `getmoduledetails.json` | `getmoduledetails.js` | body `group`, `module` |
| `getuserpermissions.json` | `getuserpermissions.js` | body `GroupID`, `ModulePermission`, `personId` |
| `updatepermissions.json` | `updatepermissions.js` | body flags, `id`, `personId` |
| `updatecustompermissions.json` | `updatecustompermissions.js` | body flags, `id`, `personId`, `memberId` |
| `updateuuid.json` | `updateuuid.js` | body `MemberID`, `uuid` |
| `getalltypes.json` | `getalltypes.js` | path `id` |
| `getallcardpermissions.json` | `getallcardpermissions.js` | identity only |
| `deletepermission.json` | `deletepermission.js` | path `id` |
| `listAll.json` | no function named `listAll` | path `id` plus identity |

No mock is committed for `create`, `get`, `getallusers`, `getallgroups`, `getrole`, the five profile updates, `updateemail`, or `updatememberactive`.

## Background work, queues, storage, jobs

None. There is no SQS, SNS, EventBridge, scheduled event, or worker. The DynamoDB log write is in-process and not queued.

S3 appears only as commented client code and as URL strings in `getuser.js` (bucket `footprint-reboot-qa-zaf`, prefix `sportscode-1/members/`). Not deployed by this template.

`libs/images-lib.js` does not implement storage.

## What this document does not establish

- The live AWS account, API id, or custom domain.
- The MySQL host, schema DDL, or which status id means active.
- The DynamoDB key schema for `footprint-log`.
- That `nodejs12.x` can still be deployed.
- Any relationship, beyond the shared bucket string `footprint-reboot-qa-zaf` and the product name, to other Silicon Ark Footprint directories or to `unity-lambda-api`.
