Skip to content

Workspace Dependencies: Firebase Functions vs API Services โ€‹

This guide explains how local workspace packages (@lantern/shared, @lantern/forge) are resolved across the two deployment platforms in this monorepo.

The Two Patterns โ€‹

Firebase FunctionsAPI Services (Cloud Run)
Deploy toolFirebase CLIgcloud CLI
What gets uploadedOnly services/functions/firebase/Service directory + copied packages
package.json referencesfile:../../../packages/shared (workspace paths)file:../../../packages/shared (workspace paths)
Deploy-time transformationpack-workspace-deps.sh creates .tgz tarballs and temporarily patches package.jsonCI copies packages into service directory with cp -r; the Dockerfile sed-rewrites the file: paths

Both patterns use workspace-relative paths (file:../../../packages/shared) in their committed package.json. The deploy-time transformation is temporary and never committed.

Firebase Functions โ€‹

Firebase CLI uploads only the services/functions/firebase/ directory to Cloud Build. It cannot follow file:../../../packages/shared symlinks because the parent monorepo structure is not included.

Solution: A predeploy hook (firebase.json) runs pack-workspace-deps.sh which:

  1. Runs npm pack on packages/shared and packages/forge to create self-contained .tgz tarballs
  2. Temporarily patches package.json to reference the tarballs (file:lantern-shared-0.1.0.tgz)
  3. Firebase CLI reads the patched version and uploads it with the tarballs
  4. Restores package.json to workspace paths via an EXIT trap

The .tgz files are gitignored (services/functions/firebase/.gitignore).

Do not commit tarball references โ€‹

The committed package.json must always use workspace paths:

json
"@lantern/shared": "file:../../../packages/shared",
"@lantern/forge": "file:../../../packages/forge"

Never commit tarball references like file:lantern-shared-0.1.0.tgz. This breaks:

  • npm ci on fresh clones (tarballs are gitignored)
  • CI pipelines (no tarballs available)
  • Emulator runs (firebase emulators:start)

If you see tarball references in a diff, the restore step in pack-workspace-deps.sh did not run. Revert the package.json to workspace paths.

API Services (Cloud Run) โ€‹

Cloud Run services use gcloud run deploy --source, which uploads the service directory. The CI workflow (and local deploy scripts) copy the shared packages into the service directory before deploy:

bash
# From deploy-dev.yml / local deploy:dev script
cp -r packages/shared services/api/auth/.shared-pkg
gcloud run deploy auth-api --source services/api/auth ...
rm -rf services/api/auth/.shared-pkg

The copy alone is not enough: inside the Docker build context the monorepo does not exist, so file:../../../packages/shared points at nothing. Each service's Dockerfile sed-rewrites the file: paths onto the copied directories before npm install runs (see services/api/analytics/Dockerfile lines 13-15, which repoint @lantern/shared and @lantern/forge at ./.shared-pkg / ./.forge-pkg, plus forge's own nested dep on shared). Adding a workspace dep to a service therefore requires the matching Dockerfile sed line, and forgetting it is a deploy-only failure: npm run validate and CI's test jobs run in the monorepo where the workspace path resolves fine, so nothing surfaces the gap until the Cloud Build image build breaks.

Lockfile Updates โ€‹

When a dependency change touches package-lock.json, prefer the minimal update (npm install <pkg> -w <workspace>, or plain npm install after editing one package.json) over deleting the lockfile and regenerating. A full regen churns thousands of lines, silently re-resolves unrelated transitive versions, and buries the real change in the diff. Verify the result with npm ci: it installs strictly from the lockfile and fails if the lockfile and the package.jsons disagree.

Key Files โ€‹

FilePurpose
services/functions/firebase/pack-workspace-deps.shPredeploy: pack tarballs, patch + restore package.json
firebase.json (functions.predeploy)Triggers pack script before deploy
services/functions/firebase/.gitignoreIgnores *.tgz tarballs
.github/workflows/deploy-dev.ymlCI: copies packages for Cloud Run deploys
services/api/*/package.json (deploy:dev script)Local: copies packages for Cloud Run deploys

Emulators โ€‹

Firebase emulators (firebase emulators:start --only functions) run functions locally and need workspace-relative paths to resolve. This is another reason the committed package.json must use file:../../../packages/shared, not tarballs.

Built with VitePress