Case study 03 / Backend engineering
Embeddable Widget Platform
A multi tenant lead capture platform that drops into any customer's website with a single script tag. The interesting problem was never building the widget. It was making certain that one customer's data could never surface in another customer's dashboard, and being able to prove it.

- Role
- Sole engineer
- Timeline
- August 2026
- Stack
- Node, Express, TypeScript, PostgreSQL, Docker
- Status
- Complete, public repository, MIT licensed
- Context
- Capstone for the FlyRank AI backend engineering internship
The problem
An embeddable widget has three different callers with three different trust levels hitting the same system.
The owner, authenticated, who should see their own data and nothing else. The customer's website, which loads widget configuration publicly and read only. And the visitor, who submits a form from a browser you do not control, on a domain you did not write, at a volume you cannot predict.
Most of the ways this goes wrong are not bugs in the widget. They are architectural. A tenant boundary enforced in the wrong layer. A public endpoint that leaks more than it should. A submission path that is abusable precisely because it has to be open.
The decisions
Tenant isolation enforced in the query layer, not in route handlers.
Every query is scoped by owner id at the point the data is fetched, so the boundary lives in one place. Rejected: checking ownership in middleware or inside each handler, which works perfectly until one route forgets, and one route always eventually forgets.
Return 404, not 403, on cross tenant access.
A 403 confirms that the resource exists and belongs to somebody else. A 404 tells an attacker nothing at all. Small decision, and it closes an enumeration channel that would otherwise let someone map which record identifiers are real.
Reused Supabase Auth rather than building authentication.
Owner id is the Supabase user UUID. Rejected: implementing sessions and password hashing myself, which would have added a week and a far larger surface to get wrong, in exchange for nothing a customer would ever notice.
Degrade, do not fail, on enrichment.
Geo enrichment runs through two providers with a fallback, and the geo columns are nullable by design. If both providers are unreachable, the submission still saves. Rejected: treating enrichment as required, which would mean a third party outage quietly destroying a customer's leads.
Three hardening layers on the public submission path, because it must stay open.
CORS with explicit preflight handling, a request size limit, per IP rate limiting, and a honeypot field for spam. None of that is clever. All of it is the difference between a demo and something you would agree to put on a paying customer's website. All of it runs before the submission is accepted, so a 201 means the size limit, the rate limiter and the honeypot have each passed.

One click produces two requests: the browser's CORS preflight, then the submission itself. A cached configuration projection for the public read path.
A widget loading on a high traffic customer site does not hit the database on every page view. Read paths and write paths have different shapes, and treating them the same is how systems fall over at exactly the moment they start working.
What did not work
The test suite was written after the hardening rather than alongside it, which meant going back to construct cases for behaviour already implemented. Tests written after the fact verify what the code does, not what it should do. Everything passed, but the ordering was wrong and it is not how I would sequence it again.
The outcome
A deterministic test suite in vitest and supertest covering tenant isolation, the public submission path and the rate limiter. One command brings the whole system up: docker compose up, with a multi stage build and migrations applied automatically on start. The repository was verified clean of secrets before publication.
The project was submitted as the culminating capstone for the FlyRank AI backend engineering internship, and was reviewed and accepted by the lead track mentor. The internship record documents at least 59 hours across practical assignments in API contracts, task design and prompting, retrieval and grounding, and evaluation and operations.
There are no usage numbers, because this has no production users. It is an engineering artefact and it is presented as one. What it demonstrates is how the decisions were made, not how much traffic it has served.
Verify: internship.flyrank.ai/verify, ID FR-D11-1A753-9D8EE
What I would do differently
Written the tenant isolation tests first, before the query layer existed, so that the boundary was defined by a test that failed rather than confirmed by one that passed.