[2026-01-18] Tech Task: Shared Zod Schema
Task Metadata
Date: 2026-01-18
Status: Complete
Related Task:
2512_genai_food_tracking-db8Blocker For:
2512_genai_food_tracking-7sc(Guest Mode Backend)
Objective
Goal: Establish a shared library for Zod validation schemas.
Context:
Both the Frontend (Guest Mode / Dexie) and the new Backend (Import Lambda) need to validate Meal objects. To avoid logic duplication and ensure the backend accepts exactly what the frontend allows, we need a single source of truth.
Crucial Architectural Note:
- AppSync JS Incompatibility: Our existing resolvers (
addMeal.js, etc.) run on the AppSync JS runtime, which cannot import NPM packages. They will continue to use manual validation. - Target Consumers: This shared library is strictly for:
- Frontend:
LocalMealService(Dexie) validation. - Backend: The new Node.js
importGuestHistoryLambda. - Scope: This task creates the infrastructure and verifies it is importable. Actual integration into the new features happens in their respective tasks.
- Trigger: Guest Mode Backend planning identified a need for shared validation.
- Constraints:
- Must work with Bun (Frontend/Local) and SAM/Node.js (Lambda Build).
- Must minimize disruption to the existing monolithic structure.
Technical Strategy
We will implement a Local Workspace pattern using Bun/NPM Workspaces.
- New Module: Create
shared/at the project root. - Configuration: Configure root
package.jsonto define a workspace containingshared,frontend, andfunctions/*. - Consumption:
- Frontend: Add
"@chatkcal/shared": "*"tofrontend/package.json. - Backend: Add
"@chatkcal/shared": "*"to the Lambda'spackage.json. SAM/Esbuild should bundle this code into the artifact.
Testing Strategy
- Unit Tests (New): Install
vitestin thesharedworkspace and implement atestscript. - Regression Tests (Frontend): Run
task test:frontendto ensure adding the workspace link doesn't break the Vite build. - Regression Tests (Backend): Run
task build(SAM Build) to verify that the backend build pipeline can correctly resolve and bundle the local workspace dependency. - CI/CD: Update
Taskfile.ymlto include the new shared library tests in the globaltask testcommand.
Risk Analysis
- Build Complexity: Adding workspaces can sometimes confuse tooling (ESLint, TSConfig paths).
- Mitigation: We will keep the
sharedpackage extremely simple (just exports, no heavy dependencies).
- Files to Modify:
package.json(Root - enable workspaces)package-lock.json&bun.lock(Regenerated)Taskfile.yml(Add shared test suite)shared/package.json(New)shared/tsconfig.json(New)shared/src/schemas/mealSchema.ts(New)frontend/package.json(Add dependency)functions/import_guest_history/package.json(Add dependency - Future)
Critique & Gaps
- Dependency Hell: Enabling NPM/Bun workspaces in a repo that wasn't designed for it can break existing
installcommands or CI pipelines that expect a flat structure. Specifically,sam buildoften struggles with symlinked local dependencies unless strictly configured. - Over-Engineering: For one file (
meal.ts), creating a full package structure seems heavy. Why not just agit submoduleor a script to copy the file? (Counter-argument: Workspaces are the standard solution; hacks are worse). - Frontend Coupling: If the
sharedpackage imports anything (even utilities) that isn't tree-shakeable or compatible with the frontend bundler (Vite) vs backend (SAM), we break the build. - Deployment Coupling: Sharing code couples release cycles. If we update the schema to make a field required, we effectively force a lock-step deployment. We must deploy the Backend before the Frontend to avoid 400 Bad Request errors.
- The 'Bun' Disconnect: We use
bunlocally, but SAM defaults tonpm. SAM might not respectbun.lockworkspaces or symlinks correctly during the build phase. Risk: The Lambda artifact might be missing thesharedcode ifsam builddoesn't resolve the local workspace dependency. - Schema Rigidity: Introducing Zod often tempts developers to use
.strict(), which rejects unknown fields. This breaks the "Tolerant Reader" pattern (Postel's Law) we currently enjoy. If the Frontend sends a new field (v2) and the Backend is still v1, a.strict()schema will throw 400. Requirement: Use.strip()(default) to silently ignore unknown fields for forward compatibility.
Gap Analysis
- Gap: No explicit step to test
sam buildwith the new workspace structure to ensure the Lambda actually packages the shared code correctly. - Gap: No strategy for keeping
package-lock.jsonin sync for SAM if we are primarily usingbun.lock.
Suggestions to Address Critique
- Keep it Pure: The
sharedpackage must have zero dependencies other thanzod. Noutils, nolodash. - SAM Verification: We must manually verify the
.aws-sam/buildoutput to ensure the code is physically present, not just symlinked.
Execution Plan
Stop: User Approval Required
Do not proceed with execution until the user has explicitly approved the Approach and Execution Plan above.
- Step 1: Initialize
sharedworkspace structure.- Create
shared/package.json(name:@chatkcal/shared). - Create
shared/tsconfig.json.
- Create
- Step 2: Configure Root
package.jsonworkspaces.- Add
workspacesarray. - Run
bun installto link. - Crucial: Run
npm install --package-lock-onlyto generate a rootpackage-lock.jsonfor SAM.
- Add
- Step 3: Implement
MealSchemainshared/src/schemas/mealSchema.ts.- Replicate rules from
ApiMealService. - Requirement: Ensure "Tolerant Reader" pattern (do NOT use
.strict()).
- Replicate rules from
- Step 4: Verify consumption in Frontend.
- Update
frontend/package.json. - Test import in a temporary file.
- Update
- Step 5: Verify consumption in Backend (SAM).
- Create a temporary test Lambda (
functions/test_shared_schema) that requires@chatkcal/shared. - Add it to
template.yaml. - Run
task build(wrapssam build) and inspect.aws-sam/buildto ensure the shared code is physically bundled (not symlinked). - Cleanup: Remove the test function and template entry.
- Create a temporary test Lambda (
Execution Notes
- Status Correction: Note that the status was prematurely set to "Plan Approved" during the drafting phase. The actual approval was received after the Senior Critique was incorporated.
- SAM Build Failure: As predicted in the Senior Critique,
sam buildfailed to resolve the@chatkcal/shareddependency.- Cause: SAM runs builds in an isolated staging directory. Relative paths (
../../shared) inpackage.jsonbreak because the rootsharedfolder is not copied into this staging context. - Failed Attempts: Manually linking
node_modulesand usingnpm --install-linksfailed because the link targets didn't exist in the staging area.
- Cause: SAM runs builds in an isolated staging directory. Relative paths (
User Approval & Key Learnings
Key Learnings
- Monorepo vs. Serverless Sandbox: Standard NPM/Bun workspace linking fails in AWS SAM because SAM isolates the build context, breaking relative paths to root packages.
- Makefile Strategy Failed: The Makefile strategy also failed because SAM moves the source code to a temporary directory (
/tmp/...) before runningmake. The../../sharedpath refers to a location that does not exist in this temporary sandbox. - The Solution (JIT Vendoring): We must physically copy the shared library into the function's directory before invoking
sam build. This "Just-in-Time Vendoring" ensures the code is present within the isolated context. We will manage this viaTaskfile.yml.
Working Snippets for Next Task:
Taskfile.yml (Vendoring Logic):
vendor:shared:
desc: JIT Vendor the shared library
cmds:
- mkdir -p functions/import_guest_history/_shared
- cp -r shared/src/* functions/import_guest_history/_shared/
- cp shared/package.json functions/import_guest_history/_shared/
package.json (Lambda Dependency):
template.yaml (Build Method):
(User to confirm approval and add notes/learnings)