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 Functions | API Services (Cloud Run) | |
|---|---|---|
| Deploy tool | Firebase CLI | gcloud CLI |
| What gets uploaded | Only services/functions/firebase/ | Service directory + copied packages |
| package.json references | file:../../../packages/shared (workspace paths) | file:../../../packages/shared (workspace paths) |
| Deploy-time transformation | pack-workspace-deps.sh creates .tgz tarballs and temporarily patches package.json | CI 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:
- Runs
npm packonpackages/sharedandpackages/forgeto create self-contained.tgztarballs - Temporarily patches
package.jsonto reference the tarballs (file:lantern-shared-0.1.0.tgz) - Firebase CLI reads the patched version and uploads it with the tarballs
- Restores
package.jsonto 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:
"@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 cion 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:
# 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-pkgThe 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 โ
| File | Purpose |
|---|---|
services/functions/firebase/pack-workspace-deps.sh | Predeploy: pack tarballs, patch + restore package.json |
firebase.json (functions.predeploy) | Triggers pack script before deploy |
services/functions/firebase/.gitignore | Ignores *.tgz tarballs |
.github/workflows/deploy-dev.yml | CI: 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.