Related: Deployment Overview · Firebase Setup · Updates
After phase 1 the cloud resources exist but are empty. This phase builds the Angular app and pushes code, rules and configuration into them, then creates the first admin user.
npm run localess:deployThat one command picks a project, checks it is one Localess manages, regenerates the local project files from the live project, installs, builds and deploys. The rest of this page explains what it does and how to configure it.
Two things it will refuse to do:
- Deploy to a project without the
localess-managedlabel. A mistyped project id would otherwise install a CMS over something unrelated. If deploy refuses, adopt the project first withnpm run localess:setup -- --project <id>— setup is idempotent and will not change existing infrastructure. - Deploy without asking. It prints the project, region and targets and defaults to no.
Pass
--yesto skip the question, which requires--project.
You do not need a local .env.<project-id> to deploy. If one is missing it is regenerated
from the live project, and the four hand-edited settings start empty with a warning naming
them.
cd functions && npm install && cd ..functions/ is a separate npm package with its own package.json. The root npm install does not
cover it.
Run it from inside functions/, not as npm --prefix functions install: npm treats the current
directory's project as a dependency of the prefix target, so --prefix silently adds
"localess": "file:.." to the tracked functions/package.json. npm run localess:deploy does
the same for this reason.
You do not need to compile functions yourself — firebase.json declares:
"functions": {
"predeploy": "npm --prefix functions run build",
"source": "functions"
}so firebase deploy runs tsc for you.
npm run version:generateWrites src/assets/version.json with the version from package.json, the build date, and
COMMIT_SHA / GITHUB_SHA if present (otherwise local-build). Optional — the file is committed,
so a stale one only means stale version metadata in the UI. npm run localess:deploy does not
run this step (cloudbuild.yaml does); run it yourself first if you want fresh metadata.
There are two production builds, and the difference matters:
| Command | Firebase config it compiles against |
|---|---|
npm run build:prod |
The tracked demo-localess-dev placeholder |
npm run build:deploy |
src/environments/firebase-config.build.json, written by sync for the selected project |
npm run build:deploynpm run localess:deploy and cloudbuild.yaml both use build:deploy. Use build:prod only to check
that a production build compiles — its output points at the demo project and must not be
deployed by hand. build:deploy fails loudly if firebase-config.build.json is absent, so
you cannot accidentally get a demo build out of it.
The swap happens through the deploy configuration in angular.json, which is why that
configuration also repeats the environment.prod.ts replacement: combining configurations is a
shallow override, so production,deploy would otherwise drop it. A test in
scripts/localess/build-config.test.mjs guards that.
Output goes to dist/localess/browser, which is what firebase.json serves as hosting.public.
The LOCALESS_* settings are compile-time constants, inlined into the bundle by esbuild
through Angular's define builder option. There is no generated source file: the identifiers
are declared in src/environments/build-constants.d.ts and read directly by
environment.prod.ts.
Defaults live in the production configuration in angular.json, so a build with no overrides
always works:
| Constant | Default | Example |
|---|---|---|
LOCALESS_REGION |
europe-west6 |
us-central1 |
LOCALESS_AUTH_PROVIDERS |
empty — login page shows Email/Password only | GOOGLE,MICROSOFT |
LOCALESS_AUTH_CUSTOM_DOMAIN |
empty — no custom auth domain restriction | auth.example.com |
LOCALESS_LOGIN_MESSAGE |
empty — no message on the login page | Welcome to Localess |
LOCALESS_UNSPLASH_ENABLE |
empty — Unsplash plugin disabled | true |
npm run localess:deploy overrides them per project, reading .env.<project-id> and passing each one
as a --define flag:
npm run build:deploy -- --define LOCALESS_REGION=\"us-central1\" --define LOCALESS_LOGIN_MESSAGE=\"Welcome\"You do not normally do this by hand — put the values in .env.<project-id> and let
npm run localess:deploy assemble the flags. Because they are passed as arguments rather than read from
the environment, a stale exported shell variable cannot change what gets built.
Note this is not import.meta.env. Angular uses Vite only for the dev server; production
builds go through esbuild, which has no .env file support. define is the Angular-native
equivalent, and it constant-folds — LOCALESS_UNSPLASH_ENABLE === 'true' is resolved to a
literal at build time and the dead branch is dropped.
LOCALESS_REGIONsets the Functions region in four places at once:functions/.env.<project-id>, the/api/v1/**Hosting rewrite, the Angular client's callable region, and the Cloud Build_REGIONsubstitution. Firestore and Storage must be in the same region — Localess uses gen-2 triggers, which have to be co-located with the resource they listen to.
This is the most common first-deploy surprise. Enabling Google sign-in in the Firebase console enables it in the backend; the button only appears if
LOCALESS_AUTH_PROVIDERSincludessrc/app/auth/login/login.component.tsderivesisGoogleAuthEnabled/isMicrosoftAuthEnabledfrom that string.
Because these are build-time values, changing them requires a rebuild and redeploy — not just a config change in the console.
npm run localess:deployOmit --project and you get the same annotated picker setup uses, minus the create option.
| Flag | Effect |
|---|---|
--project <id> |
Skip the picker |
--only <targets> |
Override the default targets (see below) |
--skip-install |
Reuse the installed node_modules |
--skip-build |
Reuse dist/localess/browser |
--dry-run |
Regenerate the local files (and correct the localess-region label), print the build and deploy commands, stop |
--yes |
Skip the confirmation. Requires --project. |
By default it pushes hosting,functions,storage,firestore,auth — the same set as
cloudbuild.yaml, so a local deploy and a CI deploy do the same thing. auth is included
because setup no longer provisions Identity Platform: setup enables the service, deploy applies
the configuration.
One target is excluded on purpose:
| Target | Why it is excluded |
|---|---|
remoteconfig |
Would overwrite console-side edits on every deploy |
Push it explicitly when you need to: npm run localess:deploy -- --only remoteconfig.
--only also accepts extensions and database, which firebase deploy understands but
Localess does not configure in firebase.json.
An unknown target is rejected before anything is touched, so a typo costs nothing.
Deploy never modifies a tracked file: it runs against a generated
firebase.<project-id>.json via the CLI's --config flag, so git status stays clean.
A bare deploy covers every target configured in firebase.json:
| Target | What is pushed |
|---|---|
auth |
Identity Platform providers (idempotent — runs on every deploy) |
firestore |
firestore.rules and firestore.indexes.json |
storage |
storage.rules |
functions |
functions/ — compiled by the predeploy hook |
hosting |
dist/localess/browser + headers and rewrites |
remoteconfig |
remoteconfig.template.json |
The first functions deploy is the slowest part: Cloud Build has to build container images for every function.
Setup enables the 15 Google Cloud APIs Localess needs, but a project can drift after that — it may have been provisioned by an older release that required fewer, or had an API switched off by hand. So deploy re-checks them, and enables whatever is off, before it installs or builds.
The check is a single Service Usage call listing every enabled API, so the normal case — a project
that is already correct — costs one round trip rather than fifteen. It runs after the confirmation,
so a cancelled or --dry-run deploy enables nothing, and before the build, so a drifted project
costs seconds instead of a full production build followed by a failure worded in terms of the
resource that could not be created.
A cancelled or --dry-run deploy is not entirely side-effect free, though: the local project
files have already been regenerated, and the remote localess-region label corrected if it
disagreed with the live Firestore location, before the dry-run exit or the confirmation prompt.
translate.googleapis.com is the one that most needs this: nothing in a deploy touches it — it is
used at runtime by functions/src/services/translate.service.ts — so if it were off, the first sign
would be the translate feature failing in production.
The browser fetches assets straight from Storage, so the default bucket needs CORS rules or those
downloads fail. Deploy checks for them alongside the APIs and applies read-only rules — GET and
HEAD from any origin — when the bucket has none.
Only an empty configuration is filled in. Any existing rule is left untouched, because someone chose
it: a tighter origin list for a locked-down install should not be silently widened to *.
This used to be done by the setup callable in functions/, which set it while creating the first
admin user. That placed it behind the configs/setup guard, so it ran exactly once in a project's
lifetime and no later change to the rules could ever reach an existing install — and it left a window
between deploy and whenever a human opened /setup in which assets simply did not load. It is
ordinary infrastructure, so it lives with the rest of it now.
Setting CORS needs storage.buckets.update. Without it setup warns and carries on rather than
failing, and deploy re-checks every run, so the project heals itself once the role is granted.
A first deploy races the provisioning that precedes it. The gcf-v2-sources bucket and the Eventarc
service agent's permissions are still propagating, so event-triggered functions and publicv1 can
fail to be created. firebase deploy reports these as warnings and still exits 0, so the exit
code alone cannot be trusted — deploy reads the output instead, and retries the whole command up to
three times, 30 seconds apart.
The whole command is retried rather than --only functions on purpose: hosting released against a
missing publicv1 serves a dead /api/v1/** rewrite until it is released again.
If functions are still failing after the third attempt, deploy stops and names them. They almost always succeed on a later run — wait a few minutes and repeat the same command.
When functions are among the targets, deploy first checks the Artifact Registry cleanup policy for
the region, and sets it if it is missing or different, so old container images do not accumulate
into a slow monthly bill. Without a policy the CLI wants to ask about one on every deploy, and
--non-interactive turns that question into a failed deploy of functions that in fact deployed
fine. The step reports what it found:
Checking the function image cleanup policy
+ europe-west6: already deleting images after 1 day(s)
Retention is 1 day, the firebase-tools default — deploy passes no --days, and reports the
number the CLI itself prints rather than keeping a second copy of it. Anything older than 24 hours
in gcf-artifacts is deleted, tagged or not.
Deleting them is safe. These are build artifacts: Cloud Build produces the image, and Cloud Run
functions takes its own copy at deploy time. Firebase's own documentation is explicit — "these
images are not required for your deployed functions to run" — so cold starts and scale-ups are
unaffected, and a 1-day default would otherwise take down every Firebase project a day after it
deployed. What you give up is the build cache (the next deploy rebuilds cold) and the ability to
pull the exact image that shipped. Rollback does not depend on them either: localess:deploy
always rebuilds from source. The function source in the gcf-v2-sources-* bucket is a separate
store and is not touched.
It is a no-op on a brand-new project — the gcf-artifacts repository does not exist until functions
have been deployed once, and the step says no function images yet; nothing to configure — so the
policy takes effect from the second deploy onward.
Failures here are logged and swallowed. The images are already live, so a policy that could not be set is a billing footnote, not a reason to fail a deploy that worked.
npm run localess:check -- --project <project-id> --fixIt asks for an email, a password and a display name, then creates the account, grants it the
role: admin custom claim, and writes its users/{uid} document alongside a seeded
Hello World space.
There is no browser route for this. The /setup wizard and the setup Cloud Function behind
it were removed — that endpoint could not require authentication, so on a freshly deployed
project anyone who knew the project id could claim the administrator account. See
why the CLI creates the admin.
For a scripted deploy, pass --admin-email and set LOCALESS_ADMIN_PASSWORD. There is
deliberately no password flag.
npm run localess:check -- --project <project-id>Reports everything below in one pass, plus the things the table cannot see — most usefully the
allUsers invoker bindings, which a partially failed first deploy silently omits and no later
deploy restores. See Checking an installation.
| Check | How |
|---|---|
| App loads | https://<project-id>.web.app |
| Public API responds | https://<project-id>.web.app/api/v1/... — see V1 API |
| Functions deployed | npx firebase functions:list --project <id> |
| Blocking functions registered | Firebase console → Authentication → Settings → Blocking functions |
| Logs | npx firebase functions:log --project <id> |
The /api/v1/** path is a Hosting rewrite to the publicv1 function. Both the rewrite and
functions/.env.<project-id> are generated from the same LOCALESS_REGION, so they cannot drift apart — if
they did, the rewrite could not resolve the function and every API request would fail.