Esmeralda C. Cabrera Ventura ← Articles Portfolio LinkedIn
Engineering Dossier Full-Stack Prototype Computer Science Capstone

Fridge2Meal

An ingredient-first meal platform built to reduce household food waste.

Architecture & Implementation
Esmeralda C. Cabrera Ventura
Lead Architect · Backend & Database Engineering · Application Security Engineer · Platform Administrator
Scope of this document
A technical account of the system I designed and built: the matching engine, the relational data layer, the identity and security model, and the containerized runtime that ships them together.
Engineering Facts
Per one deployed instance · local Docker runtime

Orchestrated services5

Core relational tablesPostgreSQL, normalized16

Backend API domainsNode.js / Express REST14

Frontend routesReact, auth-protected12

Ranking signals per recipeweighted, deterministic7

Documented test cases30
Passed30 · 100%
Failed or blocked0

Core path, external dependencies0

Recipe matching, ranking, pantry intelligence and shopping automation run entirely on PostgreSQL queries and backend logic. The generative-AI layer is an independently toggled enhancement layered on top, handling open-ended suggestion and substitution. Each layer does the work it is best suited to.
01
Thesis

Where the intelligence lives

A deliberate split: deterministic logic owns the guarantees, generative AI owns open-ended suggestion.

Roughly a third of food produced worldwide is never eaten, and much of that loss happens in home kitchens. Fridge2Meal starts from the inventory a household already owns and works forward: what can be cooked tonight from what is already there, and what is closest to spoiling.

The fastest path to that product is to forward the pantry contents to a language model and render whatever returns. It demos beautifully. It also produces different answers to identical questions, carries a per-request cost that scales with usage, and hands availability to an external provider.

I split the system instead. Every result the user depends on — which recipes match, how well they match, what is missing, what is about to spoil, what belongs on the shopping list — is computed by code and SQL I designed. The generative layer sits alongside that core, behind a provider-agnostic abstraction, clearly labeled in the interface, and independently toggleable. When enabled it does what generative models are genuinely good at: proposing variations and surfacing substitutions outside the fixed corpus.

Determinism is a feature. The same pantry returns the same ranked results every time, whether or not an external service is reachable.

That split is what makes the rest of this document interesting. It put real problems on the table: ingredient identity resolution, weighted scoring, many-to-many relational modeling, expiration-aware ranking, and the operational work of making five services start in the right order on a machine that has never seen the project before.

02
System

Layered architecture

Five tiers with strict boundaries. Each layer has one responsibility and no reach into the layer below it.

I designed the platform as a modular layered system so that the interface, the application logic, the data, the identity provider and the optional AI enhancement could each evolve independently. The rule I enforced throughout: the service layer never writes raw SQL, the repository layer never contains business logic, and the controller never touches data directly. That separation is what keeps a feature change from becoming a systemic refactor.

LayerTechnologyResponsibility I owned
PresentationReact · protected routing · context providersAPI contract design, auth-gated route structure, state boundaries between pantry, notification and session context
ApplicationNode.js · Express · RESTRoutes, controllers, middleware and services across 14 domains; centralized error handling; upload and backup middleware
Domain logicCustom Node.js servicesIngredient normalization, weighted compatibility ranking, expiration analysis, substitution resolution
DataPostgreSQLSchema design, normalization, junction modeling, indexing, seed and migration workflow
IdentityKeycloak · OAuth 2.0 / OIDCRealm configuration, PKCE flow, JWT validation middleware, role-based access control
EnhancementGroq API (optional)Provider-agnostic service abstraction, rate limiting, isolation from the core path
RuntimeDocker ComposeService definitions, dependency ordering, volume persistence, startup / reset / backup scripting

Because the AI provider lives behind a dedicated service class, it can be swapped, upgraded or removed without any change to controllers, business logic or the user interface. That abstraction keeps the enhancement layer portable across providers, and it proved its worth the moment the feature needed to be demonstrable with the key unset.

03
Engine

The matching engine

The architectural centerpiece: turning messy human ingredient input into ranked, explainable recipe results.

People do not enter ingredients the way a database stores them. They type "grilled chicken," "rotisserie chicken" and "chicken breast" to mean the same thing. They pluralize inconsistently, abbreviate, and attach preparation state to the noun. A naive keyword match fails on all of it.

So the first thing the engine does is stop treating ingredient text as text.

01

Normalization

Input is lowercased, stripped of punctuation and preparation descriptors, whitespace-trimmed, and resolved against an alias table in PostgreSQL that maps known variants onto a single canonical ingredient identity. This is the step that converts a keyword search into an ingredient-intelligence system.

02

Candidate retrieval

Normalized ingredient identifiers are queried against the recipe_ingredients junction table, which resolves the many-to-many relationship between recipes and ingredients and carries per-row quantity, unit, optional status and importance level.

03

Weighted scoring

Each candidate recipe is scored on seven signals rather than raw match count. The formula weighs the ratio of matched to required ingredients against a penalty for missing ones, so a ten-ingredient recipe with nine matches correctly outranks a four-ingredient recipe with three.

04

Substitution & severity resolution

Missing items are classified as optional or critical, and the substitution table is consulted for viable replacements. The user learns not just what is missing but whether it actually blocks the dish.

05

Explainable output

Every result returns a compatibility score, match percentage, matched list, optional-missing list, critical-missing list and a ranking rationale — all computed deterministically in the backend, never generated.

The seven ranking signals

Signal 01
Match ratio
Matched ingredients as a proportion of those the recipe requires.
Signal 02
Missing penalty
Weighted deduction scaled to how many required items are absent.
Signal 03
Core component
Presence of anchor ingredients such as the protein, weighted above garnish.
Signal 04
Expiration bonus
Recipes that consume soon-to-spoil pantry stock are promoted.
Signal 05
Substitution reach
Gaps that a known substitute can close cost less than gaps that cannot.
Signal 06
Dietary fit
Alignment with the user's stored dietary preferences.
Signal 07
Complexity
Recipe difficulty factored against effort the user is likely to accept.

Signal 04 is the one I am most attached to, because it is where the data model earns its keep. Pantry rows carry expiration dates; the backend derives a freshness state — fresh, expiring soon, expired, unknown — and the ranking engine reads that state directly. The product's core promise, wasting less food, is implemented as a term in a scoring function rather than as a marketing claim.

04
Data

Relational design

16 normalized tables. Every personalized record keyed to an authenticated identity.

I designed the schema around one hard relationship and one hard constraint. The hard relationship is many-to-many: recipes contain many ingredients, ingredients appear in many recipes, and the join carries meaning of its own — quantity, unit, optional flag, importance. That belongs in a junction table with attributes, not in a denormalized array.

The hard constraint is isolation: pantry contents, saved recipes, meal plans, shopping lists, notifications, uploads and backups are all keyed to the authenticated user, so a query can never surface another account's data.

ClusterTablesWhat the design enables
Identityusers · user_sessionsKeycloak subject linkage and session state as the anchor for every personalized row
Ingredient intelligenceingredients · ingredient_aliases · ingredient_substitutesCanonical identity, variant resolution and substitution reach — the backbone of normalization
Recipe corerecipes · recipe_ingredientsAttributed many-to-many join carrying quantity, unit, optional status and importance level
Pantryuser_ingredients · user_custom_ingredientsPersistent inventory with quantity, unit, category and expiration date driving freshness state
Workflowshopping_list · meal_plans · user_saved_recipesMissing-ingredient automation, scheduled meals and favorites as first-class records
Engagementnotifications · user_search_historyExpiration alerting, read-state management and behavioral signal capture
Media & AIuser_recipe_photos · ai_generated_mealsUploaded imagery and optional AI output stored without polluting the canonical recipe table

The database package ships with schema files, seed data, migration files and an initialization script executed by Docker Compose at first startup, so a clean checkout produces a populated, queryable database with no manual steps. Migrations are regression-safe, which mattered more than expected once the schema started moving weekly.

One design decision worth flagging: AI-generated meals are stored in their own table rather than written into recipes. Keeping generated content out of the canonical corpus means the deterministic matching engine can never be contaminated by unvalidated model output, and generated meals can be promoted deliberately rather than by accident.

05
Security

Identity and defensive posture

Authentication delegated to a dedicated identity provider. Authorization enforced at the API boundary.

The tempting shortcut here is hand-rolled auth: a users table, a password column, a session flag in the frontend. Identity belongs with a dedicated provider instead, so authentication is delegated to Keycloak and the backend's job is narrowed to one thing it can do reliably — verifying a token before performing a protected action.

ControlImplementation
Identity providerKeycloak realm configured and exported as version-controlled configuration; OAuth 2.0 and OpenID Connect flows for login, registration and token issuance
Authorization-code + PKCEPublic-client flow with proof key exchange, so no client secret is ever shipped to the browser
Token validationExpress middleware validating Keycloak-issued JWTs on every protected route before any controller executes
Access controlRole-based policy at the API layer, mirrored by protected routing in the React client — the client hides, the server enforces
Session lifecycleToken refresh, logout, password change, security-question support and password-reset flow captured locally through MailHog
Transport & headersHelmet middleware for HTTP security headers; CORS restricted to an explicit origin allowlist
Abuse resistanceRate limiting on the AI endpoints, the one surface with real per-request cost and third-party exposure
Input handlingBackend validation on all mutating routes; malformed and blank input rejected before reaching the data layer
SecretsEnvironment-variable-based credential management; no keys, connection strings or realm secrets committed to the repository
File uploadsIsolated upload infrastructure for avatars and recipe photos with backend validation and controlled serving, kept off the application path

Authentication turned out to be the highest-risk integration in the system, because nearly every feature is personalized — a failure there does not degrade the product, it locks the user out of their pantry, plans, lists and settings. Getting the frontend login flow, backend validation middleware, protected routing, refresh behavior, logout semantics and profile synchronization to agree with one another was the single hardest coordination problem I solved on this project. Verified end to end: valid login succeeds, invalid credentials return a clear error, and direct navigation to an authenticated route without a token redirects to login.

06
Platform

Runtime and DevOps

One command. Five services. Identical behavior on a machine that has never seen the project.

As platform administrator I owned the question that decides whether a project is real: can someone else run it? The answer had to be yes without a page of setup instructions, so I containerized the entire stack and orchestrated it with Docker Compose.

Service 01
React frontend
Client container serving the authenticated application.
Service 02
Express API
Node.js backend, business logic and REST surface.
Service 03
PostgreSQL
Persistent store, initialized and seeded on first boot.
Service 04
Keycloak
Identity provider with imported realm configuration.
Service 05
MailHog
Local SMTP capture for reset and notification email.
Volumes
Uploads & backups
Mounted storage that survives container teardown.

What this actually cost

Compose makes multi-service orchestration sound trivial. It is not. Getting five containers to cooperate meant solving container networking, service dependency ordering, environment-variable propagation, port mapping, startup timing races between the API and a database still initializing, first-run schema execution, and Keycloak realm import that had to complete before the backend would accept a single request.

I wrote Windows and shell scripts for start, stop, reset and backup-folder configuration so that the whole environment is reproducible and disposable — a bad state is fixed by resetting, not by debugging someone's local machine.

Why local, and why that was the right call

The original plan allowed for cloud hosting. I deprioritized it deliberately. Cloud deployment would have consumed the remaining schedule on infrastructure configuration, cost management, domain setup, managed database provisioning, object storage and monitoring — none of which demonstrates the engineering the project exists to demonstrate. Containerization delivers the same portability and environment-consistency benefits at zero recurring cost, and because the architecture is already service-isolated and volume-backed, migration to a cloud provider is a deployment exercise rather than a rewrite.

07
Durability

Backup and restore

Built past requirement, because a system that holds user data and cannot give it back is not finished.

Data protection was not a required deliverable. I built it anyway, and it became one of the more technically demanding subsystems in the application — export has to gather user-scoped database rows and uploaded files into a single archive, and restore has to reintroduce that data without corrupting live state.

  • On-demand export of user-scoped records and uploaded media into a portable ZIP archive
  • Restore from local disk or from a user-uploaded archive, exposed through the Settings interface
  • Scheduled local backups with retained history and file listing
  • Configurable backup folder, syncable to a cloud-storage directory if the user has one
  • Separate user data export, so the account owner can leave with their own data
  • Docker volume persistence underneath, so database and uploads survive container recreation

Restore is the dangerous half. It is the one operation in the product that can destroy user data if it is wrong, which is why it received disproportionate testing relative to its size.

08
Build

The running system

Captured from the running application in the local container environment.

Full walkthrough · 10:03The complete system running end to end: container startup, pantry entry, ranked matching, recipe detail, shopping-list automation, meal planning, and the backup workflow.
Application dashboard
DashboardAuthenticated landing surface routing to pantry, recipes, planning and notifications.
Pantry management
PantryPersistent inventory with quantity, unit, category and expiration date — the input to freshness state.
Recipe matching results
Ranked resultsRecipes returned by the matching engine, ordered by weighted compatibility rather than filtered by keyword.
Recipe detail
Recipe detailInstructions, full ingredient list and the missing items separating the user from the dish.
Shopping list
Shopping listMissing ingredients promoted from recipe results; cleared items flow back into pantry inventory.
Meal planner
Meal plannerRecipes scheduled by date and meal type against persisted plan records.
Optional AI generation
Optional AI layerClearly separated from database-driven results, and fully removable.
Settings and backup
Backup & syncUser-facing controls over export, restore, scheduling and backup location.
Login screen
AuthenticationKeycloak-backed login; unauthenticated route access redirects here.
Running containers
Container runtimeAll services running side by side under Docker Compose.
Local mail capture
Mail captureReset and notification email verified locally without sending to real recipients.
09
Verification

What was tested, and what was not

Results are only useful alongside their limits.

30
Cases executed
100%
Pass rate
0
Failed / blocked
13
High priority

Verification was conducted as black-box end-to-end testing against the running container stack, which meant every case was also an integration test: an authentication case exercised the frontend, Keycloak, session handling, protected routing and backend token validation simultaneously. Cases were re-executed across multiple dates after builds shipped, giving the suite a regression character.

Negative paths were tested deliberately — invalid credentials, blank input, unauthorized route access, malformed data. Notable results: duplicate pantry entries were prevented by updating the existing quantity rather than inserting a second row; blank ingredient search returned validation feedback rather than an empty query; and searches with a single ingredient and with more than twenty ingredients both returned ranked results within an acceptable response window, which is the closest thing the suite has to a load signal.

Limits I will state plainly

This was manual testing by a single tester. It does not establish behavior under concurrent load, cross-browser and cross-device compatibility, penetration-level security assurance, sustained uptime, or recovery from deliberate database, identity-provider or AI-service outages. Automated unit, integration, API and end-to-end coverage is the first thing I would add before this system faced real users, and I would rather say so than let a 100% pass rate imply more than it earned.

10
Status

Honest build state

Scoped using the same three-state model the pantry uses: shipped, prototype-level, deferred.

CapabilityStateNote
Ingredient matching & rankingShippedNormalization, weighted scoring, substitution and severity resolution operational
Relational data layerShippedSchema, seeds, migrations and container-run initialization
Identity & access controlShippedKeycloak realm, PKCE flow, JWT middleware, protected routing
Containerized runtimeShippedFive services, persistent volumes, start / stop / reset scripting
Backup & restoreShippedExport, restore from disk or upload, scheduling, history
Pantry & expiration intelligencePrototypeFully functional; wants broader real-world usability validation
Shopping automationPrototypeRecipe-to-list and list-to-pantry sync need further edge-case testing
Notifications & digestPrototypeAccuracy depends on scheduled checks and production email delivery
Optional AI generationPrototypeDeliberately non-essential; needs output validation and quota handling
Cloud deploymentDeferredArchitecture is migration-ready; deferred by schedule decision, not capability
Automated test coverageDeferredHighest-priority next investment
Observability & analyticsDeferredHealth checks and logging exist; metrics and tracing do not
Nutrition analysisDeferredSchema anticipates it; scoring engine has a slot for it
Native mobileDeferredResponsive web only in this build
What this build demonstrates

Built in layers, on purpose


Fridge2Meal is a working full-stack system with a real data model, a real security boundary, a real deployment story, and a core intelligence layer I designed and implemented directly. It runs on any machine with Docker installed, from one command, with no external service required for its primary function — and the AI enhancement layer is more useful precisely because there is a reliable foundation beneath it to enhance.


React · Node.js · Express · PostgreSQL · Keycloak · Docker Compose

Portfolio & resume  ·  LinkedIn  ·  GitHub