Abdelrahmane Bensahla Full-stack engineer

Full-stack engineer who ships production software end to end.

Work spans internal platforms at a state transportation agency, a subscription AI product with paying users, and a config-driven template system serving a client roster. Currently building network and data tooling at the New Jersey Turnpike Authority.

Open to full-stack, frontend and AI product roles Jersey City, NJ
← All work
Case study 01
SaaS, solo build

Job Cannon

A subscription product that matches a resume against a nightly-scraped corpus of startup jobs and emails the top ten every morning. Built solo, live, with paying subscribers.

Next.jsTypeScriptClaude APIStripePostgresVercel
The Job Cannon landing page: “Startup jobs, AI-matched to your resume, delivered daily” above the drag-a-resume-PDF drop zone that opens the free instant-match preview.
The pipeline
PDF resume
Claude extracts a typed profile
Keyword pre-filter over ~5K nightly-scraped jobs
narrowed candidate set
Claude re-ranks top 10 with per-job reasoning
Vercel Cron + Resend
Email digest, 8:00am ET
The decision
No vector embeddings

The obvious architecture for resume-to-job matching is embeddings and cosine similarity. I did not use them. At roughly 5,000 jobs in a narrow, highly structured domain, a keyword pre-filter followed by an LLM re-rank gave better precision than semantic similarity would have, because the domain vocabulary is standardized enough that lexical overlap is a strong signal. It also meant zero embedding infrastructure to run, version, and re-index every night.

What I traded away: this approach will not survive a corpus an order of magnitude larger or a domain with looser vocabulary. The pre-filter is the bottleneck, and at 50K jobs I would be re-ranking a candidate set too noisy to be useful. That is the point at which I would add embeddings, and I would rather add them when the data tells me to than carry the infrastructure from day one.

The rest of the build

Billing

Stripe Checkout with a trial, signature-verified webhooks keeping Stripe and Postgres in sync, and the Customer Portal for self-serve billing. The edge case that cost me an evening: checkout.session.completed can land before the subscription row exists, so the handler has to be idempotent and upsert rather than update.

Funnel

Clerk for auth, and a free instant-match preview at the top of the funnel: upload a resume, see three matches with reasoning, then hit the paywall for the daily digest.

The 8am run

Vercel Cron triggers the digest job, which fans out per subscriber and sends through Resend. A failed run is logged per user rather than per batch, so one bad resume parse does not cost everyone else their morning email.

The result
Live
jobcannon.app
3
Paying subscribers
~5K
Jobs scraped nightly
The 8am digest email in an inbox: “Your 10 startup jobs”, listing Stripe at 82 match and Chime at 78, each with a paragraph of reasoning.
What I would do differently

I built the ranking before I built any way to measure it. For the first month my only signal that matching worked was reading digests myself, which is the worst possible evaluator. I would start with a labelled set of fifty resume-to-job pairs and a script that scores a run against it, so that every prompt change has a number attached. I would also send the first digest to myself an hour early, because a scheduled job whose failure mode is silence is a job you find out about from a customer.

← All work
Work experience
Full-stack engineer, since 2025

New Jersey Turnpike Authority

I build internal tooling for the agency's network and data teams. The main system is a network visualization platform mapping 500+ enterprise devices and IP addresses, used daily by technical and non-technical staff across three departments. Before it, troubleshooting meant cross-referencing spreadsheets.

Next.jsTypeScriptPostgreSQLDrizzleNodePython
State agency system. Device names, addresses, and topology below are illustrative. Screenshots of the live app are not published.
Internal, no public link
Shape of the system
Illustrative
ingestion
scheduled ETLmonitoring agentmanual CSV import
core
deviceinterfacedevice_linkssite
surface
topology viewdevice detailsearchREST API
The performance work
2.4s to under 500ms

The topology page took 2.4 seconds to load. Two causes. First, the device-relationship tables had no composite indexes, so relationship lookups were scanning. Second, two Drizzle queries were N+1: one query for the device list, then one per device for its connections. I added composite indexes on the relationship tables and rewrote both patterns as joins. Load time dropped to under 500ms.

Before
Seq Scan on device_links
  rows=48212  cost=0.00..9821.4
+ 1 query per device (N+1)
──────────────────────────
total  2,412 ms
After
Index Scan using
  device_links_src_dst_idx
  rows=512  cost=0.29..184.6
single join, one round trip
──────────────────────────
total    418 ms
Sanitized plan summary. Figures rounded, table names generalized.
Also on this build

The API layer

REST endpoints in Node and Express: full CRUD over devices and relationships, auth middleware enforcing department-level access, and pagination on every list route because the device table only grows.

Script management service

A Python service orchestrating monitoring, ingestion, and scheduled ETL on a common runner with logging and retries, replacing 10+ hours of weekly manual work that previously lived in someone's head.

Mentoring

I onboarded two summer interns onto the codebase, paired on their features, and reviewed their PRs through to deployment. Writing the onboarding doc for them was the fastest way I found to notice which parts of the system I had never actually justified.

The result
500+
Devices mapped
−40%
Troubleshooting time
418ms
Topology load, was 2.4s
What I would do differently

I found the 2.4 second load because someone complained, not because anything told me. There was no query timing in the logs and no budget on the page, so a regression could sit there for weeks. I would put slow-query logging and a load-time assertion in CI on day one, since it is cheap to add early and awkward to retrofit into a system three departments already depend on. I would also have pushed harder for read-only demo data from the start, because the thing I most want to show about this project is exactly the thing I cannot publish.

← All work
Case study 02
Architecture

Sahl Solutions

A productized web service for local businesses. The engineering problem was not building one site, it was building the tenth site without rewriting the first nine.

Next.jsTypeScriptTailwindFramer Motion
The Sahl agency site: “Your work speaks for itself. Your website should too.” with a live client example and the headline figures — 7 days average delivery, $497 starting price, 100% you own everything.
The decision
Config-driven templates

Every client site is generated from a single typed site.config.ts holding colors, copy, section order, and integrations. Twelve reusable section components render from it. Per-client build time went from days to hours, and every site stays on one upgrade path: a fix to the contact section ships to all of them.

export const config: SiteConfig = {
  brand:    { name: "Client", accent: "#FFB000" },
  sections: ["hero", "services", "testimonials", "contact"],
  hero:     { headline: "…", cta: { label: "Book", href: "#contact" } },
  services: [{ title: "…", body: "…" }],
  integrations: { forms: "resend", analytics: "plausible" },
};
Trimmed and anonymized. The section union type is what keeps a typo from shipping.
The tooling evaluation

I evaluated Framer as a faster delivery path for the simplest builds. It wins on setup speed and loses on custom control, SEO, and bespoke integrations. I settled on using it only where speed was the whole requirement, and kept the Next.js template for anything where those three mattered.

Comfort Zone HVAC, a client site built from the template: “The air you breathe should feel intentional” over three service cards.
What I would do differently

I generalized on the second client instead of the fourth. Two of the twelve section components exist because one client asked once, and they have been dead weight in every review since. The rule I would apply now is to copy and paste until a third site needs the same thing, then extract. I would also have versioned the config schema early, because shipping a shared fix is only easy while every site is on the same shape.

abdelrahmane4216@gmail.com