Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/docs/architecture/testing-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ of operations.
| End-to-end | Playwright | `testing/src/specs/` | `pnpm test:e2e` |
| Type-check | `tsc` | — | `pnpm lint` |

**Image-only changes are the one exception to "unit test and e2e test".** Nothing in either tier
runs a production image: the e2e suite starts each app's `dev:test` directly. A change confined to
a `Dockerfile`, a container entrypoint such as `apps/gateway/scripts/run.sh`, or
`docker-compose.yaml` therefore cannot get a test in this repo. Verify it against the built image
instead, and list the commands and their results in the PR.

There is no integration tier. `apps/api` unit tests mock the Prisma layer entirely; the only code
path exercised against a real database is Playwright.

Expand Down
12 changes: 12 additions & 0 deletions apps/gateway/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ state `apps/api` owns — so persisting either would only add a second copy that
`db:push` before `dev`, `lint`, `test:e2e` and — via the `@opendatacapture/gateway#build` key in
`turbo.json` — before `build`, so a gateway build needs `GATEWAY_DATABASE_URL` set.

**The production image starts as root on purpose and drops to `node` in `scripts/run.sh`.** The
gateway is the one service published straight to the internet, so its server must not run as root,
but compose bind-mounts `./sqlite` at `/app/sqlite`, and on deployments made before this change that
directory and `gateway.db` are owned by root. `run.sh`, while still root, creates the database
directory, `chown`s anything in it not owned by `node`, then re-executes itself through
`setpriv --reuid=node --regid=node --init-groups --no-new-privs`. That directory is the only path the
server writes; `/app/dist` stays root-owned. Do not add `--reset-env`: it would drop
`GATEWAY_API_KEY` and `GATEWAY_DATABASE_URL`. Started as any other user (`docker run --user`, a
Kubernetes `runAsUser`), `run.sh` exits with a message if the directory or the database file is not
writable, instead of failing on the first write. So `docker compose exec gateway id` still prints
root; check the server's uid with `docker compose exec gateway grep ^Uid /proc/1/status`.

## Build and dev

`pnpm build` is three passes into `dist/`: Vite client build (`dist/client`), Vite SSR build
Expand Down
18 changes: 18 additions & 0 deletions apps/gateway/scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,26 @@
set -eu

DATABASE_FILE=${GATEWAY_DATABASE_URL#file:}
DATABASE_DIR=$(dirname "$DATABASE_FILE")
EMPTY_DATABASE_FILE="/app/gateway.tmpl.db"

# The gateway faces the public internet, so its server must not run as root. The image still starts
# as root: on existing deployments the bind-mounted database directory (and the database file the
# old root entrypoint copied into it) is owned by root, and only root can hand it to `node`. Fix the
# ownership of that directory, the only path the server writes, then re-run this script as `node`.
if [ "$(id -u)" = '0' ]; then
mkdir -p "$DATABASE_DIR"
find "$DATABASE_DIR" \! -user node -exec chown node:node '{}' +
exec setpriv --reuid=node --regid=node --init-groups --no-new-privs "$0" "$@"
fi

# Started as another user (`docker run --user`, a Kubernetes `runAsUser`): nothing can fix the
# ownership here, so fail now rather than on the first write to the database.
if [ ! -w "$DATABASE_DIR" ] || { [ -e "$DATABASE_FILE" ] && [ ! -w "$DATABASE_FILE" ]; }; then
echo "error: the gateway database at '$DATABASE_FILE' is not writable by uid $(id -u). Make '$DATABASE_DIR' and its contents writable by uid $(id -u), or start the container as root so it can fix the ownership itself." >&2
exit 1
fi

if [ ! -s "$DATABASE_FILE" ]; then
cp "$EMPTY_DATABASE_FILE" "$DATABASE_FILE"
fi
Expand Down
Loading