I build and ship web products, from schema to deploy.
Five years of full-stack work and test automation, mostly TypeScript and Next.js. I've been the only engineer on client projects, and spent two years applying other people's guidelines on model evaluation. Right now that's a live site holding , and a freight platform with going into its first deployment.
Every figure on this site opens to show how it was measured. Try one.
Sightline live
A site that tells you where a match, film or series can legally be watched from wherever you are. Rights get sold country by country and expire on dates nobody announces, so the real problem isn't fetching data. It's stopping a correct answer from going wrong on its own.
263 tests across 34 files, most of them demonstrated to fail against the old behaviour before being trusted. Provider failures are injected at the network layer so the real code path runs, and three guards were verified by hand-mutating the code they protect. Where a test could only ever be green, the page says so.
Test types unit · component render ·
fault injection · contract (provider response shape) · structural (rules asserted over the
source tree) · static type assertion · end-to-end · manual mutation
Tools Vitest · Playwright across Desktop Chrome, Mobile Safari and Mobile Chrome ·
Testing Library with happy-dom · MSW · Zod · @ts-expect-error compiler gates ·
tsc --noEmit
Failure modes exercised timeout · HTTP 500 · HTTP 429 · malformed JSON ·
provider down · empty result vs faulted lookup
CI, ten stages typecheck → dependency audit → unit tests with an enforced coverage
gate → browser install → build → start → wait-on → end-to-end. Pre-commit runs eslint and
the tests related to changed files only
Not present, stated no visual regression · no load testing · no automated
accessibility scanning — axe is installed and has never been called, which is worse than
not having it · no flaky-test policy · no mutation-testing tool
Eight upstream providers behind a fallback chain that preserves provenance rather than merging rows. Caching that keeps stale data physically present so it can be served when revalidation fails, an outbound quota enforced across serverless instances rather than per process, and schema validation being pushed boundary by boundary — three of eight so far.
Resilience patterns
stale-while-revalidate with a 48-hour grace window carrying the answer's age ·
negative caching of 404s · in-flight request deduplication · exponential backoff
(200/600/1800 ms) · fault memoisation so one caller discovers an outage and the rest are
told without a network wait · provider fallback with strict precedence, never a merge
Rate limiting inbound fixed-window per caller (Redis INCR with a
conditional EXPIRE) · a separate per-provider outbound budget, because the two
solve opposite problems and were once confused in a real defect
Validation Zod schemas at provider boundaries, with types inferred from the schema
so a nullable field becomes a compile error rather than a runtime 500
Stack Next.js App Router · TypeScript strict · React 19 · Upstash Redis · Vercel ·
GitHub Actions
Integrations TheSportsDB · football-data.org · TMDB · ESPN · newsdata.io ·
hCaptcha (SportMonks and API-Sports are wired with reserved budget slots and deliberately
parked)
HTTP and data standards 429 with Retry-After ·
Cache-Control: no-store on degraded responses · 404 as absence and 5xx as
fault · 17 Schema.org types · ISO 3166-1 alpha-2 · ISO 8601 · IANA time zones ·
sitemap protocol · Web App Manifest
CLS 0.000 in every run of every configuration. Analytics gated behind consent, with accept and reject given identical visual weight. Homepage 94 and guide 97 on standardised hardware, with the homepage LCP stated as failing rather than rounded into a pass.
Rendering React Server Components ·
streaming with Suspense · static generation via generateStaticParams ·
time-based revalidation · explicit dynamic opt-out where freshness matters
Core Web Vitals, by metric targeted fetchPriority="high" on the LCP
image · icons generated at build time from one SVG, 611 kB to 9 kB · a server-side slice
that cut 381 images and 2,322 DOM nodes down to what the page had actually asked for
Accessibility, by success criterion 4.1.2 Name, Role, Value · 2.1.1 Keyboard ·
2.4.3 Focus Order — combobox roles, keyboard navigation and Escape restoring focus, with
tests · 1.1.1 Non-text Content via aria-hidden on decorative slots ·
an end-to-end test that no page ships content hidden at opacity: 0.
Automated scanning is not present
Security headers Content-Security-Policy with a per-response nonce and
strict-dynamic · HSTS with preload · X-Frame-Options ·
X-Content-Type-Options · Referrer-Policy ·
Permissions-Policy disabling camera, microphone and geolocation
Privacy consent-gated analytics with accept and reject given identical weight
Security, mapped to OWASP categories
A03 Injection — JSON-LD escaping of <, with tests, plus a nonce-based CSP ·
A03 — schema validation at provider boundaries · A04 Insecure Design — rate limiting ·
A05 Misconfiguration — security headers and a repo-hygiene test that fails the build if an
ignored file is tracked · A06 Vulnerable Components — pnpm audit as a CI stage,
zero production advisories · A10 SSRF — an explicit path allowlist on the proxy route
Wino Trans pre-launch
Freight management for lorries running between Morocco and Europe, built for its first operator. The interesting problem is authorisation: the same person is often the owner and the driver, so the system has no roles at all. Permissions are capabilities, and Postgres enforces them at the row level rather than trusting the application to remember.
909 unit tests across 37 files, and zero end-to-end — Playwright is named in my own stack document and is not installed. The rules carrying real risk were verified by deliberately breaking them: removing the country guard returned listings for the wrong country, and the suite caught it.
Test types unit · integration · mutation
(manual, by deliberately removing guards) · static analysis · policy verification in
aborting transactions
Tools Vitest · dependency-cruiser layer rules · SQL assertions against
row-level-security policies
Gaps, stated zero end-to-end · Playwright named in the stack document and not
installed · no mutation-testing tool · no visual regression
21 tables, 60 foreign keys, 51 row-level-security policies, 8 parameterised capabilities instead of roles. Every write is queued in the browser first and drained later, so the same record can exist on a phone that has been offline for hours and on a server that has moved on.
Authorisation capability-based access
control rather than RBAC · PostgreSQL row-level security with
FORCE ROW LEVEL SECURITY · SECURITY DEFINER functions · JWT
custom access token hook · multi-tenancy with fail-closed policies
Offline outbox pattern · IndexedDB via Dexie · queue drain with idempotency keys ·
device-precedence conflict resolution (no trustworthy clock)
Stack TypeScript monorepo · React · Vite · Supabase Postgres · PostgREST
Domain standards CMR consignment note, box 17 payer matrix · ISO 3166-1 ·
IANA time zones · multi-currency with no implicit conversion
15 screens, offline-first, with the interface in French and Arabic — real right-to-left handling rather than a mirrored stylesheet.
Internationalisation French and Arabic ·
RTL with dir switching · logical CSS properties · isolated Latin digits and
codes
Offline UX optimistic writes · queued actions with visible state · partial-delivery
records
Stack React · Vite · IndexedDB
Sports live scores and data platform production
Built for an agency client, to their requirements and their review. A Next.js 14 platform serving live scores, fixtures and standings from multiple external providers, with Redis caching and a large dynamic route surface. 40+ routes, 50+ components, around 18,000 lines of TypeScript.
Playwright and Vitest are configured on this codebase, but I have no verified test count for it — the coverage numbers I quote are Sightline's, where I checked them.
A large build-time route surface over several external providers, with Redis caching in front of each.
Core Web Vitals work across a large dynamic route surface, SCSS and Tailwind, built for a client rather than for myself.
Stack Next.js 14 App Router ·
TypeScript · Redis · REST integrations across multiple sports data providers · Vercel
Patterns incremental static regeneration · dynamic route generation ·
server-side caching
Front-end React · Tailwind · SCSS · Core Web Vitals · responsive dynamic routes
AI and LLM model evaluation ongoing
Contract work with Outlier AI and UseBrainPlatform: evaluating model outputs and reviewing chain-of-thought reasoning, with Python tooling around the review work.
The work is adversarial reading — following a chain of reasoning to find where it becomes confidently wrong rather than where it stops. It is the same instinct the rest of my work runs on, which is why these projects are mostly about telling a real answer apart from a plausible one.
Evaluating model output is testing without a spec: looking for the case that passes every surface check and is still wrong.
Work model output evaluation ·
chain-of-thought review · comparative response assessment · annotation against task
guidelines
Platforms Outlier AI · UseBrainPlatform
Tooling Python
Data automation — AirCom Information & Technology, Shanghai past
Python and Selenium scrapers replacing a manual collection process. A run that took thirty minutes by hand took five once automated, and the output went to a team that used it.
Android application — Üsküdar University past
Built and tested an Android application end to end as a self-directed project. Never published to a store.
Where can I watch this, from where I am?
Broadcast rights get sold country by country, and they expire on dates nobody announces. Fetching the data is the easy half. The hard part is stopping a correct answer from going wrong on its own.
Live at sightlinetv.com · Next.js on Vercel · sole engineer
What this holds, and what it doesn't. Three weeks old, across seven countries. I typed each one after checking it against the broadcaster's own schedule. Every one can go stale without anything telling me, which is the problem the rest of this page is about.
The homepage misses its LCP target at 2.70 s against 2.50; the competition guide passes at 2.40. Both are lab numbers, since traffic is still below the threshold where Google reports field data. Nothing has been load-tested. That all seemed better said up front than buried at the bottom.
The one idea it's built on
Failing to fetch data isn't the same as data telling you there's nothing there. In code both arrive as an empty array. To a reader they mean opposite things. Mix them up and the site starts answering confidently when it should be saying it doesn't know, and that's worse than being down.
Publishing a broadcaster the user cannot actually watch on is worse than publishing nothing — it is the one claim this whole site is built on. lib/data/broadcast-rights.ts:11
Follow a thread
Nobody sells this data. I looked. The feeds that exist carry fixtures and metadata, and you can infer rights from them if you want to, but a guess printed as a fact sends someone to a channel they don't have.
So the table is typed by hand. It grows at whatever speed I can check things, which is slow, and every season turnover is more manual work. A feed would have covered thirty countries on day one. I'd rather have seven I can stand behind.
Three weeks into the season, the Australian row said Optus Sport. Optus had handed its rights to Nine on 30 June and shut the service down completely on 1 August. So the site was telling Australians to subscribe to something that didn't exist any more, and had been since the first weekend.
The mistake isn't the interesting part. "Verify these before deployment" had been sitting in my progress file since July, doing nothing.
A note in a queue is not a mechanism.lib/data/rights-freshness.ts
What replaced it: listings can carry an expiry date, and everything reads through a filter instead of the raw array. , so I can't honour the expiry in one place and forget it in another. A competition whose listings have all lapsed drops off the site rather than rendering an empty panel.
That same claim was also sitting in a blog post, which the expiry filter never touches.
So I fixed the post. I edited lib/blog/posts.ts, and the first line of that
file says it's auto-generated and not to be edited directly.
My local build runs Next straight and skips the generation step. The deploy doesn't. Typecheck passed, the tests passed, the build passed, the deploy went green, and production carried on serving Optus.
I lost a good while convinced it was cached. It wasn't. Cache miss, age zero, no-store. The edit had simply never survived.
Regenerating from the real source also cleaned two files I'd never opened. The claim had been living in four places. My mechanism reached one of them.
Faults get recorded separately from results. If a lookup failed, you see "we couldn't check just now". If it worked and found nothing, you see "nothing recorded". A country I've never asked about is a third thing again.
It matters more than it reads. If a lookup times out and the page 404s, Google takes that as permission to deindex, and getting back in takes far longer than the outage did.
A missing API key fell through to "123", which is TheSportsDB's shared
public test key. It answers. Valid JSON, a slice of the catalogue, so a missing key looked
like a working one at every layer above it.
There's a longer list. A rate limiter guarding routes I'd already deleted. A dependency audit reading a lockfile the project doesn't install from. A health probe I built to close a "nothing alerts" gap, which had failed on every run since I wrote it.
The one with a number on it: I deleted an environment variable nothing read. Nothing did read it. But middleware threw on it, and every route returned 500 for about .
None of these were missing checks. Every one had a check that passed, confirming something next to the thing I was trusting it for. So the assertions moved onto what the site actually serves instead of how it's configured, and now a new test has to go red against the old behaviour before I believe it.
No, and it took three tries to work that out. I quoted a Lighthouse score of 99. Four later runs of the same build gave me 92, 94, 93, 92.
So I built a proper harness. Pinned browser, fixed throttling, five runs, median and spread printed together. It told me the homepage was failing, I wrote that down as framework-bound and not worth chasing, and I reverted a change because the numbers didn't support keeping it.
The harness was the problem. Its own spread ran over thirty points, from CPU contention and no warm-up run, so it couldn't see an effect smaller than its own noise. That's exactly what I'd pointed it at. On Google's hardware: . I put the reverted fix back.
I'd built a tool to stop myself quoting single runs, then trusted it without asking whether it could see anything.
What it looks like
2880×1800
2880×4308
About
I'm a software engineer in Inezgane, Morocco. I work freelance and on contract, which means I've carried projects end to end: schema design, backend, interface, test strategy, deployment.
Working alone is how freelancing tends to go, not a preference. The sports platform was built to an agency's requirements and reviewed by them. Wino Trans came out of sitting with an operator, reading his paper documents and asking what he actually does on a Tuesday. Two years of model evaluation is nothing but applying someone else's guidelines consistently and being marked on it. I'd like a team next, and I'm looking for full-stack, front-end or test automation roles, remote or relocating.
There's a thread running through the work. I don't like systems that go wrong quietly, so I build for the case where a provider is down, a deal has expired, or a check is passing for the wrong reason. Telling "no answer" apart from "no attempt" turns out to be most of the job.
Experience
Full-stack developer — freelance
Sightline, and a sports live scores and data platform for an agency client. Next.js, TypeScript, Redis, multiple external providers.
AI and LLM model evaluation — Outlier AI, UseBrainPlatform
Evaluating model outputs and reviewing chain-of-thought reasoning, with Python tooling around the review work.
Data automation — AirCom Information & Technology, Shanghai
Python and Selenium scrapers replacing a manual collection process; thirty minutes of manual work became five.
Wino Chouk Trans — internship
International road freight operations between Morocco and Western Europe.
Education
- B.Sc. Software Engineering — Üsküdar University, Istanbul
- 2019–2023 · EQF Level 7, 242 ECTS · High Honor (Spring 2020), Honor (Fall and Spring 2021)
What I work with
- Languages
- TypeScript · JavaScript · Python · SQL (reading and writing queries; no production database ownership) · Java
- Front-end
- React · Next.js App Router · Vite · Tailwind · SCSS · Core Web Vitals · WCAG 2.2 AA · internationalisation including RTL
- Back-end and data
- Node · PostgreSQL · Supabase and PostgREST · row-level security · Redis (Upstash) · REST integration · caching strategies including stale-while-revalidate and negative caching · rate limiting (per-IP windows, token-bucket quotas)
- Testing
- Vitest · Playwright · Selenium WebDriver · JUnit · REST Assured · Postman · unit, component-render, contract, fault-injection and structural testing · static type assertions as compile-time gates · manual mutation verification · coverage gates enforced in CI
- Infrastructure
- Vercel · GitHub Actions · Jenkins · Git · scheduled jobs via cron workflows · dependency auditing as a pipeline stage · Content-Security-Policy with a per-response nonce
- Standards I work against
- WCAG 2.2 AA success criteria (4.1.2, 2.1.1, 2.4.3, 1.1.1) · Core Web Vitals · OWASP Top 10 categories A03, A04, A05, A06, A10 · Schema.org · ISO 3166-1 · ISO 8601 · IANA time zones · GDPR consent gating
Not on this list because I cannot demonstrate it: Docker, Kubernetes, Terraform, GraphQL, message queues, OAuth, observability tooling, load testing, and automated accessibility scanning.
Languages
- Arabic and Tachelhit
- Native
- English
- C1
- French
- B1–B2
- Turkish
- Intermediate
Contact
formyownwork@gmail.com
github.com/shad-yy