Skip to content

[2026-01-18] Tech Task: Shared Zod Schema

Task Metadata

  • 📆 Date: 2026-01-18
  • 🚥 Status: Complete
  • 🔗 Related Task: 2512_genai_food_tracking-db8
  • 🔒 Blocker 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 importGuestHistory Lambda.
  • 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.

  1. New Module: Create shared/ at the project root.
  2. Configuration: Configure root package.json to define a workspace containing shared, frontend, and functions/*.
  3. Consumption:
  4. Frontend: Add "@chatkcal/shared": "*" to frontend/package.json.
  5. Backend: Add "@chatkcal/shared": "*" to the Lambda's package.json. SAM/Esbuild should bundle this code into the artifact.

Testing Strategy

  • Unit Tests (New): Install vitest in the shared workspace and implement a test script.
  • Regression Tests (Frontend): Run task test:frontend to 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.yml to include the new shared library tests in the global task test command.

Risk Analysis

  • Build Complexity: Adding workspaces can sometimes confuse tooling (ESLint, TSConfig paths).
  • Mitigation: We will keep the shared package 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

  1. Dependency Hell: Enabling NPM/Bun workspaces in a repo that wasn't designed for it can break existing install commands or CI pipelines that expect a flat structure. Specifically, sam build often struggles with symlinked local dependencies unless strictly configured.
  2. Over-Engineering: For one file (meal.ts), creating a full package structure seems heavy. Why not just a git submodule or a script to copy the file? (Counter-argument: Workspaces are the standard solution; hacks are worse).
  3. Frontend Coupling: If the shared package imports anything (even utilities) that isn't tree-shakeable or compatible with the frontend bundler (Vite) vs backend (SAM), we break the build.
  4. 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.
  5. The 'Bun' Disconnect: We use bun locally, but SAM defaults to npm. SAM might not respect bun.lock workspaces or symlinks correctly during the build phase. Risk: The Lambda artifact might be missing the shared code if sam build doesn't resolve the local workspace dependency.
  6. 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 build with the new workspace structure to ensure the Lambda actually packages the shared code correctly.
  • Gap: No strategy for keeping package-lock.json in sync for SAM if we are primarily using bun.lock.

Suggestions to Address Critique

  • Keep it Pure: The shared package must have zero dependencies other than zod. No utils, no lodash.
  • SAM Verification: We must manually verify the .aws-sam/build output 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 shared workspace structure.
    • Create shared/package.json (name: @chatkcal/shared).
    • Create shared/tsconfig.json.
  • Step 2: Configure Root package.json workspaces.
    • Add workspaces array.
    • Run bun install to link.
    • Crucial: Run npm install --package-lock-only to generate a root package-lock.json for SAM.
  • Step 3: Implement MealSchema in shared/src/schemas/mealSchema.ts.
    • Replicate rules from ApiMealService.
    • Requirement: Ensure "Tolerant Reader" pattern (do NOT use .strict()).
  • Step 4: Verify consumption in Frontend.
    • Update frontend/package.json.
    • Test import in a temporary file.
  • 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 (wraps sam build) and inspect .aws-sam/build to ensure the shared code is physically bundled (not symlinked).
    • Cleanup: Remove the test function and template entry.

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 build failed to resolve the @chatkcal/shared dependency.
    • Cause: SAM runs builds in an isolated staging directory. Relative paths (../../shared) in package.json break because the root shared folder is not copied into this staging context.
    • Failed Attempts: Manually linking node_modules and using npm --install-links failed because the link targets didn't exist in the staging area.

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 running make. The ../../shared path 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 via Taskfile.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):

{
  "dependencies": {
    "@chatkcal/shared": "file:./_shared"
  }
}

template.yaml (Build Method):

Metadata:
  BuildMethod: esbuild
  BuildProperties:
    Minify: true
    Target: es2020
    Sourcemap: true
    EntryPoints:
      - app.ts

(User to confirm approval and add notes/learnings)