Status: Accepted Date: 2026-02-23 Deciders: Reflections Maintainers
Context
Loti provides daily JSONL feeds of content-monitoring results (face-match detections across platforms). Their Lambda endpoint is IP-whitelisted — only pre-registered static IPs can fetch data. Railway (the hosting platform) assigns ephemeral IPs to containers. This means the API cannot call Loti directly; a relay with a static IP is needed. Requirements:- Fetch daily JSONL files from Loti’s IP-whitelisted endpoint.
- Forward results to the API in idempotent chunks.
- Minimize cost and operational surface area.
Decision
Use an EC2 t3.micro relay with a daily cron job that fetches JSONL from Loti and POSTs it in 500-line chunks toPOST /internal/loti-feed.
Data flow:
loti_results deduped by result_id.
Alternatives considered
| Alternative | Why rejected |
|---|---|
| Direct S3 cross-account access | Loti uses IP whitelisting, not IAM cross-account roles. Would require Loti to change their auth model. |
| Lambda-to-Lambda push | Requires Loti to implement a push model; they only support pull. |
| Serverless NAT Gateway | ~$32/mo for a NAT Gateway + Lambda just to get a static IP. Overkill for one daily cron. |
| Railway static IP add-on | Not available on the current plan. Even if available, would couple relay concern to app infrastructure. |
Consequences
Benefits:- Low cost (~$7.50/mo for t3.micro + EIP).
- Minimal surface: single bash script, single cron entry, single API route.
- Fully idempotent:
ON CONFLICT (result_id) DO NOTHINGmeans re-running is safe. - Chunked delivery (500 lines/request) keeps request sizes manageable.
- EC2 instance to maintain (patching, monitoring).
- Additional infrastructure outside Railway.
- Silent cron failure if EC2 goes down or Loti endpoint changes.
- Monitor daily row count in
loti_results— alert if zero new rows for 48 hours. - EC2 instance tagged and documented; relay script is simple enough to recreate in minutes.
Implementation notes
- The migration creates the
loti_resultstable with appropriate indexes. - DB query module provides
upsertLotiResultsre-exported via the write entrypoint. - API route validates and upserts JSONL chunks, authenticated via internal secret header.
- Body limit override is configured to accommodate larger JSONL payloads (10 MB).

